diff --git a/CLAUDE.md b/CLAUDE.md index 01be7974..9480e572 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2c67dc9..55cdccd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index 24bbe8b9..32df66b0 100644 --- a/README.md +++ b/README.md @@ -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.) diff --git a/docs/ADDING_A_HOST.md b/docs/ADDING_A_HOST.md new file mode 100644 index 00000000..50654e4e --- /dev/null +++ b/docs/ADDING_A_HOST.md @@ -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.