mirror of https://github.com/garrytan/gstack.git
fix(hosts): default Codex generation to GPT overlay
This commit is contained in:
parent
a3259400a3
commit
46ee7dc4ce
|
|
@ -10,6 +10,7 @@ const claude: HostConfig = {
|
||||||
localSkillRoot: '.claude/skills/gstack',
|
localSkillRoot: '.claude/skills/gstack',
|
||||||
hostSubdir: '.claude',
|
hostSubdir: '.claude',
|
||||||
usesEnvVars: false,
|
usesEnvVars: false,
|
||||||
|
defaultModel: 'claude',
|
||||||
|
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
mode: 'denylist',
|
mode: 'denylist',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ const codex: HostConfig = {
|
||||||
localSkillRoot: '.agents/skills/gstack',
|
localSkillRoot: '.agents/skills/gstack',
|
||||||
hostSubdir: '.agents',
|
hostSubdir: '.agents',
|
||||||
usesEnvVars: true,
|
usesEnvVars: true,
|
||||||
|
defaultModel: 'gpt',
|
||||||
|
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
mode: 'allowlist',
|
mode: 'allowlist',
|
||||||
|
|
|
||||||
|
|
@ -90,12 +90,13 @@ const HOST_ARG_VAL: HostArg = (() => {
|
||||||
let HOST: Host = HOST_ARG_VAL === 'all' ? 'claude' : HOST_ARG_VAL;
|
let HOST: Host = HOST_ARG_VAL === 'all' ? 'claude' : HOST_ARG_VAL;
|
||||||
|
|
||||||
// ─── Model Overlay Selection ────────────────────────────────
|
// ─── Model Overlay Selection ────────────────────────────────
|
||||||
// --model is explicit. We do NOT auto-detect from host (host ≠ model).
|
// --model is explicit and wins over host configuration. A host may declare a
|
||||||
// Default is 'claude'. Missing overlay file → empty string (graceful).
|
// defaultModel; hosts without one retain the legacy Claude fallback. Missing
|
||||||
|
// overlay file → empty string (graceful).
|
||||||
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
|
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
|
||||||
const MODEL_ARG = process.argv.find(a => a.startsWith('--model'));
|
const MODEL_ARG = process.argv.find(a => a.startsWith('--model'));
|
||||||
const MODEL_ARG_VAL: Model = (() => {
|
const EXPLICIT_MODEL: Model | null = (() => {
|
||||||
if (!MODEL_ARG) return 'claude';
|
if (!MODEL_ARG) return null;
|
||||||
const val = MODEL_ARG.includes('=') ? MODEL_ARG.split('=')[1] : process.argv[process.argv.indexOf(MODEL_ARG) + 1];
|
const val = MODEL_ARG.includes('=') ? MODEL_ARG.split('=')[1] : process.argv[process.argv.indexOf(MODEL_ARG) + 1];
|
||||||
const resolved = resolveModel(val);
|
const resolved = resolveModel(val);
|
||||||
if (!resolved) {
|
if (!resolved) {
|
||||||
|
|
@ -104,6 +105,10 @@ const MODEL_ARG_VAL: Model = (() => {
|
||||||
return resolved;
|
return resolved;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
function modelForHost(hostConfig: HostConfig): Model {
|
||||||
|
return EXPLICIT_MODEL ?? hostConfig.defaultModel ?? 'claude';
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Catalog Mode (v1.45.0.0 T4) ────────────────────────────
|
// ─── Catalog Mode (v1.45.0.0 T4) ────────────────────────────
|
||||||
// 'trim' (default): shorten frontmatter description to lead sentence,
|
// 'trim' (default): shorten frontmatter description to lead sentence,
|
||||||
// move routing/voice prose into a "## When to invoke" body section, and
|
// move routing/voice prose into a "## When to invoke" body section, and
|
||||||
|
|
@ -731,7 +736,7 @@ function buildContext(
|
||||||
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined;
|
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined;
|
||||||
return {
|
return {
|
||||||
skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host],
|
skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host],
|
||||||
preambleTier, model: MODEL_ARG_VAL, interactive, explainLevel: EXPLAIN_LEVEL,
|
preambleTier, model: modelForHost(getHostConfig(host)), interactive, explainLevel: EXPLAIN_LEVEL,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@
|
||||||
* platform-detect, uninstall
|
* platform-detect, uninstall
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { validateModel, type Model } from './models';
|
||||||
|
|
||||||
export interface HostConfig {
|
export interface HostConfig {
|
||||||
/** Unique host identifier (e.g., 'opencode'). Must match filename in hosts/. */
|
/** Unique host identifier (e.g., 'opencode'). Must match filename in hosts/. */
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -33,6 +35,8 @@ export interface HostConfig {
|
||||||
hostSubdir: string;
|
hostSubdir: string;
|
||||||
/** Whether preamble generates $GSTACK_ROOT env vars (true for non-Claude hosts). */
|
/** Whether preamble generates $GSTACK_ROOT env vars (true for non-Claude hosts). */
|
||||||
usesEnvVars: boolean;
|
usesEnvVars: boolean;
|
||||||
|
/** Default model overlay when --model is omitted. Legacy fallback is Claude. */
|
||||||
|
defaultModel?: Model;
|
||||||
|
|
||||||
// --- Frontmatter Transformation ---
|
// --- Frontmatter Transformation ---
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
|
|
@ -151,6 +155,10 @@ export function validateHostConfig(config: HostConfig, validResolverNames?: Read
|
||||||
if (!['real-dir-symlink', 'symlink-generated'].includes(config.install.linkingStrategy)) {
|
if (!['real-dir-symlink', 'symlink-generated'].includes(config.install.linkingStrategy)) {
|
||||||
errors.push(`install.linkingStrategy must be 'real-dir-symlink' or 'symlink-generated'`);
|
errors.push(`install.linkingStrategy must be 'real-dir-symlink' or 'symlink-generated'`);
|
||||||
}
|
}
|
||||||
|
if (config.defaultModel) {
|
||||||
|
const modelError = validateModel(config.defaultModel);
|
||||||
|
if (modelError) errors.push(`defaultModel ${modelError}`);
|
||||||
|
}
|
||||||
|
|
||||||
// Cross-check suppressedResolvers against the known resolver names (injected to avoid a
|
// Cross-check suppressedResolvers against the known resolver names (injected to avoid a
|
||||||
// circular import on the resolver registry). A typo would otherwise silently no-op: the
|
// circular import on the resolver registry). A typo would otherwise silently no-op: the
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,11 @@
|
||||||
*
|
*
|
||||||
* Model families supported by model overlays in model-overlays/{family}.md.
|
* Model families supported by model overlays in model-overlays/{family}.md.
|
||||||
* Host configs can reference these as `defaultModel` strings (validated at
|
* Host configs can reference these as `defaultModel` strings (validated at
|
||||||
* generation time), but the model axis is independent of the host axis.
|
* config load time), but the model axis is independent of the host axis.
|
||||||
*
|
*
|
||||||
* IMPORTANT: host ≠ model. Claude Code can run any Claude model (Opus, Sonnet,
|
* IMPORTANT: host ≠ model. Host defaults are declarative fallbacks, not runtime
|
||||||
* Haiku, future). Codex CLI runs GPT/o-series models. Cursor and OpenCode can
|
* model detection. An explicit --model always wins, and hosts without a
|
||||||
* front multiple providers. We do NOT auto-detect the model from the host —
|
* configured default retain the legacy 'claude' fallback.
|
||||||
* users pass --model explicitly. Default is 'claude'.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ALL_MODEL_NAMES = [
|
export const ALL_MODEL_NAMES = [
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ if [ -d ".agents/skills/gstack" ] && [ ! -L ".agents/skills/gstack" ]; then
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
echo "VENDORED_GSTACK: $_VENDORED"
|
echo "VENDORED_GSTACK: $_VENDORED"
|
||||||
echo "MODEL_OVERLAY: claude"
|
echo "MODEL_OVERLAY: gpt"
|
||||||
_CHECKPOINT_MODE=$($GSTACK_BIN/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit")
|
_CHECKPOINT_MODE=$($GSTACK_BIN/gstack-config get checkpoint_mode 2>/dev/null || echo "explicit")
|
||||||
_CHECKPOINT_PUSH=$($GSTACK_BIN/gstack-config get checkpoint_push 2>/dev/null || echo "false")
|
_CHECKPOINT_PUSH=$($GSTACK_BIN/gstack-config get checkpoint_push 2>/dev/null || echo "false")
|
||||||
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
|
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
|
||||||
|
|
@ -569,23 +569,45 @@ At skill END before telemetry:
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Model-Specific Behavioral Patch (claude)
|
## Model-Specific Behavioral Patch (gpt)
|
||||||
|
|
||||||
The following nudges are tuned for the claude model family. They are
|
The following nudges are tuned for the gpt model family. They are
|
||||||
**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
|
**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
|
||||||
safety, and /ship review gates. If a nudge below conflicts with skill instructions,
|
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.
|
||||||
|
|
||||||
**Todo-list discipline.** When working through a multi-step plan, mark each task
|
**Completion bias.** Do not end your turn with a partial solution when the full
|
||||||
complete individually as you finish it. Do not batch-complete at the end. If a task
|
solution is reachable. If you encounter an error, debug it. If a test fails, fix it.
|
||||||
turns out to be unnecessary, mark it skipped with a one-line reason.
|
If something is ambiguous, make your best judgment and proceed — don't stop and ask
|
||||||
|
unless you're genuinely blocked.
|
||||||
|
|
||||||
**Think before heavy actions.** For complex operations (refactors, migrations,
|
**Prefer doing over listing.** When you'd be tempted to write "you could also try X,
|
||||||
non-trivial new features), briefly state your approach before executing. This lets
|
Y, or Z," try the best option yourself. Pick, execute, report results.
|
||||||
the user course-correct cheaply instead of mid-flight.
|
|
||||||
|
|
||||||
**Dedicated tools over Bash.** Prefer Read, Edit, Write, Glob, Grep over shell
|
**No preamble.** Skip "Great question!", "Let me help with that", and restating the
|
||||||
equivalents (cat, sed, find, grep). The dedicated tools are cheaper and clearer.
|
user's request. Start with the work.
|
||||||
|
|
||||||
|
**AskUserQuestion is NOT preamble.** The "No preamble" and "Prefer doing over listing"
|
||||||
|
rules above do NOT apply to AskUserQuestion content. When you invoke AskUserQuestion,
|
||||||
|
the user is about to make a decision — they need context, not terseness. Always emit
|
||||||
|
the full format from the preamble's AskUserQuestion Format section:
|
||||||
|
|
||||||
|
1. **Re-ground** (project + branch + task — 1-2 sentences).
|
||||||
|
2. **Simplify (ELI10)** — explain what's happening in plain English a 16-year-old could
|
||||||
|
follow. Concrete stakes, not abstract tradeoffs. Non-negotiable; this is NOT preamble.
|
||||||
|
3. **Recommend** — `RECOMMENDATION: Choose [X] because [one-line reason]` on its own
|
||||||
|
line. Never omit this line. Never collapse it into the options list.
|
||||||
|
4. **Options** — lettered `A) B) C)` with Completeness scores (coverage-differentiated)
|
||||||
|
or the "options differ in kind" note (kind-differentiated).
|
||||||
|
|
||||||
|
If you find yourself about to present an AskUserQuestion without the Simplify/ELI10
|
||||||
|
paragraph, without a RECOMMENDATION line, or by just listing options and asking "which
|
||||||
|
one?" — stop, back up, and emit the full format. The user will ask you to do it anyway,
|
||||||
|
so do it the first time.
|
||||||
|
|
||||||
|
**Reminder: subordination applies.** When a skill workflow says STOP, stop. When the
|
||||||
|
skill asks via AskUserQuestion, that is the wait-for-user gate, not an ambiguity.
|
||||||
|
Completion bias does not override safety gates.
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1705,6 +1705,28 @@ describe('Codex generation (--host codex)', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('--host codex defaults to the GPT model overlay', () => {
|
||||||
|
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||||
|
expect(content).toContain('MODEL_OVERLAY: gpt');
|
||||||
|
expect(content).not.toContain('MODEL_OVERLAY: claude');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('explicit --model overrides the Codex host default', () => {
|
||||||
|
try {
|
||||||
|
const result = Bun.spawnSync(
|
||||||
|
['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex', '--model', 'claude'],
|
||||||
|
{ cwd: ROOT, stdout: 'pipe', stderr: 'pipe' },
|
||||||
|
);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const content = fs.readFileSync(path.join(AGENTS_DIR, 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||||
|
expect(content).toContain('MODEL_OVERLAY: claude');
|
||||||
|
} finally {
|
||||||
|
Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'codex'], {
|
||||||
|
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('root gstack bundle has OpenAI metadata for Codex skill browsing', () => {
|
test('root gstack bundle has OpenAI metadata for Codex skill browsing', () => {
|
||||||
const rootMetadata = path.join(ROOT, 'agents', 'openai.yaml');
|
const rootMetadata = path.join(ROOT, 'agents', 'openai.yaml');
|
||||||
expect(fs.existsSync(rootMetadata)).toBe(true);
|
expect(fs.existsSync(rootMetadata)).toBe(true);
|
||||||
|
|
@ -2249,6 +2271,20 @@ describe('--host all', () => {
|
||||||
expect(output).toContain(`FRESH: ${hostConfig.hostSubdir}/skills/`);
|
expect(output).toContain(`FRESH: ${hostConfig.hostSubdir}/skills/`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('--host all resolves defaults independently for each host', () => {
|
||||||
|
const result = Bun.spawnSync(['bun', 'run', 'scripts/gen-skill-docs.ts', '--host', 'all'], {
|
||||||
|
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
|
||||||
|
const claudeSkill = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
|
||||||
|
const codexSkill = fs.readFileSync(path.join(ROOT, '.agents', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||||
|
const factorySkill = fs.readFileSync(path.join(ROOT, '.factory', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
|
||||||
|
expect(claudeSkill).toContain('MODEL_OVERLAY: claude');
|
||||||
|
expect(codexSkill).toContain('MODEL_OVERLAY: gpt');
|
||||||
|
expect(factorySkill).toContain('MODEL_OVERLAY: claude');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Setup script validation ─────────────────────────────────
|
// ─── Setup script validation ─────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,18 @@ describe('validateHostConfig', () => {
|
||||||
expect(validateHostConfig(c).some(e => e.includes('linkingStrategy'))).toBe(true);
|
expect(validateHostConfig(c).some(e => e.includes('linkingStrategy'))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('valid defaultModel passes', () => {
|
||||||
|
const c = makeValid();
|
||||||
|
c.defaultModel = 'gpt';
|
||||||
|
expect(validateHostConfig(c)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invalid defaultModel is caught at runtime', () => {
|
||||||
|
const c = makeValid();
|
||||||
|
(c as any).defaultModel = 'not-a-model';
|
||||||
|
expect(validateHostConfig(c).some(e => e.includes('defaultModel'))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test('paths with $ and ~ are valid', () => {
|
test('paths with $ and ~ are valid', () => {
|
||||||
const c = makeValid();
|
const c = makeValid();
|
||||||
c.globalRoot = '$HOME/.test/skills/gstack';
|
c.globalRoot = '$HOME/.test/skills/gstack';
|
||||||
|
|
@ -481,6 +493,11 @@ describe('host config correctness', () => {
|
||||||
expect(codex.generation.metadataFormat).toBe('openai.yaml');
|
expect(codex.generation.metadataFormat).toBe('openai.yaml');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Claude and Codex declare host-specific model defaults', () => {
|
||||||
|
expect(claude.defaultModel).toBe('claude');
|
||||||
|
expect(codex.defaultModel).toBe('gpt');
|
||||||
|
});
|
||||||
|
|
||||||
test('codex has sidecar config', () => {
|
test('codex has sidecar config', () => {
|
||||||
expect(codex.sidecar).toBeDefined();
|
expect(codex.sidecar).toBeDefined();
|
||||||
expect(codex.sidecar!.path).toBe('.agents/skills/gstack');
|
expect(codex.sidecar!.path).toBe('.agents/skills/gstack');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue