Files
gstack/docs/ADDING_A_HOST.md
T
82530ac143 v1.91.117.0 fix: pregate bats lanes, WTREE_UNAVAILABLE, doctor rows, SKILL.md size guard, planMode hosts (fix wave PR 5) (#3236)
* fix(pregate): recognise bats test files so lanes does not fail in bats repos (#3161)

gstack-pregate never treated a .bats file as a test, so in a repository whose
tests are bats files the lanes check failed every run with PREGATE_NO_LANE: a
.gstack/pregate.json dependency pointing at tests/foo.bats selected nothing, a
selection manifest printing .bats paths yielded 0 files, and a bats run that
executed no tests could not trip the zero-run rule.

Recognise .bats in the test-file pattern and in the manifest-output parser, and
read the bats TAP plan and pretty summary (as a last resort, after every
existing runner's summary) so a run that reports zero tests is a failure.

Fixes #3161.

Cherry-picked from #3177 (d355c116b). Conflicts resolved as in the author's own
merges (b09476beb, 444f4e768): lane-selection.ts keeps main's isTestFile-based
manifest parser, and lanes.ts carries the bats summary on main's supervised runner.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(pregate): a nested .bats file is selected and run end to end (#3161)

Reviewer test for #3177: touched itself with no declaration, and through a
declared dependency; a failing nested bats run reports the bats counts.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(pregate): a wtree failure is WTREE_UNAVAILABLE, not an unresolvable base (#3197)

The pregate consumers of the working-tree fingerprint (buildContext,
withFingerprint, verifyPregate) move to lib/script-invocation.ts's
{tree}|{error} form, and lib/regen.ts drops its string-returning wrapper.

- A fingerprint failure was reported as "policy source unavailable:
  origin/<base> is not a resolvable commit" with git fetch as the fix. It is
  now WTREE_UNAVAILABLE: a launch failure lists each Windows bash candidate
  with its rejection reason and names GSTACK_GIT_BASH; a gstack-wtree exit
  keeps the repair it printed (set TMPDIR to a writable directory).
- A fingerprint failing after a check is incomplete with that cause, not
  "working tree changed"; verify says the tree cannot be fingerprinted.
- #3190's gstack-config read (test_backend) goes through runScriptSync, so
  Windows reaches it through Git Bash. The tripwire learns the bin-constant
  shape that let it through.
- Under --json every context refusal is one envelope on stdout
  ({ok:false,status:'refused',code,detail,fix,docs}, exit 3); the run object
  carries ok and status too, and GSTACK_RESULT goes to stderr.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci(pregate): gstack-pregate joins the Windows pregate job; pregate tests return to the Windows lane (#3197)

- windows-pregate-e2e runs gstack-pregate run (preflight: regen, secrets
  through the gstack-redact bun bin, literals, guards) in both journeys, and
  asserts a missing GSTACK_GIT_BASH refuses with one WTREE_UNAVAILABLE --json
  envelope.
- test/helpers/pregate-fixture.ts and restamp-fixture.ts runBin launch bins
  through lib/script-invocation.ts (bun bins as Bun argv, bash bins through
  Git Bash on Windows), which removes the "shebang via CreateProcess" reason
  for the pregate tests; they move from KNOWN_WINDOWS_INCOMPATIBLE to
  KNOWN_WINDOWS_SAFE.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* doctor: list rescue refs and warn about CLAUDE.md skill lists

gstack-doctor's plain report gains two rows. 'rescue refs' lists each
refs/gstack/rescue/* commit in the install checkout, newest first, with its
date, a needs-manual-resolution label when the rescue held unmerged files, and
the shell-quoted inspect and delete commands bin/gstack-ff-update's banner
prints. 'CLAUDE.md' warns when a gstack section in ~/.claude/CLAUDE.md or the
project's CLAUDE.md enumerates skills (#3169) and prints the short block to
paste instead; docs/troubleshooting.md#claude-md-skill-list explains it.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* doctor: name the Windows bash gstack resolves and what chose it

On Windows (OSTYPE msys/cygwin) gstack-doctor adds a 'windows bash' row from
lib/script-invocation.ts resolveWindowsBash, run through the new thin
bin/gstack-windows-bash.ts with the bun on PATH: the resolved bash and the
override or discovery step that chose it, a warn when GSTACK_GIT_BASH and
GSTACK_BASH differ, and a fail carrying the BASH_UNAVAILABLE line (every
candidate with its rejection reason) when none qualifies. Tests inject the
platform (GSTACK_DOCTOR_PLATFORM) and the discovery inputs
(GSTACK_DOCTOR_BASH_PROBE) so the row runs on every OS.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* doctor --check: name the held-install recovery on Windows, not bin/gstack-upgrade

bin/gstack-upgrade compares a pwd -P path (/c/...) with git's toplevel
(C:/...) and refuses on every Git Bash install, so on Windows the revision
row's upgrade fix (REVISION_BELOW_MINIMUM, the Capy-wrapper variant and
REVISION_UNMET) names the held-install recovery forms from
docs/troubleshooting.md#auto-update-held-install-recovery instead: the Git
Bash form and the Windows PowerShell 5.1 form, each labelled by shell with the
install path quoted for that shell. Other platforms keep bin/gstack-upgrade.
A test pins both printed forms to the documented ones.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(skills): omit the plan-mode section on hosts without plan mode (#3168)

generatePlanModeInfo now honours capabilities.planMode. Hosts that declare
planMode:false (codex, agy, hermes, openclaw, and define-host's default:
factory, kiro, slate, cursor, gbrain) no longer render the
'## Plan Mode Safe Operations' and '## Skill Invocation During Plan Mode'
preamble sections. Claude, copilot and opencode are unchanged.

The codex, factory and agy ship goldens are regenerated. The clarity test that
pinned the plan-mode entry-order prose on every host now pins it on planMode
hosts and asserts its absence elsewhere (intentional contract change).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(skills): guard generated SKILL.md size at 100,000 chars on every host (#3199)

A free test renders every host and fails when a generated SKILL.md exceeds
100,000 characters, naming host, skill, actual size, limit and the fix. The 89
current offenders are pinned at their exact size in
test/fixtures/skill-md-size-allowlist.json: growth past a pin fails, and a
shrink fails until the pin is lowered. The shared-preamble hoist is deferred
(owner Garry, revisit 2026-11-15).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(pregate): keep #3177's bats tests beside the nested-directory test

Moves #3177's bats describe block verbatim from test/pregate-selection.test.ts
into test/pregate-bats-nested.test.ts, so pregate-selection.test.ts is
unchanged on this branch and the in-flight field-list-2 pregate lane
(capy/fl2-L6-pregate, which edits that file's imports and adds cases) merges
without a conflict. No assertion changes.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* doctor --check: keep bin/gstack-upgrade as the Windows fix (path spelling fixed on main)

Reverts 37bac07f8. Plan item D named the held-install recovery forms on
Windows only until bin/gstack-upgrade stopped refusing Git Bash installs.
bin/gstack-upgrade:63-67 now compares with gstack_path_same from
bin/gstack-path-spelling.sh, pinned by test/gstack-upgrade-path-spelling.test.ts,
so bin/gstack-upgrade is the right fix on every platform again.

* test(skills): measure the size-guard pins on v1.91.109.0

#3225 (merged into this branch) grew 53 pinned skills: 3 characters each from
the Aside driver resolver, and about 340 more in plan-design-review and
plan-devex-review from the #3219 summary sections. The pins are this PR's
starting measurement, so they are taken on the merged base; no skill this PR
renders grew.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* ci(windows): only the pregate tests that pass on windows-latest join the lane

windows-free-tests run 38984824731 (this branch at 8ed6da402) ran the moved
files. gstack-regen and the new pregate-wtree-refusal passed. pregate-guards,
-remote, -regate, -invalidation, -selection and -bats-nested launched
gstack-pregate fine but fail on Linux-host expectations: their fixture lanes are
Linux workflow jobs, which a Windows host records as requires-remote
obligations. They go back to KNOWN_WINDOWS_INCOMPATIBLE with that reason
instead of "shebang via CreateProcess".

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* windows curation: keep CI run ids out of reason prose (pii.phone false positive)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* skill-md size pins: follow #3231's review-army line (+21 chars on five inlining hosts)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* doctor test: drop the reverted Windows upgrade-fix wording from item D's describe

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* skill-md size pins: follow #3226 (plan-devex-review +3324) and #3234 (autoplan +59)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* WTREE_UNAVAILABLE moves to lib/result-codes-release.ts (#3235 split; result-codes.ts stays at 796 of 800 lines); doctor fixture copies the release table; size pins follow #3235's review +1

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): fix wave PR 5 release notes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* v1.91.117.0 fix: fix wave PR 5 (pregate bats + WTREE_UNAVAILABLE, doctor rows, SKILL.md size guard, planMode hosts)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(qa): an invalid-input probe breaks only the rule it tests

The paid gate's qa-functional-cli-report failed again on #3236 (run
39001107574): the agent probed the digits-only amount rule with -5, which
the positivity check rejected first, so the planted parseInt defect stayed
hidden and it reported no defect. The contract map's invalid-input row now
asks for each input to break only the rule it tests, so another check
cannot reject it first.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: harjothkhara <48686985+harjothkhara@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-17 04:36:00 +00:00

28 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, Copilot, Antigravity) is defined as a typed TypeScript config object built by the defineHost() factory. Rendering a new host means creating one file and re-exporting it: the generator, tests, and validation pick it up. Installing it is not free: setup still has one install arm per installable host (see "What setup changes on disk" below), so an installable host also needs a setup arm, an entry in the install registry tier table, and a certification run before it is labelled full.

Install ownership rules

These rules hold for every host, every scope, and every upgrade path (./setup, ./setup --refresh-registered, /gstack-upgrade, the team-mode auto-update). Each refusal names what was left alone, the rule that applied, and the exact command to proceed.

  1. Project-local setup never silently replaces a global install. Setup run from a project-vendored copy, or from any checkout other than the one a global install links to, leaves that global install alone and prints ./setup --global as the way to replace it on purpose. (Codex: setup:186-207, #2879. Claude: the global ~/.claude/skills/gstack link.)
  2. An explicit --host X never mutates another host. --host codex writes only Codex's skills directory and the checkout's own .agents/ render. It does not heal, register, or remove Claude Code hooks in settings.json, and it does not touch the Claude gbrain render (#2347). One deliberate exception: the one-time /claude → /claude-code rename repair (bin/gstack-migrate-claude-code) renames gstack-owned entries in every install that shares the checkout.
  3. Upgrades refresh exactly the registered installs. Every install setup activates is recorded in the install registry. Upgrades run ./setup --refresh-registered, which runs setup once per registered host of that checkout (printing the source first) and nothing else. Installs owned by another checkout, and project installs of another project, are listed with the command that refreshes them there; they are never repointed (#1925).
  4. User-owned files survive. Setup, gstack-relink, and gstack-uninstall delete or link over an entry only with proof that gstack created it (the #2119 ownership gate); anything else is reported and left.

The install registry

$GSTACK_STATE_ROOT/installs.tsv (mode 0600) is the one record of installs. Only bin/gstack-install-registry.sh writes it, under a lock directory with an atomic rename. Columns: host, scope (global/project), project, destination (the skills directory the host discovers), install root, source realpath, version, prefix setting, render, updated-at. A row publishes only after the host arm finished activating. Rows whose install root is gone are dropped (gstack_install_registry_reconcile) by every refresh and by gstack-uninstall, so an upgrade never resurrects an uninstalled host.

./setup --status prints one row per install (host, tier, scope, result, version, destination, source) without building or writing anything, plus any pre-registry install it finds on disk, labelled unregistered with the command that registers it. A Codex install captured by a project-vendored copy (#2879) is reported with both choices instead of being repointed. The setup summary, the /gstack-upgrade summary, and --status share one renderer (gstack_install_registry_render).

Migrations

gstack-upgrade/migrations/v*.sh are state-root migrations. Their marker is $GSTACK_STATE_ROOT/.last-setup-version (read from ~/.gstack once for installs that predate the move). It advances only past migrations that succeeded; a failed migration stops the sequence, prints a failed row with the retry command, and runs again on the next ./setup. Install and host migrations are tracked per install by the registry row's version.

What setup changes on disk

Inventory of every setup side effect, by phase (line numbers are approximate):

Phase Writes Scope
Build browse/dist, design/dist, make-pdf/dist, bin/gstack-cso-* in the source checkout source
Hosts record .gstack-installed-hosts in the source checkout: every host setup installed from it, additive (#1694) source
Generation renders for the recorded hosts only (.agents/, .kiro/, .factory/, .opencode/, .cursor/, .copilot/, .agy/) in the source checkout; gstack-patch-names rewrites Claude name: fields in place source
Render prune renders of hosts the record does not name: links into the checkout removed, generated files moved to $GSTACK_STATE_ROOT/backups/host-renders/<ts>-<id>/, other files kept source / state root
Chromium Playwright cache (~/.cache/ms-playwright), lock under the state root machine
Config skill_prefix, timeline_stop_hook, team-mode keys in $GSTACK_STATE_ROOT/config.yaml state root
Claude arm ~/.claude/skills/gstack link, one dir per skill with a SKILL.md link, alias copies selected host
Codex arm ${CODEX_HOME:-~/.codex}/skills/gstack runtime root, gstack-* links into .agents/skills, .agents/skills/gstack sidecar selected host
Kiro, Factory, OpenCode, Cursor, Copilot, Antigravity arms the host's skills/gstack runtime root (built beside the live one and swapped in) and gstack-* links selected host
OpenCode commands ~/.config/opencode/commands/gstack-*.md with a managed marker (#2629); user command files are never touched selected host
Registry one row per activated install in installs.tsv state root
Migrations whatever each v*.sh repairs; marker in the state root state root
Claude hooks ~/.claude/settings.json (heal, SessionStart, plan-tune, Stop) — only when Claude is selected Claude
gbrain gbrain-detection.json; the Claude render in $GSTACK_STATE_ROOT/render/claude — only when Claude is selected state root / Claude

Activation today: on macOS and Linux every install is a set of symlinks into the source checkout, so a git pull changes the active payload before setup runs and "keep the prior payload" means "keep the prior checkout" — which the upgrade skill does on the vendored path (.bak restore) and records on the git path (PRE_UPGRADE_COMMIT). On Windows, where setup copies instead of linking, every copy is staged as <dst>.gstack-new.<pid> and swapped in only when complete; an interrupted swap is repaired on the next run (a missing destination is restored from <dst>.gstack-old.<pid>). Versioned payload directories for the symlink platforms are not built yet.

Install-context render contract

Committed renders name each host's default install root. A render for any other root (project install, CLAUDE_CONFIG_DIR, renamed checkout) is made with gen-skill-docs --host <h> --install-root <abs path> --out-dir <tmp> --link-root <dir>: every default-root path becomes the given root, and resolvers see it as TemplateContext.installRoot (null keeps committed bytes). Per-install renders live in $GSTACK_STATE_ROOT/render/installs/<host>-<id>/ (<id> keyed by the root's realpath) and are recorded in the registry's render column. Setup makes one for a Claude install outside ~/.claude/skills/gstack (renamed checkout, project-vendored copy, CLAUDE_CONFIG_DIR) and a global Codex install outside ~/.codex/skills/gstack (CODEX_HOME); repo-local Codex installs resolve their root at run time. A root that is not a plain path (whitespace, shell metacharacters) is named through a <host>-<id>.root alias symlink: worktree-isolated Claude Code refuses a command path containing a space in any quoting.

Host renders in an install (#1694)

A checkout renders skills only for the hosts installed from it. bin/gstack-host-renders.sh owns the record, .gstack-installed-hosts in the checkout: setup adds each host it installs and never removes one, so ./setup --host codex keeps a --host factory render. The first run after an upgrade seeds it from the hosts the checkout already serves (registry rows, install roots that resolve to it, host skills that link into its renders, and legacy copies with no runtime root). scripts/build.sh renders claude plus the recorded hosts and prints which hosts and why; a checkout with no record (a development checkout) or GSTACK_RENDER_HOSTS=all renders every host.

Every setup run then prunes the renders of unrecorded hosts with bin/gstack-relink's proof rules: a symlink into the checkout (strong proof) is removed; a file proven generated (weak proof: the gen-skill-docs banner, the generator's exact openai.yaml shape, byte identity with the checkout) is moved to $GSTACK_STATE_ROOT/backups/host-renders/<ts>-<id>/; every other file stays where it is, and only directories left empty are removed. The backup's prune.log lists every path. A gstack-upgrade migration runs the same prune for an install upgraded without a setup run. Instruction-only hosts (Hermes, OpenClaw, GBrain) are never installed by setup, so a user who renders one by hand inside an install adds its name to the record first.

Host tiers and capabilities

Every host declares tier and capabilities (scripts/host-config.ts). One vocabulary is used everywhere: ./setup --help, setup and --status rows, host errors, and the README host matrix, all checked against hosts/index.ts by test/host-config.test.ts.

Tier Meaning
full Installs, renders, passes the conformance kit, and has a dated certification record below.
experimental Installs and renders and passes the conformance kit; no certification record yet.
instruction-only No install arm. ./setup --host X prints what to copy and changes nothing.

Rendering alone never certifies full.

Capability Values Used for
toolExecution boolean preambles and bin/ helpers run
questions native / prose structured question tool vs plain-text questions
planMode boolean plan-mode transitions; false omits the preamble's plan-mode section
delegation boolean sub-agents
browser boolean (needs toolExecution) gstack's browser
safetyHooks enforced / advisory /careful, /freeze, /guard block (Claude) or only warn

Hosts with safetyHooks: 'advisory' get one line at the top of /careful, /freeze and /guard: "not enforced on : advisory, not blocked".

Certify your host

  1. Pass the conformance kit: bun test test/host-conformance.test.ts installs every installable host into a temp HOME, checks discovery directories, no other host's variant, no duplicates, ./setup --status, and an upgrade from the previous release's install layout.
  2. Run one representative workflow for real on the host (for example /review on a small diff), and an upgrade from an existing install.
  3. Add a row below with the date, host version, gstack version, and where the transcript lives, then set tier: 'full'.
Host Date Host version gstack version Evidence
claude 2026-10-02 Claude Code 2.1.284 (CI image pin) v1.91.13.0 (1c54555) gate-tier evals, real Claude Code sessions: https://github.com/garrytan/gstack/actions/runs/37033755821

Codex stays experimental until its periodic cases (codex-discover-skill, codex-review) pass on the CI image's pinned Codex CLI and a row is added.

GitHub Copilot CLI (#393)

Shipped as experimental (hosts/copilot.ts, ./setup --host copilot): skills in ~/.copilot/skills/gstack-*, runtime root ~/.copilot/skills/gstack with a .source-path marker so /gstack-upgrade finds the checkout, names prefixed gstack- (Copilot's built-in /review would win otherwise), disable-model-invocation: true on sensitive skills, and a one-paragraph tool-name glossary (ask_user, exit_plan_mode, task, view, skill) inserted ahead of the preamble's STATUS rules. A shared directory was not enough: Copilot CLI stopped reading ~/.claude/skills in 1.0.36, gstack never writes ~/.agents/skills, and a Codex render carries Codex paths and the Codex model profile.

Disposition of the community attempts:

PR Decision Why
#2323 (@andrey-esipov) Selectively reused, with credit Kept: ~/.copilot/skills and .github/skills roots, copilot-binary detection, .source-path, the gstack- prefix, the 1024-char description limit, uninstall coverage. Not kept: new schema fields and generator edits (predate defineHost()), an unguarded rm -rf of the runtime root (#2142), links to setup/scripts/Claude sources inside a recursively scanned dir, a test-only install backdoor in setup, and a blanket AskUserQuestion → ask_user rewrite.
#396 (@ridermw) Rejected Sed-rewrites Codex output into a shared .agents/skills; its E2E runner copies real ~/.copilot credentials into a temp HOME. Its session scanner is a candidate for a later /retro global change.
#487 (@ridermw) Rejected Relies on ~/.claude/skills, which Copilot CLI no longer reads.
#1852 (@lolisaigao1234) Rejected Detects gh instead of copilot, installs to a directory Copilot does not scan, links bin without lib.

Unverified without a real Copilot CLI (why it is not full): autopilot may answer ask_user itself, plan mode blocks mutating shell commands, symlinked skill dirs need Copilot CLI 1.0.62 or later, and COPILOT_HOME other than ~/.copilot is refused for now.

Antigravity CLI

Shipped as experimental (hosts/agy.ts, ./setup --host agy, alias --host antigravity) for Google's Antigravity CLI. The host is named after the agy binary: with the longer name, the per-fence runtime prelude was 408 bytes against its 400-byte budget (the global root is long); agy renders at exactly 400, so the next byte added to that prelude must come with a budget change. Verified against Google's docs on 2026-10-08:

Fact Value Source
Global skills ~/.gemini/antigravity-cli/skills/<skill>/SKILL.md Agent skills (CLI section), Plugins & skills, Migrating from Gemini CLI
Workspace skills .agents/skills/<skill>/SKILL.md same pages
Frontmatter name (optional, defaults to the directory), description (required) Agent skills
Description limit 1024 characters (Agent Skills format) What are Agent Skills?
Tool names run_command, view_file, write_to_file, replace_file_content, multi_replace_file_content, grep_search, find_by_name, invoke_subagent, ask_question Hooks: supported tools
Plan mode agy --mode=plan / /plan, no exit-plan tool Execution modes
Binary agy, installed to ~/.local/bin/agy; config in ~/.gemini/antigravity-cli/ Installation, Settings

What the host does: skills in ~/.gemini/antigravity-cli/skills/gstack-*, runtime root ~/.gemini/antigravity-cli/skills/gstack with .source-path for /gstack-upgrade, name: equal to the gstack-<skill> directory (the CLI turns each name into a slash command, and has /plan, /diff, /skills and other built-ins), frontmatter limited to name and description, the Gemini model overlay, and a one-paragraph tool glossary ahead of the preamble's STATUS rules. The glossary also tells the agent to keep reading when view_file returns only part of a long SKILL.md: users report a first read that stops at 800 lines (forum), which Google's docs do not state, so the rendered text names no number.

Shared ~/.gemini. Antigravity CLI and Gemini CLI both live under ~/.gemini. Gemini CLI reads ~/.gemini/skills (and ~/.agents/skills); Antigravity CLI reads ~/.gemini/antigravity-cli/skills. Setup writes only gstack entries in the latter, uninstall removes only gstack-managed entries there, and --host auto selects Antigravity only for agy on PATH or an existing ~/.gemini/antigravity-cli, never for a bare ~/.gemini. test/setup-install-registry.test.ts holds both directions. Gemini CLI as a host is not supported. Two overlaps remain: workspace .agents/skills is also where a project-local Codex install puts its Codex-flavored gstack-* skills, which Antigravity will list in that repo, and the Antigravity 2.0 app and IDE read ~/.gemini/config/skills, which this host does not install (Google's codelab says that directory also reaches the CLI; the CLI docs do not, so setup follows the CLI docs).

Disposition of the community attempts:

PR Decision Why
#2134 (@0xshae) Selectively reused, with credit Kept: agy as the host name and binary, .agents/skills as the workspace root, and a .agy render directory separate from Codex's .agents. Not kept: the hand-written config (predates defineHost()), ~/.gemini/config/skills as the CLI root, and the setup arm without the ownership gates.
#2893 (@ManchalaSashank) Selectively reused, with credit Kept: agy/antigravity as host name and alias in setup and generation, the Gemini overlay, uninstall coverage. Not kept: ~/.gemini/config/skills, auto-detect on a bare ~/.gemini (Gemini CLI's directory), .gemini as the render directory, and the repo-local .gemini/skills sidecar.
#2726 (@voipexpert) Selectively reused, with credit Kept: Antigravity's tool names from its own docs, and that manage_task (background processes) is not a to-do tool. Not kept: blanket tool-name rewrites (a preamble glossary replaces them) and suppressing outside reviews, which run through the shell.
#2244 (@KiDDarn) Selectively reused, with credit Kept: fail the install when no skill was linked. Not kept: the ~/.gemini/config/plugins plugin layout (CLI plugins live under ~/.gemini/antigravity-cli/plugins and are installed by agy plugin install), the benchmark provider work, and extra frontmatter fields.
#413 (@jnMetaCode) Rejected Installs to ~/.gemini/skills and detects gemini: that is Gemini CLI. A comment there (@doogiehowzer) reported the partial view_file read the glossary now handles.
#2079 (@FACUTUCCI10) Rejected Gemini CLI paths (~/.gemini/skills).
#2187 (@NotArsal) Rejected Duplicate of #2134 without setup.
#1490 (@7H0M45-4N70NY) Rejected ~/.antigravity/skills, which no Antigravity surface reads.
#565 (@ChetanTekur) Rejected Gemini CLI GEMINI.md support; Gemini CLI is out of scope.

Unverified without a real Antigravity CLI (why it is not full): that the CLI follows symlinked skill directories, that it does not import the runtime root's extra markdown files (ETHOS.md, review/*.md) as skills, the partial-read behavior of view_file on long skills, ask_question option handling, and an upgrade from an existing install.

Instruction-only hosts

OpenClaw, Hermes, Slate and GBrain have no install arm. Hermes stays instruction-only until someone certifies a native install (#2826 measured one on Hermes 0.21.0): ./setup --host hermes prints the digest path and the generator command, and the Hermes render writes name: gstack-<skill> so Hermes, which indexes skills by frontmatter name, cannot shadow them (#2825).

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
├── copilot.ts       # GitHub Copilot CLI
├── agy.ts           # Google Antigravity CLI
└── 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: [] (both outside-review skills are enabled; Claude and Codex explicitly omit their own wrapper)
  • 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: sharedRuntimeRoot(), every file the skills run or read as $GSTACK_ROOT/<path> (bin, lib, the compiled tools, freeze/bin, the review checklists and specialists, the jargon list and question registry, the AskUserQuestion docs, the DX Hall of Fame, VERSION, and the office-hours and plan-design-review SKILL.md copies). It mirrors setup's _link_runtime_dists and _copy_runtime_skill_refs; test/runtime-root-assets.test.ts checks each staged root on disk. A host with extra assets passes them: sharedRuntimeRoot(['qa/templates']).
  • 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 (outside-provider review resolvers plus Review Army, suppressed on hosts that opt out; Codex keeps outside reviews and suppresses Review Army), 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 + extra runtime assets), 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 and the render table

Add .myhost/ to .gitignore (generated skill docs are gitignored), and add myhost:.myhost to GSTACK_HOST_RENDER_DIRS in bin/gstack-host-renders.sh so an install renders it only when it is installed and prunes it otherwise (#1694; test/host-renders.test.ts checks the table against hosts/*.ts).

4. Generate and verify

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

# Verify output exists
ls .myhost/skills/gstack-*/SKILL.md

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

# Validate all host content and tracked output freshness, including the new host
bun run skill:check
# Claude install paths are rejected in prose; legitimate Bash fallbacks are allowed.

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. They verify: output exists, no path leakage, valid frontmatter, freshness check passes, and outside-review skills match each host's exclusions.

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
tier full, experimental, or instruction-only (see Host tiers)
capabilities What the host runtime offers; rendering and docs must not claim more
frontmatter.nameMatchesDirectory Write gstack-<skill> into name: for hosts that index by frontmatter name
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.