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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ bun run skill:check # health dashboard for all skills
## Key conventions

- SKILL.md files are **generated** from `.tmpl` templates. Edit the template, not the output.
- Run `bun run gen:skill-docs --host codex` to regenerate Codex-specific output.
- Run `bun run gen:skill-docs --host codex` to regenerate Codex output (Copilot derives from this at setup time).
- The browse binary provides headless browser access. Use `$B <command>` in skills.
- Safety skills (careful, freeze, guard) use inline advisory prose — always confirm before destructive operations.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## [0.11.10.0] - 2026-03-23 — Copilot CLI Support

### Added

- **gstack now works with GitHub Copilot CLI.** Run `./setup --host copilot` and all 28 skills install to `~/.copilot/skills/`. Works the same as Codex — skills live in `.agents/skills/` and are discovered automatically.
- **Auto-detection finds Copilot CLI.** `./setup --host auto` now detects `copilot` alongside Claude Code, Codex, and Kiro. Install once, works everywhere.
- **Session discovery includes Copilot.** The global discover tool (`bin/gstack-global-discover.ts`) scans `~/.copilot/session-state/` so `/retro` and cross-project dashboards count Copilot sessions.
- **Health checks cover Copilot.** `bun run skill:check` and CI now verify Copilot skill freshness alongside Claude and Codex.

### For contributors

- Added `'copilot'` host type to `gen-skill-docs.ts`, `setup`, `gstack-global-discover.ts`, and `skill-check.ts`.
- New E2E test infrastructure: `copilot-e2e.test.ts` and `copilot-session-runner.ts` paralleling Codex equivalents.
- Updated `CONTRIBUTING.md` "Dual-host" → "Multi-host" with Copilot generation commands and testing guidance.
- Targets the standalone GA Copilot CLI (`copilot` binary via `npm install -g @github/copilot`), not the legacy `gh copilot` extension.

## [0.11.9.0] - 2026-03-23 — Codex Skill Loading Fix

### Fixed
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bun run eval:summary # aggregate stats across all eval runs

`test:evals` requires `ANTHROPIC_API_KEY`. Codex E2E tests (`test/codex-e2e.test.ts`)
use Codex's own auth from `~/.codex/` config — no `OPENAI_API_KEY` env var needed.
Copilot E2E tests (`test/copilot-e2e.test.ts`) require the standalone Copilot CLI (`npm install -g @github/copilot`).
E2E tests stream progress in real-time (tool-by-tool via `--output-format stream-json
--verbose`). Results are persisted to `~/.gstack-dev/evals/` with auto-comparison
against the previous run.
Expand Down
40 changes: 20 additions & 20 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,11 @@ 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
# 2. Regenerate for all hosts
bun run gen:skill-docs
bun run gen:skill-docs --host codex

# 3. Check health (reports both Claude and Codex)
# 3. Check health (reports Claude and Codex; Copilot derives from Codex at setup time)
bun run skill:check

# Or use watch mode — auto-regenerates on save
Expand All @@ -228,17 +228,17 @@ 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 (Claude + Codex + Copilot)

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 multiple hosts: **Claude** (`.claude/skills/`), **Codex** (`.agents/skills/`), and **Copilot** (`.agents/skills/`). Codex and Copilot share the same `.agents/skills/` output directory — only `--host codex` needs to be run during generation. Copilot-specific paths are rewritten at `setup --host copilot` time via sed. Every template change needs to be generated for Claude and Codex.

### Generating for both hosts
### Generating for all hosts

```bash
# Generate Claude output (default)
bun run gen:skill-docs

# Generate Codex output
# Generate Codex output (Copilot shares this — paths are rewritten at setup time)
bun run gen:skill-docs --host codex
# --host agents is an alias for --host codex

Expand All @@ -248,37 +248,37 @@ 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, 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) |
| Aspect | Claude | Codex | Copilot |
|--------|--------|-------|---------|
| Output directory | `{skill}/SKILL.md` | `.agents/skills/gstack-{skill}/SKILL.md` (generated at setup, gitignored) | `.agents/skills/gstack-{skill}/SKILL.md` (generated at setup, gitignored) |
| Frontmatter | Full (name, description, allowed-tools, hooks, version) | Minimal (name + description only) | Minimal (name + description only) |
| Paths | `~/.claude/skills/gstack` | `$GSTACK_ROOT` (`.agents/skills/gstack` in a repo, otherwise `~/.codex/skills/gstack`) | `$GSTACK_ROOT` (`.agents/skills/gstack` in a repo, otherwise `~/.copilot/skills/gstack`) |
| Hook skills | `hooks:` frontmatter (enforced by Claude) | Inline safety advisory prose (advisory only) | Inline safety advisory prose (advisory only) |
| `/codex` skill | Included (Claude wraps codex exec) | Excluded (self-referential) | Excluded (shares Codex output) |

### Testing Codex output
### Testing Codex and Copilot output

```bash
# Run all static tests (includes Codex validation)
# Run all static tests (includes Codex and Copilot validation)
bun test

# Check freshness for both hosts
# Check freshness for all hosts
bun run gen:skill-docs --dry-run
bun run gen:skill-docs --host codex --dry-run

# Health dashboard covers both hosts
# Health dashboard covers all hosts
bun run skill:check
```

### Dev setup for .agents/

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.
When you run `bin/dev-setup`, it creates symlinks in both `.claude/skills/` and `.agents/skills/` (if applicable), so Codex, Copilot, and other 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.

### 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)
2. Run `bun run gen:skill-docs` (Claude output) and `bun run gen:skill-docs --host codex` (Codex output — Copilot derives from this at setup time)
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

Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,28 +54,29 @@ Open Claude Code and paste this. Claude does the rest.
Real files get committed to your repo (not a submodule), so `git clone` just works. Everything lives inside `.claude/`. Nothing touches your PATH or runs in the background.

### Codex, Gemini CLI, or Cursor
### Codex, Gemini CLI, Copilot CLI, or Cursor

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 https://github.com/garrytan/gstack.git .agents/skills/gstack
cd .agents/skills/gstack && ./setup --host codex
cd .agents/skills/gstack && ./setup --host codex # or --host copilot
```

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`.
When setup runs from `.agents/skills/gstack`, it installs the generated Codex and Copilot skills next to it in the same repo and does not write to `~/.codex/skills` or `~/.copilot/skills`.

Install once for your user account:

```bash
git clone https://github.com/garrytan/gstack.git ~/gstack
cd ~/gstack && ./setup --host codex
cd ~/gstack && ./setup --host codex # or --host copilot
```

`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
links the generated Codex skills at the top level. `setup --host copilot` does
the same at `~/.copilot/skills/gstack`. This avoids duplicate skill
discovery from the source repo checkout.

Or let setup auto-detect which agents you have installed:
Expand All @@ -85,7 +86,7 @@ git clone https://github.com/garrytan/gstack.git ~/gstack
cd ~/gstack && ./setup --host auto
```

For Codex-compatible hosts, setup now supports both repo-local installs from `.agents/skills/gstack` and user-global installs from `~/.codex/skills/gstack`. All 28 skills work across all supported agents. Hook-based safety skills (careful, freeze, guard) use inline safety advisory prose on non-Claude hosts.
For Codex, Copilot, and other compatible hosts, setup supports both repo-local installs from `.agents/skills/gstack` and user-global installs from `~/.codex/skills/gstack` or `~/.copilot/skills/gstack`. All 28 skills work across all supported agents. Hook-based safety skills (careful, freeze, guard) use inline safety advisory prose on non-Claude hosts.

## See it work

Expand Down Expand Up @@ -156,7 +157,7 @@ Each skill feeds into the next. `/office-hours` writes a design doc that `/plan-
| `/canary` | **SRE** | Post-deploy monitoring loop. Watches for console errors, performance regressions, and page failures. |
| `/benchmark` | **Performance Engineer** | Baseline page load times, Core Web Vitals, and resource sizes. Compare before/after on every PR. |
| `/document-release` | **Technical Writer** | Update all project docs to match what you just shipped. Catches stale READMEs automatically. |
| `/retro` | **Eng Manager** | Team-aware weekly retro. Per-person breakdowns, shipping streaks, test health trends, growth opportunities. `/retro global` runs across all your projects and AI tools (Claude Code, Codex, Gemini). |
| `/retro` | **Eng Manager** | Team-aware weekly retro. Per-person breakdowns, shipping streaks, test health trends, growth opportunities. `/retro global` runs across all your projects and AI tools (Claude Code, Codex, Gemini, Copilot). |
| `/browse` | **QA Engineer** | Real Chromium browser, real clicks, real screenshots. ~100ms per command. |
| `/setup-browser-cookies` | **Session Manager** | Import cookies from your real browser (Chrome, Arc, Brave, Edge) into the headless session. Test authenticated pages. |
| `/autoplan` | **Review Pipeline** | One command, fully reviewed plan. Runs CEO → design → eng review automatically with encoded decision principles. Surfaces only taste decisions for your approval. |
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.11.9.0
0.11.10.0
89 changes: 78 additions & 11 deletions bin/gstack-global-discover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
/**
* gstack-global-discover — Discover AI coding sessions across Claude Code, Codex CLI, and Gemini CLI.
* gstack-global-discover — Discover AI coding sessions across Claude Code, Codex CLI, Gemini CLI, and Copilot CLI.
* Resolves each session's working directory to a git repo, deduplicates by normalized remote URL,
* and outputs structured JSON to stdout.
*
Expand All @@ -17,15 +17,15 @@ import { homedir } from "os";
// ── Types ──────────────────────────────────────────────────────────────────

interface Session {
tool: "claude_code" | "codex" | "gemini";
tool: "claude_code" | "codex" | "gemini" | "copilot";
cwd: string;
}

interface Repo {
name: string;
remote: string;
paths: string[];
sessions: { claude_code: number; codex: number; gemini: number };
sessions: { claude_code: number; codex: number; gemini: number; copilot: number };
}

interface DiscoveryResult {
Expand All @@ -36,6 +36,7 @@ interface DiscoveryResult {
claude_code: { total_sessions: number; repos: number };
codex: { total_sessions: number; repos: number };
gemini: { total_sessions: number; repos: number };
copilot: { total_sessions: number; repos: number };
};
total_sessions: number;
total_repos: number;
Expand Down Expand Up @@ -440,7 +441,69 @@ function scanGemini(since: Date): Session[] {
return sessions;
}

// ── Deduplication ──────────────────────────────────────────────────────────
function scanCopilot(since: Date): Session[] {
// GitHub Copilot CLI (standalone) stores session data in ~/.copilot/session-state/
// Each session is a subdirectory: ~/.copilot/session-state/{session-id}/
// containing events.jsonl, workspace.yaml, and other session files.
const sessionStateDir = join(homedir(), ".copilot", "session-state");
if (!existsSync(sessionStateDir)) return [];

const sessions: Session[] = [];

try {
const sessionDirs = readdirSync(sessionStateDir);
for (const sessionId of sessionDirs) {
const sessionDir = join(sessionStateDir, sessionId);
try {
const dirStat = statSync(sessionDir);
if (!dirStat.isDirectory() || dirStat.mtime < since) continue;

// Look for session files within the subdirectory
const sessionFiles = readdirSync(sessionDir);
let found = false;

for (const file of sessionFiles) {
if (found) break;
const filePath = join(sessionDir, file);
try {
const fileStat = statSync(filePath);
if (!fileStat.isFile()) continue;
if (!file.endsWith(".json") && !file.endsWith(".jsonl") && !file.endsWith(".yaml")) continue;

const fd = openSync(filePath, "r");
const buf = Buffer.alloc(4096);
const bytesRead = readSync(fd, buf, 0, 4096, 0);
closeSync(fd);
const text = buf.toString("utf-8", 0, bytesRead);

// Look for cwd in JSON/JSONL metadata
for (const line of text.split("\n").slice(0, 15)) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
if (obj.cwd && existsSync(obj.cwd)) {
sessions.push({ tool: "copilot", cwd: obj.cwd });
found = true;
break;
}
} catch {
continue;
}
}
} catch {
continue;
}
}
} catch {
continue;
}
}
} catch {
// Directory read error
}

return sessions;
}

async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
// Group sessions by cwd
Expand Down Expand Up @@ -496,7 +559,7 @@ async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
}
}

const sessionCounts = { claude_code: 0, codex: 0, gemini: 0 };
const sessionCounts = { claude_code: 0, codex: 0, gemini: 0, copilot: 0 };
for (const s of data.sessions) {
sessionCounts[s.tool]++;
}
Expand All @@ -512,8 +575,8 @@ async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
// Sort by total sessions descending
repos.sort(
(a, b) =>
b.sessions.claude_code + b.sessions.codex + b.sessions.gemini -
(a.sessions.claude_code + a.sessions.codex + a.sessions.gemini)
b.sessions.claude_code + b.sessions.codex + b.sessions.gemini + b.sessions.copilot -
(a.sessions.claude_code + a.sessions.codex + a.sessions.gemini + a.sessions.copilot)
);

return repos;
Expand All @@ -530,12 +593,13 @@ async function main() {
const ccSessions = scanClaudeCode(sinceDate);
const codexSessions = scanCodex(sinceDate);
const geminiSessions = scanGemini(sinceDate);
const copilotSessions = scanCopilot(sinceDate);

const allSessions = [...ccSessions, ...codexSessions, ...geminiSessions];
const allSessions = [...ccSessions, ...codexSessions, ...geminiSessions, ...copilotSessions];

// Summary to stderr
console.error(
`Discovered: ${ccSessions.length} CC sessions, ${codexSessions.length} Codex sessions, ${geminiSessions.length} Gemini sessions`
`Discovered: ${ccSessions.length} CC sessions, ${codexSessions.length} Codex sessions, ${geminiSessions.length} Gemini sessions, ${copilotSessions.length} Copilot sessions`
);

// Deduplicate
Expand All @@ -547,6 +611,7 @@ async function main() {
const ccRepos = new Set(repos.filter((r) => r.sessions.claude_code > 0).map((r) => r.remote)).size;
const codexRepos = new Set(repos.filter((r) => r.sessions.codex > 0).map((r) => r.remote)).size;
const geminiRepos = new Set(repos.filter((r) => r.sessions.gemini > 0).map((r) => r.remote)).size;
const copilotRepos = new Set(repos.filter((r) => r.sessions.copilot > 0).map((r) => r.remote)).size;

const result: DiscoveryResult = {
window: since,
Expand All @@ -556,6 +621,7 @@ async function main() {
claude_code: { total_sessions: ccSessions.length, repos: ccRepos },
codex: { total_sessions: codexSessions.length, repos: codexRepos },
gemini: { total_sessions: geminiSessions.length, repos: geminiRepos },
copilot: { total_sessions: copilotSessions.length, repos: copilotRepos },
},
total_sessions: allSessions.length,
total_repos: repos.length,
Expand All @@ -566,15 +632,16 @@ async function main() {
} else {
// Summary format
console.log(`Window: ${since} (since ${startDate})`);
console.log(`Sessions: ${allSessions.length} total (CC: ${ccSessions.length}, Codex: ${codexSessions.length}, Gemini: ${geminiSessions.length})`);
console.log(`Sessions: ${allSessions.length} total (CC: ${ccSessions.length}, Codex: ${codexSessions.length}, Gemini: ${geminiSessions.length}, Copilot: ${copilotSessions.length})`);
console.log(`Repos: ${repos.length} unique`);
console.log("");
for (const repo of repos) {
const total = repo.sessions.claude_code + repo.sessions.codex + repo.sessions.gemini;
const total = repo.sessions.claude_code + repo.sessions.codex + repo.sessions.gemini + repo.sessions.copilot;
const tools = [];
if (repo.sessions.claude_code > 0) tools.push(`CC:${repo.sessions.claude_code}`);
if (repo.sessions.codex > 0) tools.push(`Codex:${repo.sessions.codex}`);
if (repo.sessions.gemini > 0) tools.push(`Gemini:${repo.sessions.gemini}`);
if (repo.sessions.copilot > 0) tools.push(`Copilot:${repo.sessions.copilot}`);
console.log(` ${repo.name} (${total} sessions) — ${tools.join(", ")}`);
console.log(` Remote: ${repo.remote}`);
console.log(` Paths: ${repo.paths.join(", ")}`);
Expand Down
Loading