From 408ee77cdece17ca38c8d8e53cc4d7dfc40b2fce Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 14 Aug 2026 19:32:22 -0700 Subject: [PATCH] =?UTF-8?q?refactor(hosts):=20defineHost()=20factory=20?= =?UTF-8?q?=E2=80=94=2010=20copy-paste=20host=20files=20become=20declarati?= =?UTF-8?q?ons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hosts/*.ts were ten copies of one file: runtimeRoot byte-identical in 9/10, pathRewrites mechanically derivable from the host name for 7/10, the 11-entry toolRewrites map byte-identical between openclaw and gbrain, and every asset change a 10-file edit (cursor and slate had already fallen out of three other hand-maintained lists). defineHost() owns the defaults; each host file now declares only what makes it different (slate/cursor: 8 lines each). Shared constants: CROSS_MODEL_RESOLVERS, GBRAIN_RESOLVERS, EXEC_STYLE_TOOL_REWRITES. Genuinely-different things stayed explicit: codex/factory $GSTACK_ROOT rewrites, hermes's tool vocabulary, claude's denylist+prefixable install, opencode's wider runtimeRoot. Proof: JSON.stringify(ALL_HOST_CONFIGS) dump-diff before/after EMPTY (and a runtime walk confirmed no function-valued or undefined-keyed fields, so the JSON diff is complete); gen:skill-docs --host all zero-diff; host-config + gen-skill-docs + idempotency suites 485/485. Host files 595 -> 285 lines. docs/ADDING_A_HOST.md teaches the factory pattern. Co-Authored-By: Claude Fable 5 --- docs/ADDING_A_HOST.md | 116 +++++++++++++++-------------- hosts/claude.ts | 21 +----- hosts/codex.ts | 35 ++------- hosts/cursor.ts | 46 +----------- hosts/define-host.ts | 167 ++++++++++++++++++++++++++++++++++++++++++ hosts/factory.ts | 33 ++------- hosts/gbrain.ts | 51 ++----------- hosts/hermes.ts | 43 ++--------- hosts/kiro.ts | 47 ++---------- hosts/openclaw.ts | 57 ++------------ hosts/opencode.ts | 41 ++--------- hosts/slate.ts | 46 +----------- 12 files changed, 284 insertions(+), 419 deletions(-) create mode 100644 hosts/define-host.ts diff --git a/docs/ADDING_A_HOST.md b/docs/ADDING_A_HOST.md index 50654e4e2..995803e40 100644 --- a/docs/ADDING_A_HOST.md +++ b/docs/ADDING_A_HOST.md @@ -1,14 +1,16 @@ # 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) is defined -as a typed TypeScript config object. Adding a new host means creating one file -and re-exporting it. Zero code changes to the generator, setup, or tooling. +(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 @@ -16,11 +18,14 @@ hosts/ ├── opencode.ts # OpenCode ├── slate.ts # Slate (Random Labs) ├── cursor.ts # Cursor -├── openclaw.ts # OpenClaw (hybrid: config + adapter) +├── openclaw.ts # OpenClaw +├── hermes.ts # Hermes (Nous Research) +├── gbrain.ts # GBrain └── index.ts # Registry: imports all, derives Host type ``` -Each config file exports a `HostConfig` object that tells the generator: +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) @@ -35,59 +40,60 @@ copy, and tests all read from these configs. None of them have per-host code. ### 1. Create the config file -Copy an existing config as a starting point. `hosts/opencode.ts` is a good -minimal example. `hosts/factory.ts` shows tool rewrites and conditional fields. -`hosts/openclaw.ts` shows the adapter pattern for hosts with different tool models. - -Create `hosts/myhost.ts`: +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`): ```typescript -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const myhost: HostConfig = { +const myhost = defineHost({ name: 'myhost', displayName: 'MyHost', - cliCommand: 'myhost', // binary name for `command -v` detection - cliAliases: [], // alternative binary names - - globalRoot: '.myhost/skills/gstack', - localSkillRoot: '.myhost/skills/gstack', - hostSubdir: '.myhost', - usesEnvVars: true, // false only for Claude (uses literal ~ paths) - - frontmatter: { - mode: 'allowlist', // 'allowlist' keeps only listed fields - keepFields: ['name', 'description'], - descriptionLimit: null, // set to 1024 for hosts with limits - }, - - generation: { - generateMetadata: false, // true only for Codex (openai.yaml) - skipSkills: ['codex'], // codex skill is Claude-only - }, - - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.myhost/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.myhost/skills/gstack' }, - { from: '.claude/skills', to: '.myhost/skills' }, - ], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { 'review': ['checklist.md', 'TODOS-format.md'] }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - - learningsMode: 'basic', -}; +}); export default myhost; ``` +That expands to the full `HostConfig` with these defaults: + +- `cliCommand: 'myhost'` (the name; binary for `command -v` detection) +- `cliAliases: []` +- `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 sidecar, `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`: `{ prefixable: false, 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`: @@ -97,11 +103,11 @@ import myhost from './myhost'; // Add to ALL_HOST_CONFIGS array: export const ALL_HOST_CONFIGS: HostConfig[] = [ - claude, codex, factory, kiro, opencode, slate, cursor, openclaw, myhost + claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, myhost ]; // Add to re-exports: -export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, myhost }; +export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, myhost }; ``` ### 3. Add to .gitignore @@ -155,7 +161,8 @@ Key fields: | `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. | +| `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 | @@ -165,8 +172,9 @@ Key fields: ## Adapter pattern (for hosts with different tool models) If string-replace tool rewrites aren't enough (the host has fundamentally -different tool semantics), use the adapter pattern. See `hosts/openclaw.ts` -and `scripts/host-adapters/openclaw-adapter.ts`. +different tool semantics), use the adapter pattern: set the `adapter` field +to the adapter module path. See `scripts/host-adapters/openclaw-adapter.ts` +for the reference implementation (no shipped host currently sets `adapter`). The adapter runs as a post-processing step after all generic rewrites. It exports `transform(content: string, config: HostConfig): string`. diff --git a/hosts/claude.ts b/hosts/claude.ts index f805da040..425232bbd 100644 --- a/hosts/claude.ts +++ b/hosts/claude.ts @@ -1,15 +1,10 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const claude: HostConfig = { +const claude = defineHost({ name: 'claude', displayName: 'Claude Code', - cliCommand: 'claude', - cliAliases: [], - globalRoot: '.claude/skills/gstack', - localSkillRoot: '.claude/skills/gstack', - hostSubdir: '.claude', - usesEnvVars: false, + usesEnvVars: false, // primary host — literal ~ paths, no $GSTACK_ROOT env vars frontmatter: { mode: 'denylist', @@ -24,14 +19,6 @@ const claude: HostConfig = { pathRewrites: [], // Claude is the primary host — no rewrites needed toolRewrites: {}, - suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, install: { prefixable: true, @@ -40,6 +27,6 @@ const claude: HostConfig = { coAuthorTrailer: 'Co-Authored-By: Claude Opus 4.7 ', learningsMode: 'full', -}; +}); export default claude; diff --git a/hosts/codex.ts b/hosts/codex.ts index 7dc80ea87..8a301f152 100644 --- a/hosts/codex.ts +++ b/hosts/codex.ts @@ -1,15 +1,12 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost, CROSS_MODEL_RESOLVERS, GBRAIN_RESOLVERS } from './define-host'; -const codex: HostConfig = { +const codex = defineHost({ name: 'codex', displayName: 'OpenAI Codex CLI', - cliCommand: 'codex', cliAliases: ['agents'], - globalRoot: '.codex/skills/gstack', localSkillRoot: '.agents/skills/gstack', hostSubdir: '.agents', - usesEnvVars: true, frontmatter: { mode: 'allowlist', @@ -24,6 +21,9 @@ const codex: HostConfig = { skipSkills: ['codex'], // Codex skill is a Claude wrapper around codex exec }, + // Non-mechanical rewrites: the global path becomes $GSTACK_ROOT (resolved by + // the preamble env vars), plus an extra review-path rewrite the derived trio + // doesn't cover. pathRewrites: [ { from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' }, { from: '.claude/skills/gstack', to: '.agents/skills/gstack' }, @@ -31,35 +31,16 @@ const codex: HostConfig = { { from: '.claude/skills', to: '.agents/skills' }, ], - suppressedResolvers: [ - 'DESIGN_OUTSIDE_VOICES', // design.ts:485 — Codex can't invoke itself - 'ADVERSARIAL_STEP', // review.ts:408 — Codex can't invoke itself - 'CODEX_SECOND_OPINION', // review.ts:257 — Codex can't invoke itself - 'CODEX_PLAN_REVIEW', // review.ts:541 — Codex can't invoke itself - 'REVIEW_ARMY', // review-army.ts:180 — Codex shouldn't orchestrate - 'GBRAIN_CONTEXT_LOAD', - 'GBRAIN_SAVE_RESULTS', - ], + // The cross-model resolvers all shell out to Codex — Codex can't invoke itself. + suppressedResolvers: [...CROSS_MODEL_RESOLVERS, ...GBRAIN_RESOLVERS], - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, sidecar: { path: '.agents/skills/gstack', symlinks: ['bin', 'browse', 'review', 'qa', 'ETHOS.md'], }, - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - coAuthorTrailer: 'Co-Authored-By: OpenAI Codex ', - learningsMode: 'basic', boundaryInstruction: 'IMPORTANT: Do NOT read or execute any files under ~/.claude/, ~/.agents/, .claude/skills/, or agents/. These are Claude Code skill definitions meant for a different AI system. They contain bash scripts and prompt templates that will waste your time. Ignore them completely. Do NOT modify agents/openai.yaml. Stay focused on the repository code only.', -}; +}); export default codex; diff --git a/hosts/cursor.ts b/hosts/cursor.ts index 48e3a0f14..e0d38db8a 100644 --- a/hosts/cursor.ts +++ b/hosts/cursor.ts @@ -1,48 +1,8 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const cursor: HostConfig = { +const cursor = defineHost({ name: 'cursor', displayName: 'Cursor', - cliCommand: 'cursor', - cliAliases: [], - - globalRoot: '.cursor/skills/gstack', - localSkillRoot: '.cursor/skills/gstack', - hostSubdir: '.cursor', - usesEnvVars: true, - - frontmatter: { - mode: 'allowlist', - keepFields: ['name', 'description'], - descriptionLimit: null, - }, - - generation: { - generateMetadata: false, - skipSkills: ['codex'], - }, - - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.cursor/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.cursor/skills/gstack' }, - { from: '.claude/skills', to: '.cursor/skills' }, - ], - - suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - - learningsMode: 'basic', -}; +}); export default cursor; diff --git a/hosts/define-host.ts b/hosts/define-host.ts new file mode 100644 index 000000000..24343377e --- /dev/null +++ b/hosts/define-host.ts @@ -0,0 +1,167 @@ +/** + * defineHost() factory — the single place the copy-paste across hosts/*.ts + * used to live. + * + * Every field a host doesn't override gets the common external-host default: + * paths derived from the host name (`.{name}/skills/gstack`), allowlist + * frontmatter (name + description), no metadata sidecar, skip the codex + * skill, the standard three-entry pathRewrite trio derived from the resolved + * paths, the shared runtimeRoot asset list, and symlink-generated install. + * + * Defaults are constructed fresh per call, so no two host configs ever share + * a mutable array/object. Fields that are absent today (staticFiles, adapter, + * sidecar, toolRewrites, coAuthorTrailer, boundaryInstruction) stay absent + * unless a host explicitly sets them — the factory never default-populates + * optional fields (test/host-config.test.ts pins e.g. openclaw.adapter as + * undefined). + */ + +import type { HostConfig } from '../scripts/host-config'; + +type PathRewrite = { from: string; to: string }; + +/** + * Preamble resolvers that orchestrate cross-model second opinions (they shell + * out to Codex or spin up the review army). Suppressed on hosts that can't or + * shouldn't invoke other models — Codex itself (can't invoke itself) and the + * non-Claude agent runtimes (OpenClaw, Hermes, GBrain). + */ +export const CROSS_MODEL_RESOLVERS: string[] = [ + 'DESIGN_OUTSIDE_VOICES', // design.ts:485 — invokes Codex for outside voices + 'ADVERSARIAL_STEP', // review.ts:408 — invokes Codex adversarially + 'CODEX_SECOND_OPINION', // review.ts:257 — invokes Codex + 'CODEX_PLAN_REVIEW', // review.ts:541 — invokes Codex + 'REVIEW_ARMY', // review-army.ts:180 — multi-model orchestration +]; + +/** + * Brain-aware resolvers. Suppressed by default on every host — only hosts + * that can run with a GBrain (hermes, gbrain) leave these active. + */ +export const GBRAIN_RESOLVERS: string[] = [ + 'GBRAIN_CONTEXT_LOAD', + 'GBRAIN_SAVE_RESULTS', +]; + +/** + * Tool-name rewrites for OpenClaw-style agent runtimes (lowercase exec / + * read / write / edit tools, sessions_spawn for subagents). OpenClaw and + * GBrain share these byte-for-byte; spread into `toolRewrites` at the use + * site so each config owns its own copy. + */ +export const EXEC_STYLE_TOOL_REWRITES: Record = { + 'use the Bash tool': 'use the exec tool', + 'use the Write tool': 'use the write tool', + 'use the Read tool': 'use the read tool', + 'use the Edit tool': 'use the edit tool', + 'use the Agent tool': 'use sessions_spawn', + 'use the Grep tool': 'search for', + 'use the Glob tool': 'find files matching', + 'the Bash tool': 'the exec tool', + 'the Read tool': 'the read tool', + 'the Write tool': 'the write tool', + 'the Edit tool': 'the edit tool', +}; + +/** + * Host definition input: name + displayName are required, everything else is + * an override on the common external-host defaults documented above. + * + * `extraPathRewrites` appends to the derived standard trio + * (`~/.claude/skills/gstack` → `~/{globalRoot}`, `.claude/skills/gstack` → + * localSkillRoot, `.claude/skills` → `{hostSubdir}/skills`). Hosts whose + * rewrites aren't mechanically derivable (codex, factory use $GSTACK_ROOT and + * an extra review rewrite; claude has none) replace the whole list via + * `pathRewrites` instead. The two are mutually exclusive. + */ +export interface HostOverrides + extends Partial> { + name: N; + displayName: string; + /** Appended after the derived pathRewrite trio. Mutually exclusive with `pathRewrites`. */ + extraPathRewrites?: PathRewrite[]; +} + +export function defineHost(overrides: HostOverrides): HostConfig & { name: N } { + const { + name, + displayName, + cliCommand = name, + cliAliases = [], + globalRoot = `.${name}/skills/gstack`, + localSkillRoot = `.${name}/skills/gstack`, + hostSubdir = `.${name}`, + usesEnvVars = true, // false only for Claude (literal ~ paths, no $GSTACK_ROOT) + frontmatter = { + mode: 'allowlist', + keepFields: ['name', 'description'], + descriptionLimit: null, + }, + generation = { + generateMetadata: false, + skipSkills: ['codex'], // Codex skill is a Claude wrapper around codex exec + }, + pathRewrites, + extraPathRewrites, + toolRewrites, + suppressedResolvers = [...GBRAIN_RESOLVERS], + runtimeRoot = { + globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], + globalFiles: { + 'review': ['checklist.md', 'TODOS-format.md'], + }, + }, + sidecar, + install = { + prefixable: false, + linkingStrategy: 'symlink-generated', + }, + coAuthorTrailer, + learningsMode = 'basic', + boundaryInstruction, + staticFiles, + adapter, + } = overrides; + + if (pathRewrites && extraPathRewrites) { + throw new Error( + `[${name}] pathRewrites and extraPathRewrites are mutually exclusive: ` + + `pathRewrites replaces the derived trio, extraPathRewrites appends to it` + ); + } + + const resolvedPathRewrites: PathRewrite[] = pathRewrites ?? [ + { from: '~/.claude/skills/gstack', to: `~/${globalRoot}` }, + { from: '.claude/skills/gstack', to: localSkillRoot }, + { from: '.claude/skills', to: `${hostSubdir}/skills` }, + ...(extraPathRewrites ?? []), + ]; + + // Field order below mirrors the HostConfig interface (and the original + // hand-written configs) so serialized output is stable. Optional fields are + // conditionally spread so absent overrides stay truly absent (no + // `key: undefined` entries). + return { + name, + displayName, + cliCommand, + cliAliases, + globalRoot, + localSkillRoot, + hostSubdir, + usesEnvVars, + frontmatter, + generation, + pathRewrites: resolvedPathRewrites, + ...(toolRewrites !== undefined ? { toolRewrites } : {}), + suppressedResolvers, + runtimeRoot, + ...(sidecar !== undefined ? { sidecar } : {}), + install, + ...(coAuthorTrailer !== undefined ? { coAuthorTrailer } : {}), + learningsMode, + ...(boundaryInstruction !== undefined ? { boundaryInstruction } : {}), + ...(staticFiles !== undefined ? { staticFiles } : {}), + ...(adapter !== undefined ? { adapter } : {}), + }; +} diff --git a/hosts/factory.ts b/hosts/factory.ts index 08ac2f9a1..9161d7faf 100644 --- a/hosts/factory.ts +++ b/hosts/factory.ts @@ -1,16 +1,11 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const factory: HostConfig = { +const factory = defineHost({ name: 'factory', displayName: 'Factory Droid', cliCommand: 'droid', cliAliases: ['droid'], - globalRoot: '.factory/skills/gstack', - localSkillRoot: '.factory/skills/gstack', - hostSubdir: '.factory', - usesEnvVars: true, - frontmatter: { mode: 'allowlist', keepFields: ['name', 'description', 'user-invocable'], @@ -23,11 +18,9 @@ const factory: HostConfig = { ], }, - generation: { - generateMetadata: false, - skipSkills: ['codex'], // Codex skill is a Claude wrapper around codex exec - }, - + // Non-mechanical rewrites: the global path becomes $GSTACK_ROOT (resolved by + // the preamble env vars), plus an extra review-path rewrite the derived trio + // doesn't cover. pathRewrites: [ { from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' }, { from: '.claude/skills/gstack', to: '.factory/skills/gstack' }, @@ -43,22 +36,8 @@ const factory: HostConfig = { 'use the Glob tool': 'find files matching', }, - suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - coAuthorTrailer: 'Co-Authored-By: Factory Droid ', learningsMode: 'full', -}; +}); export default factory; diff --git a/hosts/gbrain.ts b/hosts/gbrain.ts index ae777f2f1..95defe5e9 100644 --- a/hosts/gbrain.ts +++ b/hosts/gbrain.ts @@ -1,20 +1,13 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost, CROSS_MODEL_RESOLVERS, EXEC_STYLE_TOOL_REWRITES } from './define-host'; /** * GBrain host config. * Compatible with GBrain >= v0.10.0 (doctor --fast --json, search CLI, entity enrichment). * When updating, check INSTALL_FOR_AGENTS.md in the GBrain repo for breaking changes. */ -const gbrain: HostConfig = { +const gbrain = defineHost({ name: 'gbrain', displayName: 'GBrain', - cliCommand: 'gbrain', - cliAliases: [], - - globalRoot: '.gbrain/skills/gstack', - localSkillRoot: '.gbrain/skills/gstack', - hostSubdir: '.gbrain', - usesEnvVars: true, frontmatter: { mode: 'allowlist', @@ -28,51 +21,19 @@ const gbrain: HostConfig = { includeSkills: [], }, - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.gbrain/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.gbrain/skills/gstack' }, - { from: '.claude/skills', to: '.gbrain/skills' }, + extraPathRewrites: [ { from: 'CLAUDE.md', to: 'AGENTS.md' }, ], - toolRewrites: { - 'use the Bash tool': 'use the exec tool', - 'use the Write tool': 'use the write tool', - 'use the Read tool': 'use the read tool', - 'use the Edit tool': 'use the edit tool', - 'use the Agent tool': 'use sessions_spawn', - 'use the Grep tool': 'search for', - 'use the Glob tool': 'find files matching', - 'the Bash tool': 'the exec tool', - 'the Read tool': 'the read tool', - 'the Write tool': 'the write tool', - 'the Edit tool': 'the edit tool', - }, + toolRewrites: { ...EXEC_STYLE_TOOL_REWRITES }, // GBrain gets brain-aware resolvers. All other hosts suppress these. suppressedResolvers: [ - 'DESIGN_OUTSIDE_VOICES', - 'ADVERSARIAL_STEP', - 'CODEX_SECOND_OPINION', - 'CODEX_PLAN_REVIEW', - 'REVIEW_ARMY', + ...CROSS_MODEL_RESOLVERS, // NOTE: GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS are NOT suppressed here. // GBrain is the only host that gets brain-first lookup and save-to-brain behavior. ], - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - coAuthorTrailer: 'Co-Authored-By: GBrain Agent ', - learningsMode: 'basic', -}; +}); export default gbrain; diff --git a/hosts/hermes.ts b/hosts/hermes.ts index 43598989d..c89f96d6f 100644 --- a/hosts/hermes.ts +++ b/hosts/hermes.ts @@ -1,21 +1,8 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost, CROSS_MODEL_RESOLVERS } from './define-host'; -const hermes: HostConfig = { +const hermes = defineHost({ name: 'hermes', displayName: 'Hermes', - cliCommand: 'hermes', - cliAliases: [], - - globalRoot: '.hermes/skills/gstack', - localSkillRoot: '.hermes/skills/gstack', - hostSubdir: '.hermes', - usesEnvVars: true, - - frontmatter: { - mode: 'allowlist', - keepFields: ['name', 'description'], - descriptionLimit: null, - }, generation: { generateMetadata: false, @@ -23,10 +10,7 @@ const hermes: HostConfig = { includeSkills: [], }, - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.hermes/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.hermes/skills/gstack' }, - { from: '.claude/skills', to: '.hermes/skills' }, + extraPathRewrites: [ { from: 'CLAUDE.md', to: 'AGENTS.md' }, ], toolRewrites: { @@ -44,30 +28,13 @@ const hermes: HostConfig = { }, suppressedResolvers: [ - 'DESIGN_OUTSIDE_VOICES', - 'ADVERSARIAL_STEP', - 'CODEX_SECOND_OPINION', - 'CODEX_PLAN_REVIEW', - 'REVIEW_ARMY', + ...CROSS_MODEL_RESOLVERS, // GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS are NOT suppressed. // The resolvers handle GBrain-not-installed gracefully ("proceed without brain context"). // If Hermes has GBrain as a mod, brain features activate automatically. ], - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - coAuthorTrailer: 'Co-Authored-By: Hermes Agent ', - learningsMode: 'basic', -}; +}); export default hermes; diff --git a/hosts/kiro.ts b/hosts/kiro.ts index 31adc7c72..05d4b9ee5 100644 --- a/hosts/kiro.ts +++ b/hosts/kiro.ts @@ -1,50 +1,17 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const kiro: HostConfig = { +const kiro = defineHost({ name: 'kiro', displayName: 'Kiro', cliCommand: 'kiro-cli', - cliAliases: [], - globalRoot: '.kiro/skills/gstack', - localSkillRoot: '.kiro/skills/gstack', - hostSubdir: '.kiro', - usesEnvVars: true, - - frontmatter: { - mode: 'allowlist', - keepFields: ['name', 'description'], - descriptionLimit: null, - }, - - generation: { - generateMetadata: false, - skipSkills: ['codex'], // Codex skill is a Claude wrapper around codex exec - }, - - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.kiro/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.kiro/skills/gstack' }, - { from: '.claude/skills', to: '.kiro/skills' }, + // Beyond the standard .claude/* trio, Kiro also cleans up codex-style paths: + // template prose that references ~/.codex/skills/gstack or .codex/skills + // (e.g. cross-host examples) must land on Kiro's own paths. + extraPathRewrites: [ { from: '~/.codex/skills/gstack', to: '~/.kiro/skills/gstack' }, { from: '.codex/skills', to: '.kiro/skills' }, ], - - suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - - learningsMode: 'basic', -}; +}); export default kiro; diff --git a/hosts/openclaw.ts b/hosts/openclaw.ts index f8268b5c7..b19609299 100644 --- a/hosts/openclaw.ts +++ b/hosts/openclaw.ts @@ -1,15 +1,8 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost, CROSS_MODEL_RESOLVERS, GBRAIN_RESOLVERS, EXEC_STYLE_TOOL_REWRITES } from './define-host'; -const openclaw: HostConfig = { +const openclaw = defineHost({ name: 'openclaw', displayName: 'OpenClaw', - cliCommand: 'openclaw', - cliAliases: [], - - globalRoot: '.openclaw/skills/gstack', - localSkillRoot: '.openclaw/skills/gstack', - hostSubdir: '.openclaw', - usesEnvVars: true, frontmatter: { mode: 'allowlist', @@ -23,54 +16,18 @@ const openclaw: HostConfig = { generation: { generateMetadata: false, skipSkills: ['codex'], - includeSkills: [], + includeSkills: [], // native ClawHub skills replaced the generated ones }, - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.openclaw/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.openclaw/skills/gstack' }, - { from: '.claude/skills', to: '.openclaw/skills' }, + extraPathRewrites: [ { from: 'CLAUDE.md', to: 'AGENTS.md' }, ], - toolRewrites: { - 'use the Bash tool': 'use the exec tool', - 'use the Write tool': 'use the write tool', - 'use the Read tool': 'use the read tool', - 'use the Edit tool': 'use the edit tool', - 'use the Agent tool': 'use sessions_spawn', - 'use the Grep tool': 'search for', - 'use the Glob tool': 'find files matching', - 'the Bash tool': 'the exec tool', - 'the Read tool': 'the read tool', - 'the Write tool': 'the write tool', - 'the Edit tool': 'the edit tool', - }, + toolRewrites: { ...EXEC_STYLE_TOOL_REWRITES }, // Suppress Claude-specific preamble sections that don't apply to OpenClaw - suppressedResolvers: [ - 'DESIGN_OUTSIDE_VOICES', - 'ADVERSARIAL_STEP', - 'CODEX_SECOND_OPINION', - 'CODEX_PLAN_REVIEW', - 'REVIEW_ARMY', - 'GBRAIN_CONTEXT_LOAD', - 'GBRAIN_SAVE_RESULTS', - ], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, + suppressedResolvers: [...CROSS_MODEL_RESOLVERS, ...GBRAIN_RESOLVERS], coAuthorTrailer: 'Co-Authored-By: OpenClaw Agent ', - learningsMode: 'basic', -}; +}); export default openclaw; diff --git a/hosts/opencode.ts b/hosts/opencode.ts index 3ad0901ec..fcba39d91 100644 --- a/hosts/opencode.ts +++ b/hosts/opencode.ts @@ -1,48 +1,19 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const opencode: HostConfig = { +const opencode = defineHost({ name: 'opencode', displayName: 'OpenCode', - cliCommand: 'opencode', - cliAliases: [], - globalRoot: '.config/opencode/skills/gstack', - localSkillRoot: '.opencode/skills/gstack', - hostSubdir: '.opencode', - usesEnvVars: true, - - frontmatter: { - mode: 'allowlist', - keepFields: ['name', 'description'], - descriptionLimit: null, - }, - - generation: { - generateMetadata: false, - skipSkills: ['codex'], - }, - - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.config/opencode/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.opencode/skills/gstack' }, - { from: '.claude/skills', to: '.opencode/skills' }, - ], - - suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'], + globalRoot: '.config/opencode/skills/gstack', // XDG config dir, not ~/.opencode + // OpenCode links a wider runtime asset set than the shared default + // (design binary, review specialists, qa templates/references, DX hall of fame). runtimeRoot: { globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'design/dist', 'gstack-upgrade', 'ETHOS.md', 'review/specialists', 'qa/templates', 'qa/references', 'plan-devex-review/dx-hall-of-fame.md'], globalFiles: { 'review': ['checklist.md', 'design-checklist.md', 'greptile-triage.md', 'TODOS-format.md'], }, }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - - learningsMode: 'basic', -}; +}); export default opencode; diff --git a/hosts/slate.ts b/hosts/slate.ts index 0c29cf8f6..99fa7197a 100644 --- a/hosts/slate.ts +++ b/hosts/slate.ts @@ -1,48 +1,8 @@ -import type { HostConfig } from '../scripts/host-config'; +import { defineHost } from './define-host'; -const slate: HostConfig = { +const slate = defineHost({ name: 'slate', displayName: 'Slate', - cliCommand: 'slate', - cliAliases: [], - - globalRoot: '.slate/skills/gstack', - localSkillRoot: '.slate/skills/gstack', - hostSubdir: '.slate', - usesEnvVars: true, - - frontmatter: { - mode: 'allowlist', - keepFields: ['name', 'description'], - descriptionLimit: null, - }, - - generation: { - generateMetadata: false, - skipSkills: ['codex'], - }, - - pathRewrites: [ - { from: '~/.claude/skills/gstack', to: '~/.slate/skills/gstack' }, - { from: '.claude/skills/gstack', to: '.slate/skills/gstack' }, - { from: '.claude/skills', to: '.slate/skills' }, - ], - - suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'], - - runtimeRoot: { - globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'gstack-upgrade', 'ETHOS.md'], - globalFiles: { - 'review': ['checklist.md', 'TODOS-format.md'], - }, - }, - - install: { - prefixable: false, - linkingStrategy: 'symlink-generated', - }, - - learningsMode: 'basic', -}; +}); export default slate;