Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bin/gstack-global-discover*
.openclaw/
.hermes/
.gbrain/
.copilot/
.gbrain-source
.context/
extension/.auth.json
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ These are conversational skills. Your OpenClaw agent runs them directly via chat

### Other AI Agents

gstack works on 10 AI coding agents, not just Claude. Setup auto-detects which
gstack works on 11 AI coding agents, not just Claude. Setup auto-detects which
agents you have installed:

```bash
Expand All @@ -113,6 +113,7 @@ Or target a specific agent with `./setup --host <name>`:

| Agent | Flag | Skills install to |
|-------|------|-------------------|
| GitHub Copilot CLI + app | `--host copilot` | `~/.copilot/skills/gstack-*/` |
| OpenAI Codex CLI | `--host codex` | `~/.codex/skills/gstack-*/` |
| OpenCode | `--host opencode` | `~/.config/opencode/skills/gstack-*/` |
| Cursor | `--host cursor` | `~/.cursor/skills/gstack-*/` |
Expand All @@ -122,6 +123,15 @@ Or target a specific agent with `./setup --host <name>`:
| Hermes | `--host hermes` | `~/.hermes/skills/gstack-*/` |
| GBrain (mod) | `--host gbrain` | `~/.gbrain/skills/gstack-*/` |

Copilot CLI and the GitHub Copilot app use the same [Agent Skills
contract](https://docs.github.com/en/copilot/concepts/agents/about-agent-skills). One
gstack host supports both: personal skills install to `~/.copilot/skills`, and
`./setup --host copilot --local` installs repository skills to `.github/skills`.
GitHub documents that the [Copilot app automatically makes CLI and repository
skills available in project sessions](https://docs.github.com/en/copilot/how-tos/github-copilot-app/customize-github-copilot-app).
If `COPILOT_HOME` is set, setup installs personal skills under
`$COPILOT_HOME/skills` instead.

**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.

Expand Down
6 changes: 4 additions & 2 deletions bin/gstack-platform-detect
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ printf "%-16s %-10s %-40s %s\n" "-----" "-------" "----------" "------"

for host in $(bun run "$GSTACK_DIR/scripts/host-config-export.ts" list 2>/dev/null); do
cmd=$(bun run "$GSTACK_DIR/scripts/host-config-export.ts" get "$host" cliCommand 2>/dev/null)
root=$(bun run "$GSTACK_DIR/scripts/host-config-export.ts" get "$host" globalRoot 2>/dev/null)
spath="$HOME/$root"
spath=$(bun run "$GSTACK_DIR/scripts/host-config-export.ts" resolve-global-root "$host" 2>/dev/null)
if command -v cygpath >/dev/null 2>&1; then
spath=$(cygpath -u "$spath")
fi

if command -v "$cmd" >/dev/null 2>&1; then
ver=$("$cmd" --version 2>/dev/null | head -1 || echo "unknown")
Expand Down
30 changes: 30 additions & 0 deletions bin/gstack-uninstall
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# ~/.claude/skills/gstack — global Claude skill install (git clone or vendored)
# ~/.claude/skills/{skill} — per-skill symlinks created by setup
# ~/.codex/skills/gstack* — Codex skill install + per-skill symlinks
# ${COPILOT_HOME:-~/.copilot}/skills/gstack*
# — Copilot CLI/app personal skill install
# ~/.factory/skills/gstack* — Factory Droid skill install + per-skill symlinks
# ~/.kiro/skills/gstack* — Kiro skill install + per-skill symlinks
# ~/.gstack/ — global state (config, analytics, sessions, projects,
Expand All @@ -18,6 +20,7 @@
# .gstack/ — per-project browse state (in current git repo)
# .gstack-worktrees/ — per-project test worktrees (in current git repo)
# .agents/skills/gstack* — Codex/Gemini/Cursor sidecar (in current git repo)
# .github/skills/gstack* — Copilot repository skills (in current git repo)
# Running browse daemons — stopped via SIGTERM before cleanup
#
# What is NOT REMOVED:
Expand All @@ -38,6 +41,11 @@ fi

GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
COPILOT_CONFIG_HOME="${COPILOT_HOME:-$HOME/.copilot}"
if command -v cygpath >/dev/null 2>&1; then
COPILOT_CONFIG_HOME="$(cygpath -u "$COPILOT_CONFIG_HOME")"
fi
COPILOT_SKILLS="$COPILOT_CONFIG_HOME/skills"
_GIT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || true)"

# ─── Parse flags ─────────────────────────────────────────────
Expand All @@ -64,6 +72,7 @@ if [ "$FORCE" -eq 0 ]; then
echo "This will remove gstack from your system:"
{ [ -d "$HOME/.claude/skills/gstack" ] || [ -L "$HOME/.claude/skills/gstack" ]; } && echo " ~/.claude/skills/gstack (+ per-skill symlinks)"
[ -d "$HOME/.codex/skills" ] && echo " ~/.codex/skills/gstack*"
[ -d "$COPILOT_SKILLS" ] && echo " $COPILOT_SKILLS/gstack*"
[ -d "$HOME/.factory/skills" ] && echo " ~/.factory/skills/gstack*"
[ -d "$HOME/.kiro/skills" ] && echo " ~/.kiro/skills/gstack*"
[ "$KEEP_STATE" -eq 0 ] && [ -d "$STATE_DIR" ] && echo " $STATE_DIR"
Expand All @@ -73,6 +82,7 @@ if [ "$FORCE" -eq 0 ]; then
[ -d "$_GIT_ROOT/.gstack" ] && echo " $_GIT_ROOT/.gstack/ (browse state + reports)"
[ -d "$_GIT_ROOT/.gstack-worktrees" ] && echo " $_GIT_ROOT/.gstack-worktrees/"
[ -d "$_GIT_ROOT/.agents/skills" ] && echo " $_GIT_ROOT/.agents/skills/gstack*"
[ -d "$_GIT_ROOT/.github/skills" ] && echo " $_GIT_ROOT/.github/skills/gstack*"
fi

# Preview running daemons
Expand Down Expand Up @@ -171,6 +181,15 @@ if [ -d "$CODEX_SKILLS" ]; then
done
fi

# ─── Remove GitHub Copilot skills ────────────────────────────
if [ -d "$COPILOT_SKILLS" ]; then
for _ITEM in "$COPILOT_SKILLS"/gstack*; do
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
rm -rf "$_ITEM"
REMOVED+=("copilot/$(basename "$_ITEM")")
done
fi

# ─── Remove Factory Droid skills ────────────────────────────
FACTORY_SKILLS="$HOME/.factory/skills"
if [ -d "$FACTORY_SKILLS" ]; then
Expand Down Expand Up @@ -203,6 +222,17 @@ if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.agents/skills" ]; then
rmdir "$_GIT_ROOT/.agents" 2>/dev/null || true
fi

# ─── Remove per-project Copilot skills ──────────────────────
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.github/skills" ]; then
for _ITEM in "$_GIT_ROOT/.github/skills"/gstack*; do
[ -e "$_ITEM" ] || [ -L "$_ITEM" ] || continue
rm -rf "$_ITEM"
REMOVED+=("copilot/$(basename "$_ITEM")")
done

rmdir "$_GIT_ROOT/.github/skills" 2>/dev/null || true
fi

# ─── Remove per-project .factory/ sidecar ────────────────────
if [ -n "$_GIT_ROOT" ] && [ -d "$_GIT_ROOT/.factory/skills" ]; then
for _ITEM in "$_GIT_ROOT/.factory/skills"/gstack*; do
Expand Down
15 changes: 13 additions & 2 deletions docs/ADDING_A_HOST.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# 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
(Claude, Codex, Factory, Kiro, OpenCode, Slate, Cursor, OpenClaw, Copilot) 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.

Expand All @@ -17,6 +17,7 @@ hosts/
├── slate.ts # Slate (Random Labs)
├── cursor.ts # Cursor
├── openclaw.ts # OpenClaw (hybrid: config + adapter)
├── copilot.ts # GitHub Copilot CLI + app (shared Agent Skills contract)
└── index.ts # Registry: imports all, derives Host type
```

Expand All @@ -25,6 +26,7 @@ Each config file exports a `HostConfig` object that tells the generator:
- How to transform frontmatter (allowlist/denylist fields)
- What Claude-specific references to rewrite (paths, tool names)
- What binary to detect for auto-install
- Which product surfaces share the contract
- What resolver sections to suppress
- What assets to symlink at install time

Expand Down Expand Up @@ -133,10 +135,15 @@ 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
The parameterized smoke tests automatically pick up the new host. Zero generic test
code to write. They verify: output exists, no path leakage, valid frontmatter,
freshness check passes, codex skill excluded.

Add focused host tests for claims the generic matrix cannot prove, such as executable
detection, official discovery roots, host-specific frontmatter limits, and install
behavior. Copilot is the reference example because its CLI and app share one skill
format while using different personal and repository roots.

### 6. Update README.md

Add install instructions for the new host in the appropriate section.
Expand All @@ -155,7 +162,11 @@ 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) |
| `frontmatter.namePrefix` | Prefix generated names for hosts that require name to match the skill directory |
| `frontmatter.nameLimit` | Fail generation when the host's maximum skill-name length is exceeded |
| `pathRewrites` | Literal replaceAll on content. Order matters. |
| `supportedSurfaces` | Product surfaces covered by one contract (`cli`, optionally `app`) |
| `globalRootEnv` | Optional config-root override env var honored by setup and generated preambles |
| `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 |
Expand Down
48 changes: 1 addition & 47 deletions gstack-upgrade/SKILL.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -143,53 +143,7 @@ cd "$INSTALL_DIR" && ./setup
rm -rf "$INSTALL_DIR.bak" "$TMP_DIR"
```

### Step 4.5: Handle local vendored copy

Use the install directory from Step 2. Check if there's also a local vendored copy, and whether team mode is active:

```bash
_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
LOCAL_GSTACK=""
if [ -n "$_ROOT" ] && [ -d "$_ROOT/.claude/skills/gstack" ]; then
_RESOLVED_LOCAL=$(cd "$_ROOT/.claude/skills/gstack" && pwd -P)
_RESOLVED_PRIMARY=$(cd "$INSTALL_DIR" && pwd -P)
if [ "$_RESOLVED_LOCAL" != "$_RESOLVED_PRIMARY" ]; then
LOCAL_GSTACK="$_ROOT/.claude/skills/gstack"
fi
fi
_TEAM_MODE=$(~/.claude/skills/gstack/bin/gstack-config get team_mode 2>/dev/null || echo "false")
echo "LOCAL_GSTACK=$LOCAL_GSTACK"
echo "TEAM_MODE=$_TEAM_MODE"
```

**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is `true`:** Remove the vendored copy. Team mode uses the global install as the single source of truth.

```bash
cd "$_ROOT"
git rm -r --cached .claude/skills/gstack/ 2>/dev/null || true
if ! grep -qF '.claude/skills/gstack/' .gitignore 2>/dev/null; then
echo '.claude/skills/gstack/' >> .gitignore
fi
rm -rf "$LOCAL_GSTACK"
```
Tell user: "Removed vendored copy at `$LOCAL_GSTACK` (team mode active — global install is the source of truth). Commit the `.gitignore` change when ready."

**If `LOCAL_GSTACK` is non-empty AND `TEAM_MODE` is NOT `true`:** Update it by copying from the freshly-upgraded primary install (same approach as README vendored install):
```bash
mv "$LOCAL_GSTACK" "$LOCAL_GSTACK.bak"
cp -Rf "$INSTALL_DIR" "$LOCAL_GSTACK"
rm -rf "$LOCAL_GSTACK/.git"
cd "$LOCAL_GSTACK" && ./setup
rm -rf "$LOCAL_GSTACK.bak"
```
Tell user: "Also updated vendored copy at `$LOCAL_GSTACK` — commit `.claude/skills/gstack/` when you're ready."

If `./setup` fails, restore from backup and warn the user:
```bash
rm -rf "$LOCAL_GSTACK"
mv "$LOCAL_GSTACK.bak" "$LOCAL_GSTACK"
```
Tell user: "Sync failed — restored previous version at `$LOCAL_GSTACK`. Run `/gstack-upgrade` manually to retry."
{{UPGRADE_LOCAL_INSTALL_SYNC}}

### Step 4.75: Run version migrations

Expand Down
150 changes: 150 additions & 0 deletions hosts/copilot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { HostConfig } from '../scripts/host-config';

/**
* GitHub documents one Agent Skills contract for Copilot CLI and the Copilot app:
* personal skills live in ~/.copilot/skills and repository skills in .github/skills.
* Keep generated staging under .copilot/ so Copilot never collides with Codex's
* .agents/skills output.
*/
const copilot: HostConfig = {
name: 'copilot',
displayName: 'GitHub Copilot CLI and app',
cliCommand: 'copilot',
cliAliases: [],
supportedSurfaces: ['cli', 'app'],

globalRoot: '.copilot/skills/gstack',
globalRootEnv: 'COPILOT_HOME',
localSkillRoot: '.github/skills/gstack',
hostSubdir: '.copilot',
usesEnvVars: true,

frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
namePrefix: 'gstack-',
nameLimit: 64,
descriptionLimit: 1024,
descriptionLimitBehavior: 'error',
},

generation: {
generateMetadata: false,
skipSkills: ['codex'],
},

pathRewrites: [
{ from: '${HOME}/.claude/skills/gstack', to: '$GSTACK_ROOT' },
{
from: '$HOME/.claude/skills/gstack/.git',
to: '$(cat "$GSTACK_ROOT/.source-path")/.git',
},
{
from: 'INSTALL_DIR="$HOME/.claude/skills/gstack"',
to: 'INSTALL_DIR="$(cat "$GSTACK_ROOT/.source-path")"',
},
{ from: '$HOME/.claude/skills/gstack', to: '$GSTACK_ROOT' },
{ from: '~/.claude/skills/gstack', to: '$GSTACK_ROOT' },
{ from: '.claude/skills/gstack', to: '$GSTACK_ROOT' },
{ from: '.claude/skills/review', to: '$GSTACK_ROOT/review' },
{
from: '[ -f "$GSTACK_ROOT/VERSION" ] || [ -d "$GSTACK_ROOT/.git" ]',
to: '[ -d "$GSTACK_ROOT/.git" ]',
},
{ from: '\n./setup\n', to: '\n./setup --host copilot\n' },
{ from: '&& ./setup\n', to: '&& ./setup --host copilot\n' },
{ from: '.claude/skills', to: '.github/skills' },
{ from: '.agents/skills/gstack', to: '.github/skills/gstack' },
{ from: '.agents/skills', to: '.github/skills' },
],
toolRewrites: {
'AskUserQuestion': 'ask_user',
'use the Bash tool': 'use the bash tool',
'use the Write tool': 'edit the file',
'use the Read tool': 'use the view tool',
'use the Edit tool': 'edit the file',
'use the Agent tool': 'use the task tool',
'use the Grep tool': 'search the repository',
'use the Glob tool': 'find matching files',
'the Bash tool': 'the bash tool',
'the Write tool': 'file editing',
'the Read tool': 'the view tool',
'the Edit tool': 'file editing',
'the Agent tool': 'the task tool',
'the Grep tool': 'repository search',
'the Glob tool': 'file matching',
},

// Copilot supports task and skill tools, so its same-host subagent orchestration
// remains enabled. Only optional gbrain blocks are suppressed by default.
suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'],

runtimeRoot: {
globalSymlinks: [
'bin',
'browse/dist',
'browse/bin',
'design/dist',
'make-pdf/dist',
'design-html/vendor',
'extension',
'hosts',
'lib',
'scripts',
'ios-qa/templates',
'ios-qa/scripts',
'ios-qa/daemon',
'supabase',
'VERSION',
'CHANGELOG.md',
'setup',
'package.json',
'bun.lock',
'ETHOS.md',
'office-hours/SKILL.md',
'document-release/SKILL.md',
'plan-ceo-review/SKILL.md',
'plan-design-review/SKILL.md',
'plan-devex-review/SKILL.md',
'plan-eng-review/SKILL.md',
'gstack-upgrade/SKILL.md',
'gstack-upgrade/migrations',
'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'],
},
},
sidecar: {
path: '.github/skills/gstack',
symlinks: [
'bin',
'browse',
'design',
'make-pdf',
'design-html',
'extension',
'hosts',
'lib',
'scripts',
'gstack-upgrade',
'ETHOS.md',
'review',
'qa',
'plan-devex-review',
],
},

install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},

coAuthorTrailer: 'Co-Authored-By: GitHub Copilot <copilot@github.com>',
learningsMode: 'basic',
};

export default copilot;
Loading
Loading