Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ffe11d9
added --no-browser option
jimmckeeth Mar 30, 2026
9299def
Windows installer
jimmckeeth Mar 30, 2026
8e9c8af
Merge branch 'garrytan:main' into windows-installer
jimmckeeth Mar 30, 2026
3cb9e4c
Merge branch 'main' into no-browser
jimmckeeth Mar 30, 2026
b7e174d
feat: --no-browser setup flag with config persistence
jimmckeeth Mar 30, 2026
3034de6
Merge remote-tracking branch 'upstream/main' into no-browser
jimmckeeth Mar 30, 2026
954cfe7
Merge upstream/main and bump version to v0.13.11.0 (No-Browser Mode)
jimmckeeth Mar 30, 2026
dad2424
Merge remote-tracking branch 'upstream/main' into windows-installer
jimmckeeth Mar 30, 2026
5950285
Merge upstream/main and bump version to v0.14.1.0 (No-Browser Mode)
jimmckeeth Mar 30, 2026
f222904
fix: remove leaked no-browser changes from metadata
jimmckeeth Mar 30, 2026
bd61963
Merge upstream/main and resolve conflicts (bump No-Browser mode to 0.…
jimmckeeth Mar 30, 2026
a47f476
Merge remote-tracking branch 'upstream/main' into windows-installer
jimmckeeth Mar 30, 2026
4ffdc30
Merge upstream/main into no-browser, bump to v0.15.2.0
jimmckeeth Apr 2, 2026
f022bec
Merge remote-tracking branch 'upstream/main' into windows-installer
jimmckeeth Apr 2, 2026
808441a
Merge upstream/main into windows-installer, bump to v0.15.3.0
jimmckeeth Apr 2, 2026
00572a7
revert VERSION/package.json/CHANGELOG to match upstream/main
jimmckeeth Apr 2, 2026
356972a
revert VERSION/package.json/CHANGELOG to match upstream/main
jimmckeeth Apr 2, 2026
fa878a1
Add Copilot CLI support via Claude-compatible skills
ridermw Mar 25, 2026
c5fae60
Add Copilot to global retro discovery
ridermw Mar 25, 2026
e37c90c
Merged in Copilot support
jimmckeeth Apr 2, 2026
3d66bbc
Merge pull request #1 from jimmckeeth/no-browser
jimmckeeth Apr 2, 2026
5dc32fa
Merge branch 'main' into windows-installer
jimmckeeth Apr 2, 2026
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
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ To add a browse command, add it to `browse/src/commands.ts`. To add a snapshot f

gstack generates SKILL.md files for two hosts: **Claude** (`.claude/skills/`) and **Codex** (`.agents/skills/`). Every template change needs to be generated for both.

**Copilot CLI support intentionally reuses the Claude-compatible `.claude/skills/` output.**
There is no separate `--host copilot` generator mode to maintain. If you change
install paths or setup behavior, make sure the Claude-format skill tree still works
for Copilot's documented `.claude/skills` support.

### Generating for both hosts

```bash
Expand Down
54 changes: 51 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Fork it. Improve it. Make it yours. And if you want to hate on free open source

## Install — 30 seconds

**Requirements:** [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Git](https://git-scm.com/), [Bun](https://bun.sh/) v1.0+, [Node.js](https://nodejs.org/) (Windows only)
**Requirements:** One supported host ([Claude Code](https://docs.anthropic.com/en/docs/claude-code), [GitHub Copilot CLI](https://docs.github.com/copilot/how-tos/use-copilot-agents/use-copilot-cli), or another SKILL.md-compatible agent), [Git](https://git-scm.com/), [Bun](https://bun.sh/) v1.0+, [Node.js](https://nodejs.org/) ([Windows](#windows) only)

### Step 1: Install on your machine

Expand All @@ -59,6 +59,23 @@ 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
> ```

### GitHub Copilot CLI

GitHub Copilot CLI officially supports skills from `.claude/skills` and
`~/.claude/skills`, so the lowest-friction gstack install reuses the same
Claude-compatible layout instead of introducing a separate Copilot-only tree.

Install once for your user account:

```bash
git clone https://github.com/garrytan/gstack.git ~/.claude/skills/gstack
cd ~/.claude/skills/gstack && ./setup --host copilot
```

For repo-local installs, vendoring gstack into `.claude/skills/gstack` works with
Copilot CLI too. Put your guidance in `AGENTS.md` or `.github/copilot-instructions.md`,
then use `/skills reload` if Copilot is already running.

### Codex, Gemini 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.
Expand Down Expand Up @@ -103,6 +120,28 @@ cd ~/gstack && ./setup --host factory

Skills install to `~/.factory/skills/gstack-*/`. Restart `droid` to rescan skills, then type `/qa` to get started.

### Windows

**Windows users:** gstack works on Windows 11 via Git Bash or WSL. Node.js is required in addition to Bun — Bun has a known bug with Playwright's pipe transport on Windows ([bun#4253](https://github.com/oven-sh/bun/issues/4253)). The browse server automatically falls back to `Node.js`. Make sure both `bun` and `node` are on your PATH.

On Windows, you can use the PowerShell wrapper instead of `./setup`. It auto-installs `Node.js`, `Bun`, and `Git` via `winget` if they're missing, then delegates to `./setup` inside Git Bash. Git Bash has the same home path as Windows (unlike WSL) so the sills install when run outside Git Bash too.

```powershell
.\setup.ps1 --keep-open --auto
# ^ ^ Passed to setup
# | Handled by setup.ps1
```

The window stays open for 20 seconds after setup completes so you can read the output. Control this with flags:

| Flag | Behavior |
| ----------------| -------------------------------------------------------- |
| *(default)* | Prompts "Autoclose [Y/n]" — auto-closes after 20s |
| `--keep-open` | Drops to an interactive bash shell after setup completes |
| `--auto-close` | Closes the window immediately on completion |

All other flags (`--no-browser`, `--prefix`, `--no-prefix`, `--host`, etc.) are forwarded to `./setup`.

## See it work

```
Expand Down Expand Up @@ -174,7 +213,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** | Give the agent eyes. Real Chromium browser, real clicks, real screenshots. ~100ms per command. `$B connect` launches your real Chrome as a headed window — watch every action live. |
| `/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 Expand Up @@ -278,9 +317,18 @@ Data is stored in [Supabase](https://supabase.com) (open source Firebase alterna

**Want namespaced commands?** `cd ~/.claude/skills/gstack && ./setup --prefix` — switches from `/qa` to `/gstack-qa`. Useful if you run other skill packs alongside gstack.

**Don't need browser features?** Pass `--no-browser` to skip building the browse binary and installing Playwright Chromium. Useful on CI, headless servers, or if you only use the non-browser skills. `/browse`, `/qa`, and `/connect-chrome` won't work without it.

```bash
./setup --no-browser # Unix / macOS / Git Bash on Windows
```

**Codex says "Skipped loading skill(s) due to invalid SKILL.md"?** Your Codex skill descriptions are stale. Fix: `cd ~/.codex/skills/gstack && git pull && ./setup --host codex` — or for repo-local installs: `cd "$(readlink -f .agents/skills/gstack)" && git pull && ./setup --host codex`

**Windows users:** gstack works on Windows 11 via Git Bash or WSL. Node.js is required in addition to Bun — Bun has a known bug with Playwright's pipe transport on Windows ([bun#4253](https://github.com/oven-sh/bun/issues/4253)). The browse server automatically falls back to Node.js. Make sure both `bun` and `node` are on your PATH.
**Copilot CLI doesn't see the skills?** Run `/skills list` or `/skills reload`. If you're using the Claude-compatible install path, rerun: `cd ~/.claude/skills/gstack && ./setup --host copilot`


**Copilot CLI doesn't see the skills?** Run `/skills list` or `/skills reload`. If you're using the Claude-compatible install path, rerun: `cd ~/.claude/skills/gstack && ./setup --host copilot`

**Claude says it can't see the skills?** Make sure your project's `CLAUDE.md` has a gstack section. Add this:

Expand Down
118 changes: 108 additions & 10 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 GitHub 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 @@ -290,6 +291,42 @@ function extractCwdFromJsonl(filePath: string): string | null {
return null;
}

function extractCopilotCwdFromWorkspace(filePath: string): string | null {
try {
const text = readFileSync(filePath, { encoding: "utf-8" });
const match = text.match(/^cwd:\s*(.+)$/m);
if (!match) return null;
return match[1].trim().replace(/^['"]|['"]$/g, "");
} catch {
return null;
}
}

function extractCopilotCwdFromEvents(filePath: string): string | null {
try {
const fd = openSync(filePath, "r");
const buf = Buffer.alloc(16384);
const bytesRead = readSync(fd, buf, 0, 16384, 0);
closeSync(fd);
const lines = buf.toString("utf-8", 0, bytesRead).split("\n").slice(0, 25);
for (const line of lines) {
if (!line.trim()) continue;
try {
const obj = JSON.parse(line);
const cwd = obj.data?.context?.cwd;
if (obj.type === "session.start" && typeof cwd === "string") {
return cwd;
}
} catch {
continue;
}
}
} catch {
// File read error
}
return null;
}

function scanCodex(since: Date): Session[] {
const sessionsDir = join(homedir(), ".codex", "sessions");
if (!existsSync(sessionsDir)) return [];
Expand Down Expand Up @@ -440,6 +477,59 @@ function scanGemini(since: Date): Session[] {
return sessions;
}

function scanCopilot(since: Date): Session[] {
const sessionsDir = join(homedir(), ".copilot", "session-state");
if (!existsSync(sessionsDir)) return [];

const sessions: Session[] = [];

let dirs: string[];
try {
dirs = readdirSync(sessionsDir);
} catch {
return [];
}

for (const dirName of dirs) {
const dirPath = join(sessionsDir, dirName);
try {
const stat = statSync(dirPath);
if (!stat.isDirectory()) continue;
} catch {
continue;
}

const workspacePath = join(dirPath, "workspace.yaml");
const eventsPath = join(dirPath, "events.jsonl");

const hasRecentWorkspace = existsSync(workspacePath) && (() => {
try {
return statSync(workspacePath).mtime >= since;
} catch {
return false;
}
})();
const hasRecentEvents = existsSync(eventsPath) && (() => {
try {
return statSync(eventsPath).mtime >= since;
} catch {
return false;
}
})();
if (!hasRecentWorkspace && !hasRecentEvents) continue;

let cwd = hasRecentWorkspace ? extractCopilotCwdFromWorkspace(workspacePath) : null;
if ((!cwd || !existsSync(cwd)) && hasRecentEvents) {
cwd = extractCopilotCwdFromEvents(eventsPath);
}
if (!cwd || !existsSync(cwd)) continue;

sessions.push({ tool: "copilot", cwd });
}

return sessions;
}

// ── Deduplication ──────────────────────────────────────────────────────────

async function resolveAndDeduplicate(sessions: Session[]): Promise<Repo[]> {
Expand Down Expand Up @@ -496,7 +586,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 +602,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 +620,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 +638,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 +648,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 +659,20 @@ 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
1 change: 1 addition & 0 deletions careful/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-careful.sh"
statusMessage: "Checking for destructive commands..."
sensitive: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
Expand Down
1 change: 1 addition & 0 deletions freeze/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
sensitive: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
Expand Down
1 change: 1 addition & 0 deletions guard/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ hooks:
- type: command
command: "bash ${CLAUDE_SKILL_DIR}/../freeze/bin/check-freeze.sh"
statusMessage: "Checking freeze boundary..."
sensitive: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
Expand Down
1 change: 1 addition & 0 deletions land-and-deploy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ allowed-tools:
- Write
- Glob
- AskUserQuestion
sensitive: true
---
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs -->
Expand Down
Loading