Files
gstack/docs/ADDING_A_HOST.md
T
Garry TanandClaude Fable 5 60e51342b5 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>
2026-08-18 17:00:04 -07:00

7.3 KiB

Adding a New Host to gstack

gstack uses a declarative host config system. Each supported AI coding agent (Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Hermes, GBrain) is defined as a typed TypeScript config object built by the defineHost() factory. Adding a new host means creating one file and re-exporting it. Zero code changes to the generator, setup, or tooling.

How it works

hosts/
├── define-host.ts   # defineHost() factory: shared defaults + derived fields
├── claude.ts        # Primary host
├── codex.ts         # OpenAI Codex CLI
├── factory.ts       # Factory Droid
├── kiro.ts          # Amazon Kiro
├── opencode.ts      # OpenCode
├── slate.ts         # Slate (Random Labs)
├── cursor.ts        # Cursor
├── openclaw.ts      # OpenClaw
├── hermes.ts        # Hermes (Nous Research)
├── gbrain.ts        # GBrain
└── index.ts         # Registry: imports all, derives Host type

Each config file calls defineHost() and exports the resulting HostConfig object, which tells the generator:

  • Where to put generated skills (paths)
  • How to transform frontmatter (allowlist/denylist fields)
  • What Claude-specific references to rewrite (paths, tool names)
  • What binary to detect for auto-install
  • What resolver sections to suppress
  • What assets to symlink at install time

The generator, setup script, platform-detect, uninstall, health checks, worktree copy, and tests all read from these configs. None of them have per-host code.

Step-by-step: add a new host

1. Create the config file

Configs are built with the defineHost() factory in hosts/define-host.ts. You only write the fields that differ from the common external-host defaults; everything else is derived from the host name. A fully-default host is two fields (see hosts/slate.ts or hosts/cursor.ts):

import { defineHost } from './define-host';

const myhost = defineHost({
  name: 'myhost',
  displayName: 'MyHost',
});

export default myhost;

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
  • generation: no metadata file, skipSkills: ['codex'] (codex skill is Claude-only)
  • pathRewrites: the standard trio derived from the resolved paths (~/.claude/skills/gstack~/{globalRoot}, .claude/skills/gstack{localSkillRoot}, .claude/skills{hostSubdir}/skills)
  • suppressedResolvers: the GBrain pair (GBRAIN_CONTEXT_LOAD, GBRAIN_SAVE_RESULTS)
  • runtimeRoot: the shared asset list (bin, browse/dist, browse/bin, gstack-upgrade, ETHOS.md + review checklist files)
  • install: { linkingStrategy: 'symlink-generated' }
  • learningsMode: 'basic'

Override any field by passing it to defineHost(). Two path-rewrite options:

  • extraPathRewrites: appends entries AFTER the derived trio (e.g. kiro's codex-path cleanup, or { from: 'CLAUDE.md', to: 'AGENTS.md' } for AGENTS.md hosts). Use this when the standard trio is right but you need more.
  • pathRewrites: replaces the derived list entirely. Only for non-mechanical cases — codex and factory rewrite the global path to $GSTACK_ROOT and add an extra review-path rewrite; claude has an empty list.

The two are mutually exclusive (the factory throws if you pass both).

Shared constants exported from define-host.ts for spread-composition: CROSS_MODEL_RESOLVERS (the five Codex-invoking resolvers suppressed on hosts that can't invoke other models), GBRAIN_RESOLVERS (the default suppression pair), and EXEC_STYLE_TOOL_REWRITES (the OpenClaw-style lowercase-tool rewrites shared by openclaw and gbrain).

Good examples: hosts/opencode.ts (path + runtimeRoot overrides), hosts/factory.ts (tool rewrites and conditional fields), hosts/hermes.ts (AGENTS.md host with custom tool rewrites and resolver composition).

2. Register in the index

Edit hosts/index.ts:

import myhost from './myhost';

// Add to ALL_HOST_CONFIGS array:
export const ALL_HOST_CONFIGS: HostConfig[] = [
  claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, myhost
];

// Add to re-exports:
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, myhost };

3. Add to .gitignore

Add .myhost/ to .gitignore (generated skill docs are gitignored).

4. Generate and verify

# Generate skill docs for the new host
bun run gen:skill-docs --host myhost

# Verify output exists and has no .claude/skills leakage
ls .myhost/skills/gstack-*/SKILL.md
grep -r ".claude/skills" .myhost/skills/ | head -5
# (should be empty)

# Generate for all hosts (includes the new one)
bun run gen:skill-docs --host all

# Health dashboard shows the new host
bun run skill:check

5. Run tests

bun test test/gen-skill-docs.test.ts
bun test test/host-config.test.ts

The parameterized smoke tests automatically pick up the new host. Zero test code to write. They verify: output exists, no path leakage, valid frontmatter, freshness check passes, codex skill excluded.

6. Update README.md

Add install instructions for the new host in the appropriate section.

Config field reference

See scripts/host-config.ts for the full HostConfig interface with JSDoc comments on every field.

Key fields:

Field Purpose
defaultModel Model overlay rendered when generation gets no explicit --model (validated against ALL_MODEL_NAMES in scripts/models.ts)
frontmatter.mode allowlist (keep only listed) or denylist (strip listed)
frontmatter.descriptionLimit Max chars, null for no limit
frontmatter.descriptionLimitBehavior error (fail build), truncate, warn
frontmatter.conditionalFields Add fields based on template values (e.g., sensitive → disable-model-invocation)
frontmatter.renameFields Rename template fields (e.g., voice-triggers → triggers)
pathRewrites Literal replaceAll on content. Order matters. Replaces the derived trio.
extraPathRewrites (defineHost input only) Appended after the derived trio.
toolRewrites Rewrite Claude tool names (e.g., "use the Bash tool" → "run this command")
suppressedResolvers Resolver functions that return empty for this host
coAuthorTrailer Git co-author string for commits
boundaryInstruction Anti-prompt-injection warning for cross-model invocations

Validation

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

Run bun run scripts/host-config-export.ts validate to check all configs.