mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 22:48:57 +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.
This commit is contained in:
@@ -60,6 +60,7 @@ That expands to the full `HostConfig` with these defaults:
|
||||
|
||||
- `cliCommand: 'myhost'` (the name; binary for `command -v` detection)
|
||||
- `cliAliases: []`
|
||||
- `defaultModel: 'claude'` (model overlay used when generation gets no explicit `--model`; codex overrides to `'gpt'`)
|
||||
- `globalRoot` / `localSkillRoot`: `.myhost/skills/gstack`, `hostSubdir`: `.myhost`
|
||||
- `usesEnvVars: true` (false only for Claude, which uses literal `~` paths)
|
||||
- `frontmatter`: allowlist keeping `name` + `description`, no description limit
|
||||
@@ -173,6 +174,7 @@ Key fields:
|
||||
The `validateHostConfig()` function in `scripts/host-config.ts` checks:
|
||||
- Name: lowercase alphanumeric with hyphens
|
||||
- CLI command: alphanumeric with hyphens/underscores
|
||||
- `defaultModel`: must be a known model family from `scripts/models.ts` `ALL_MODEL_NAMES`
|
||||
- Paths: safe characters only (alphanumeric, `.`, `/`, `$`, `{}`, `~`, `-`, `_`)
|
||||
- No duplicate names, hostSubdirs, or globalRoots across configs
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ const codex = defineHost({
|
||||
name: 'codex',
|
||||
displayName: 'OpenAI Codex CLI',
|
||||
cliAliases: ['agents'],
|
||||
defaultModel: 'gpt',
|
||||
|
||||
localSkillRoot: '.agents/skills/gstack',
|
||||
hostSubdir: '.agents',
|
||||
|
||||
@@ -86,6 +86,7 @@ export function defineHost<const N extends string>(overrides: HostOverrides<N>):
|
||||
displayName,
|
||||
cliCommand = name,
|
||||
cliAliases = [],
|
||||
defaultModel = 'claude',
|
||||
globalRoot = `.${name}/skills/gstack`,
|
||||
localSkillRoot = `.${name}/skills/gstack`,
|
||||
hostSubdir = `.${name}`,
|
||||
@@ -140,6 +141,7 @@ export function defineHost<const N extends string>(overrides: HostOverrides<N>):
|
||||
displayName,
|
||||
cliCommand,
|
||||
cliAliases,
|
||||
defaultModel,
|
||||
globalRoot,
|
||||
localSkillRoot,
|
||||
hostSubdir,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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';
|
||||
|
||||
+33
-11
@@ -113,7 +113,7 @@ if [ -d ".agents/skills/gstack" ] && [ ! -L ".agents/skills/gstack" ]; then
|
||||
fi
|
||||
fi
|
||||
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_PUSH=$($GSTACK_BIN/gstack-config get checkpoint_push 2>/dev/null || echo "false")
|
||||
echo "CHECKPOINT_MODE: $_CHECKPOINT_MODE"
|
||||
@@ -579,23 +579,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
|
||||
safety, and /ship review gates. If a nudge below conflicts with skill instructions,
|
||||
the skill wins. Treat these as preferences, not rules.
|
||||
|
||||
**Todo-list discipline.** When working through a multi-step plan, mark each task
|
||||
complete individually as you finish it. Do not batch-complete at the end. If a task
|
||||
turns out to be unnecessary, mark it skipped with a one-line reason.
|
||||
**Completion bias.** Do not end your turn with a partial solution when the full
|
||||
solution is reachable. If you encounter an error, debug it. If a test fails, fix it.
|
||||
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,
|
||||
non-trivial new features), briefly state your approach before executing. This lets
|
||||
the user course-correct cheaply instead of mid-flight.
|
||||
**Prefer doing over listing.** When you'd be tempted to write "you could also try X,
|
||||
Y, or Z," try the best option yourself. Pick, execute, report results.
|
||||
|
||||
**Dedicated tools over Bash.** Prefer Read, Edit, Write, Glob, Grep over shell
|
||||
equivalents (cat, sed, find, grep). The dedicated tools are cheaper and clearer.
|
||||
**No preamble.** Skip "Great question!", "Let me help with that", and restating the
|
||||
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
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ describe('validateHostConfig', () => {
|
||||
name: 'test-host',
|
||||
displayName: 'Test Host',
|
||||
cliCommand: 'testcli',
|
||||
defaultModel: 'claude',
|
||||
globalRoot: '.test/skills/gstack',
|
||||
localSkillRoot: '.test/skills/gstack',
|
||||
hostSubdir: '.test',
|
||||
@@ -165,6 +166,12 @@ describe('validateHostConfig', () => {
|
||||
expect(validateHostConfig(c)).toEqual([]);
|
||||
});
|
||||
|
||||
test('invalid defaultModel is caught', () => {
|
||||
const c = makeValid();
|
||||
(c as any).defaultModel = 'llama-local';
|
||||
expect(validateHostConfig(c).some(e => e.includes('defaultModel'))).toBe(true);
|
||||
});
|
||||
|
||||
test('invalid globalRoot is caught', () => {
|
||||
const c = makeValid();
|
||||
c.globalRoot = 'path with spaces';
|
||||
@@ -470,6 +477,13 @@ describe('golden-file regression', () => {
|
||||
// ─── Individual host config correctness ─────────────────────
|
||||
|
||||
describe('host config correctness', () => {
|
||||
test('Codex defaults to generic GPT while all existing hosts retain Claude', () => {
|
||||
expect(codex.defaultModel).toBe('gpt');
|
||||
for (const host of ALL_HOST_CONFIGS.filter(h => h.name !== 'codex')) {
|
||||
expect(host.defaultModel).toBe('claude');
|
||||
}
|
||||
});
|
||||
|
||||
test('claude is the only host with real-dir-symlink strategy', () => {
|
||||
for (const config of ALL_HOST_CONFIGS) {
|
||||
if (config.name === 'claude') {
|
||||
|
||||
Reference in New Issue
Block a user