docs: add ADDING_A_HOST.md guide + update docs for multi-host system

- docs/ADDING_A_HOST.md: step-by-step guide for adding a new host
  (create config, register, gitignore, generate, test). Covers the
  full HostConfig interface, adapter pattern, and validation.
- CONTRIBUTING.md: replace stale "Dual-host development" section with
  "Multi-host development" covering all 8 hosts and linking to the guide.
- README.md: consolidate Codex/Factory install sections into one
  "Other AI Agents" section listing all supported hosts with auto-detect.
- CLAUDE.md: add hosts/, host-config.ts, host-adapters/, contrib/ to
  project structure tree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-04 13:40:30 -07:00
parent 8247161d2b
commit 4b647fac03
4 changed files with 258 additions and 66 deletions
+11 -1
View File
@@ -63,8 +63,16 @@ gstack/
│ │ └── snapshot.ts # SNAPSHOT_FLAGS metadata array
│ ├── test/ # Integration tests + fixtures
│ └── dist/ # Compiled binary
├── hosts/ # Typed host configs (one per AI agent)
│ ├── claude.ts # Primary host config
│ ├── codex.ts, factory.ts, kiro.ts # Existing hosts
│ ├── opencode.ts, slate.ts, cursor.ts, openclaw.ts # New hosts
│ └── index.ts # Registry: exports all, derives Host type
├── scripts/ # Build + DX tooling
│ ├── gen-skill-docs.ts # Template → SKILL.md generator
│ ├── gen-skill-docs.ts # Template → SKILL.md generator (config-driven)
│ ├── host-config.ts # HostConfig interface + validator
│ ├── host-config-export.ts # Shell bridge for setup script
│ ├── host-adapters/ # Host-specific adapters (OpenClaw tool mapping)
│ ├── resolvers/ # Template resolver modules (preamble, design, review, etc.)
│ ├── skill-check.ts # Health dashboard
│ └── dev-skill.ts # Watch mode
@@ -108,6 +116,8 @@ gstack/
├── .github/ # CI workflows + Docker image
│ ├── workflows/ # evals.yml (E2E on Ubicloud), skill-docs.yml, actionlint.yml
│ └── docker/ # Dockerfile.ci (pre-baked toolchain + Playwright/Chromium)
├── contrib/ # Contributor-only tools (never installed for users)
│ └── add-host/ # /gstack-contrib-add-host skill
├── setup # One-time setup: build binary + symlink skills
├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly)
├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs
+47 -33
View File
@@ -216,11 +216,10 @@ SKILL.md files are **generated** from `.tmpl` templates. Don't edit the `.md` di
# 1. Edit the template
vim SKILL.md.tmpl # or browse/SKILL.md.tmpl
# 2. Regenerate for both hosts
bun run gen:skill-docs
bun run gen:skill-docs --host codex
# 2. Regenerate for all hosts
bun run gen:skill-docs --host all
# 3. Check health (reports both Claude and Codex)
# 3. Check health (reports all hosts)
bun run skill:check
# Or use watch mode — auto-regenerates on save
@@ -231,59 +230,74 @@ For template authoring best practices (natural language over bash-isms, dynamic
To add a browse command, add it to `browse/src/commands.ts`. To add a snapshot flag, add it to `SNAPSHOT_FLAGS` in `browse/src/snapshot.ts`. Then rebuild.
## Dual-host development (Claude + Codex)
## Multi-host development
gstack generates SKILL.md files for two hosts: **Claude** (`.claude/skills/`) and **Codex** (`.agents/skills/`). Every template change needs to be generated for both.
gstack generates SKILL.md files for 8 hosts from one set of `.tmpl` templates.
Each host is a typed config in `hosts/*.ts`. The generator reads these configs
to produce host-appropriate output (different frontmatter, paths, tool names).
### Generating for both hosts
**Supported hosts:** Claude (primary), Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw.
### Generating for all hosts
```bash
# Generate Claude output (default)
bun run gen:skill-docs
# Generate for a specific host
bun run gen:skill-docs # Claude (default)
bun run gen:skill-docs --host codex # Codex
bun run gen:skill-docs --host opencode # OpenCode
bun run gen:skill-docs --host all # All 8 hosts
# Generate Codex output
bun run gen:skill-docs --host codex
# --host agents is an alias for --host codex
# Or use build, which does both + compiles binaries
# Or use build, which does all hosts + compiles binaries
bun run build
```
### What changes between hosts
| Aspect | Claude | Codex |
|--------|--------|-------|
| Output directory | `{skill}/SKILL.md` | `.agents/skills/gstack-{skill}/SKILL.md` (generated at setup, gitignored) |
| Frontmatter | Full (name, description, voice-triggers, allowed-tools, hooks, version) | Minimal (name + description only) |
| Paths | `~/.claude/skills/gstack` | `$GSTACK_ROOT` (`.agents/skills/gstack` in a repo, otherwise `~/.codex/skills/gstack`) |
| Hook skills | `hooks:` frontmatter (enforced by Claude) | Inline safety advisory prose (advisory only) |
| `/codex` skill | Included (Claude wraps codex exec) | Excluded (self-referential) |
Each host config (`hosts/*.ts`) controls:
### Testing Codex output
| Aspect | Example (Claude vs Codex) |
|--------|---------------------------|
| Output directory | `{skill}/SKILL.md` vs `.agents/skills/gstack-{skill}/SKILL.md` |
| Frontmatter | Full (name, description, hooks, version) vs minimal (name + description) |
| Paths | `~/.claude/skills/gstack` vs `$GSTACK_ROOT` |
| Tool names | "use the Bash tool" vs same (Factory rewrites to "run this command") |
| Hook skills | `hooks:` frontmatter vs inline safety advisory prose |
| Suppressed sections | None vs Codex self-invocation sections stripped |
See `scripts/host-config.ts` for the full `HostConfig` interface.
### Testing host output
```bash
# Run all static tests (includes Codex validation)
# Run all static tests (includes parameterized smoke tests for all hosts)
bun test
# Check freshness for both hosts
bun run gen:skill-docs --dry-run
bun run gen:skill-docs --host codex --dry-run
# Check freshness for all hosts
bun run gen:skill-docs --host all --dry-run
# Health dashboard covers both hosts
# Health dashboard covers all hosts
bun run skill:check
```
### Dev setup for .agents/
### Adding a new host
When you run `bin/dev-setup`, it creates symlinks in both `.claude/skills/` and `.agents/skills/` (if applicable), so Codex-compatible agents can discover your dev skills too. The `.agents/` directory is generated at setup time from `.tmpl` templates — it is gitignored and not committed.
See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md) for the full guide. Short version:
1. Create `hosts/myhost.ts` (copy from `hosts/opencode.ts`)
2. Add to `hosts/index.ts`
3. Add `.myhost/` to `.gitignore`
4. Run `bun run gen:skill-docs --host myhost`
5. Run `bun test` (parameterized tests auto-cover it)
Zero generator, setup, or tooling code changes needed.
### Adding a new skill
When you add a new skill template, both hosts get it automatically:
When you add a new skill template, all hosts get it automatically:
1. Create `{skill}/SKILL.md.tmpl`
2. Run `bun run gen:skill-docs` (Claude output) and `bun run gen:skill-docs --host codex` (Codex output)
3. The dynamic template discovery picks it up no static list to update
4. Commit `{skill}/SKILL.md` — `.agents/` is generated at setup time and gitignored
2. Run `bun run gen:skill-docs --host all`
3. The dynamic template discovery picks it up, no static list to update
4. Commit `{skill}/SKILL.md`, external host output is generated at setup time and gitignored
## Conductor workspaces
+18 -32
View File
@@ -59,49 +59,35 @@ Real files get committed to your repo (not a submodule), so `git clone` just wor
> git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
> ```
### Codex, Gemini CLI, or Cursor
### Other AI Agents (Codex, OpenCode, Cursor, Slate, Factory, Kiro, OpenClaw)
gstack works on any agent that supports the [SKILL.md standard](https://github.com/anthropics/claude-code). Skills live in `.agents/skills/` and are discovered automatically.
Install to one repo:
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git .agents/skills/gstack
cd .agents/skills/gstack && ./setup --host codex
```
When setup runs from `.agents/skills/gstack`, it installs the generated Codex skills next to it in the same repo and does not write to `~/.codex/skills`.
Install once for your user account:
gstack works on any AI coding agent, not just Claude. Each host gets skill docs
generated from the same templates with host-appropriate paths, frontmatter, and
tool names. Setup auto-detects which agents you have installed.
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/gstack
cd ~/gstack && ./setup --host codex
cd ~/gstack && ./setup
```
`setup --host codex` creates the runtime root at `~/.codex/skills/gstack` and
links the generated Codex skills at the top level. This avoids duplicate skill
discovery from the source repo checkout.
Or let setup auto-detect which agents you have installed:
Setup auto-detects installed agents (`claude`, `codex`, `opencode`, `droid`,
`kiro-cli`, `slate`, `cursor`, `openclaw`) and installs skills for each one.
Or specify a host explicitly:
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/gstack
cd ~/gstack && ./setup --host auto
./setup --host codex # OpenAI Codex CLI
./setup --host opencode # OpenCode
./setup --host factory # Factory Droid
./setup --host cursor # Cursor
./setup --host openclaw # OpenClaw
```
For Codex-compatible hosts, setup now supports both repo-local installs from `.agents/skills/gstack` and user-global installs from `~/.codex/skills/gstack`. All 31 skills work across all supported agents. Hook-based safety skills (careful, freeze, guard) use inline safety advisory prose on non-Claude hosts.
All 31 skills work across all supported agents. Hook-based safety skills
(careful, freeze, guard) use inline safety advisory prose on non-Claude hosts.
Sensitive skills use `disable-model-invocation: true` on Factory Droid.
### Factory Droid
gstack works with [Factory Droid](https://factory.ai). Skills install to `.factory/skills/` and are discovered automatically. Sensitive skills (ship, land-and-deploy, guard) use `disable-model-invocation: true` so Droids don't auto-invoke them.
```bash
git clone --single-branch --depth 1 https://github.com/garrytan/gstack.git ~/gstack
cd ~/gstack && ./setup --host factory
```
Skills install to `~/.factory/skills/gstack-*/`. Restart `droid` to rescan skills, then type `/qa` to get started.
**Want to add support for another agent?** See [docs/ADDING_A_HOST.md](docs/ADDING_A_HOST.md).
It's one TypeScript config file, zero code changes.
### Voice input (AquaVoice, Whisper, etc.)
+182
View File
@@ -0,0 +1,182 @@
# 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.
## How it works
```
hosts/
├── 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 (hybrid: config + adapter)
└── index.ts # Registry: imports all, derives Host type
```
Each config file exports a `HostConfig` object that 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
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`:
```typescript
import type { HostConfig } from '../scripts/host-config';
const myhost: HostConfig = {
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;
```
### 2. Register in the index
Edit `hosts/index.ts`:
```typescript
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
];
// Add to re-exports:
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, myhost };
```
### 3. Add to .gitignore
Add `.myhost/` to `.gitignore` (generated skill docs are gitignored).
### 4. Generate and verify
```bash
# 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
```bash
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 |
|-------|---------|
| `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. |
| `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 |
| `adapter` | Path to adapter module for complex transformations |
## 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`.
The adapter runs as a post-processing step after all generic rewrites. It
exports `transform(content: string, config: HostConfig): string`.
## Validation
The `validateHostConfig()` function in `scripts/host-config.ts` checks:
- Name: lowercase alphanumeric with hyphens
- CLI command: alphanumeric with hyphens/underscores
- 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.