From 81785805ac98192a1233f71904138949b820486c Mon Sep 17 00:00:00 2001 From: Francis Batac <15894826+francisfuzz@users.noreply.github.com> Date: Wed, 17 Jun 2026 02:35:09 +0000 Subject: [PATCH 1/4] Add AGENTS.md and copilot-instructions.md for AI agent onboarding Add two complementary instruction files so AI coding agents can work effectively in this repository without re-discovering conventions: - AGENTS.md (7K chars): agent-agnostic open standard with full project structure, build/test commands, coding patterns, testing conventions, error handling, key interfaces, and non-obvious gotchas. - .github/copilot-instructions.md (2K chars): concise Copilot-specific instructions under the 4K code review limit, covering the essentials and referencing AGENTS.md for full details. Closes #132 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 36 +++++++++ AGENTS.md | 131 ++++++++++++++++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 AGENTS.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..842bbc7 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,36 @@ +# gh-stack: Copilot Instructions + +A Go CLI extension (`gh stack`) for managing stacked branches and pull requests. Uses Cobra for commands, bubbletea/lipgloss for TUI, and `stretchr/testify` for tests. + +## Build and validate + +```sh +go mod download # install deps +go build ./... # build +go vet ./... # static analysis. Always run before tests. +go test -race -count=1 ./... # tests with race detection +``` + +No Makefile, no code generation, no external linter config. Standard Go toolchain only. + +## Project layout + +- `cmd/`: One Cobra command per file. Each exports `Cmd(cfg *config.Config)` with logic in `run()`. +- `internal/git/`: `Ops` interface (52 methods) wrapping git CLI. `MockOps` for tests. Package-level functions delegate to swappable `ops` variable. +- `internal/github/`: `ClientOps` interface (11 methods) for GitHub API. `MockClient` for tests. +- `internal/config/`: `Config` struct passed to all commands. Holds I/O, colors, and test hooks (`SelectFn`, `ConfirmFn`, `InputFn`, `GitHubClientOverride`). +- `internal/stack/`: Stack file (`.git/gh-stack`, JSON) management with file locking. +- `internal/tui/`: bubbletea views (`stackview`, `modifyview`). + +## Coding conventions + +- Return typed `ExitError` sentinels (codes 1-10 in `cmd/utils.go`) from `RunE`. Never call `os.Exit()` directly. +- Check errors with `errors.As(err, &ExitError{})`. +- Table-driven tests with `t.Run()` subtests. +- Use `config.NewTestConfig()` for test configs with captured I/O. +- Mock git: `restore := git.SetOps(&git.MockOps{...}); defer restore()`. Always defer restore. +- Mock GitHub: `cfg.GitHubClientOverride = &github.MockClient{...}`. +- Mock prompts: set `cfg.SelectFn`, `cfg.ConfirmFn`, or `cfg.InputFn`. +- Load stack files with `stack.Load(dir)` after writing to get correct checksums. + +For full architecture details, see [AGENTS.md](../AGENTS.md) in the repository root. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a38f004 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,131 @@ +# gh-stack: Agent Instructions + +A GitHub CLI (`gh`) extension for managing stacked branches and pull requests. Written in Go, it automates creating branches, keeping them rebased, setting PR base branches, and navigating between stack layers. + +## Build, test, and validate + +```sh +go mod download # install dependencies +go build ./... # build (produces ./gh-stack binary) +go vet ./... # static analysis. Run before tests. +go test -race -count=1 ./... # all tests with race detection +``` + +Always run `go vet` before `go test`. CI runs both on every push/PR across ubuntu, windows, and macOS (`test.yml`). + +There is no Makefile, linter config, or code generation step. The standard Go toolchain is all that's needed. + +### Install locally as a `gh` extension + +```sh +go build -o gh-stack . +gh extension remove stack 2>/dev/null +gh extension install . +``` + +## Project structure + +``` +main.go # entrypoint. Calls cmd.Execute(). +cmd/ # Cobra commands (one file per command + tests) + root.go # registers all subcommands in four groups + utils.go # shared helpers, ExitError types, exit codes +internal/ + git/ # git.Ops interface + defaultOps (exec-based) + gitops.go # Ops interface (52 methods) + mock_ops.go # MockOps. Each method has a corresponding *Fn field. + github/ # github.ClientOps interface + real Client + client_interface.go # ClientOps interface (11 methods) + mock_client.go # MockClient. Uses function-pointer fields for testing. + stack/ # stack file (.git/gh-stack) management, JSON schema, locking + schema.json # JSON Schema for the stack file format + config/ # Config struct (I/O, colors, test overrides) + testing.go # NewTestConfig(). Returns *Config + stdout/stderr pipes. + branch/ # branch naming (Slugify, DateSlug, NextNumberedName) + modify/ # interactive stack modification state machine + pr/ # PR template discovery + tui/ # bubbletea/bubbles/lipgloss terminal UI + stackview/ # interactive stack visualization + modifyview/ # interactive modify session UI + shared/ # shared TUI types +docs/ # Astro + Starlight documentation site +skills/ # AI agent skill definition (SKILL.md) +``` + +### Command groups (registered in `cmd/root.go`) + +| Group | Commands | +|-------|----------| +| Stack management | `init`, `add`, `view`, `checkout`, `modify`, `unstack` | +| Remote operations | `submit`, `sync`, `rebase`, `push`, `link` | +| Navigation | `switch`, `up`, `down`, `top`, `bottom`, `trunk` | +| Utilities | `alias`, `feedback` | + +## Coding patterns + +### Command structure + +Each command lives in its own file (`cmd/.go`) and follows this pattern: + +1. Define an `Options` struct for flags/args. +2. Export a `Cmd(cfg *config.Config) *cobra.Command` constructor. +3. Implement logic in a private `run(cfg, opts, args)` function. +4. The `RunE` field on the command calls `run`. + +### Error handling + +Use typed exit codes defined in `cmd/utils.go`: + +| Code | Sentinel | Meaning | +|------|----------|---------| +| 1 | `ErrSilent` | Error already printed | +| 2 | `ErrNotInStack` | Branch/stack not found | +| 3 | `ErrConflict` | Rebase conflict | +| 4 | `ErrAPIFailure` | GitHub API error | +| 5 | `ErrInvalidArgs` | Invalid arguments or flags | +| 6 | `ErrDisambiguate` | Multiple stacks/remotes, can't auto-select | +| 7 | `ErrRebaseActive` | Rebase already in progress | +| 8 | `ErrLockFailed` | Stack file lock contention | +| 9 | `ErrStacksUnavailable` | Stacked PRs not enabled for repository | +| 10 | `ErrModifyRecovery` | Modify session interrupted | + +Return these from `RunE`. Never call `os.Exit()` directly from commands. Check with `errors.As(err, &ExitError{})`. + +### Testing patterns + +- **Framework:** `stretchr/testify` (`assert`, `require`) for assertions. +- **Table-driven tests** are the norm. See `cmd/utils_test.go` for examples. +- **Config:** Use `config.NewTestConfig()` which returns `(*Config, stdoutReader, stderrReader)` with captured I/O and no-op color functions. +- **Git mocking:** Call `git.SetOps(&git.MockOps{...})`. It returns a restore function. Always `defer restore()` to prevent test pollution. +- **GitHub mocking:** Set `cfg.GitHubClientOverride = &github.MockClient{...}`. +- **Prompt mocking:** Set `cfg.SelectFn`, `cfg.ConfirmFn`, or `cfg.InputFn` on the config to simulate interactive input. +- **Stack file setup:** Use `stack.Load(dir)` after writing a stack file to get correct checksums for `Save`. + +### Key interfaces + +- **`git.Ops`** (`internal/git/gitops.go`): 52 methods wrapping git CLI calls. The production implementation uses `cli/go-gh`'s `client.Command()` via `run()` and `runSilent()` helpers. Package-level functions (e.g., `git.CurrentBranch()`) delegate to a swappable package-level `ops` variable. +- **`github.ClientOps`** (`internal/github/client_interface.go`): 11 methods for GitHub API (PRs, stacks). Injected via `cfg.GitHubClientOverride` in tests. +- **`config.Config`** (`internal/config/config.go`): Central configuration passed to all commands. Holds I/O streams, color functions, and test hook fields (`SelectFn`, `ConfirmFn`, `InputFn`, `TokenForHostFn`, `RepoOverride`). + +### Stack file + +- **Location:** `.git/gh-stack` (JSON format, schema version 1). +- **Schema:** `internal/stack/schema.json`. +- **Locking:** Exclusive file lock at `.git/gh-stack.lock` with 5-second timeout. Errors surface as `LockError`. +- **Staleness:** Concurrent modifications detected via `StaleError`. + +## CI workflows (`.github/workflows/`) + +| Workflow | Trigger | What it does | +|----------|---------|-------------| +| `test.yml` | push to main, PRs | `go vet` + `go test -race -count=1 ./...` on 3 OS matrix | +| `release.yml` | `v*` tags | Cross-platform precompiled binaries via `cli/gh-extension-precompile` | +| `docs.yml` | push to main (docs/**) | Builds Astro/Starlight docs, deploys to GitHub Pages | + +## Non-obvious things + +- The `Queued` field on `BranchRef` is transient (populated from GitHub API, never persisted to the stack JSON file). +- `git.SetOps()` replaces the **package-level** ops variable. Forgetting `defer restore()` in a test will break every subsequent test in the package. +- Interrupt detection: Ctrl+C is caught as `terminal.InterruptErr`, wrapped into an `errInterrupt` sentinel, and printed with a friendly message before a silent exit. +- Rerere: on first rebase conflict, the user is prompted to enable `git rerere`. If declined, a flag file prevents future prompts. `tryAutoResolveRebase()` loops up to 1000 times auto-continuing when rerere resolves conflicts. +- The `.gitignore` ignores `/gh-stack` and `/gh-stack.exe` (the built binary). From 96b0b6ff5d212008eff3465023f6bd1acdcf1906 Mon Sep 17 00:00:00 2001 From: Francis Batac <15894826+francisfuzz@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:13:11 -0700 Subject: [PATCH 2/4] fix: update cmd to generate an executable binary Co-authored-by: Sameen Karim --- .github/copilot-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 842bbc7..4970757 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,7 +6,7 @@ A Go CLI extension (`gh stack`) for managing stacked branches and pull requests. ```sh go mod download # install deps -go build ./... # build +go build -o gh-stack . # build go vet ./... # static analysis. Always run before tests. go test -race -count=1 ./... # tests with race detection ``` From 31ee458d2091775bcf9c88fbcba8059cc9fb0b23 Mon Sep 17 00:00:00 2001 From: Francis Batac <15894826+francisfuzz@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:13:37 -0700 Subject: [PATCH 3/4] fix: update cmd to get the build output as an exec binary Co-authored-by: Sameen Karim --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index a38f004..d6bb388 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ A GitHub CLI (`gh`) extension for managing stacked branches and pull requests. W ```sh go mod download # install dependencies -go build ./... # build (produces ./gh-stack binary) +go build -o gh-stack . # build (produces ./gh-stack binary) go vet ./... # static analysis. Run before tests. go test -race -count=1 ./... # all tests with race detection ``` From 0803d010b58f9926f02536fb2f8e40fafdd5f52f Mon Sep 17 00:00:00 2001 From: Francis Batac <15894826+francisfuzz@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:21:09 +0000 Subject: [PATCH 4/4] Fix build command and errors.As usage per review feedback - go build ./... compiles but does not produce a binary. Changed to go build -o gh-stack . which actually outputs the executable. - errors.As(err, &ExitError{}) panics at runtime because the value type ExitError does not satisfy the error interface (only *ExitError does). Updated to the correct two-line pattern matching cmd/root.go. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 +- AGENTS.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4970757..3c239e4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -25,7 +25,7 @@ No Makefile, no code generation, no external linter config. Standard Go toolchai ## Coding conventions - Return typed `ExitError` sentinels (codes 1-10 in `cmd/utils.go`) from `RunE`. Never call `os.Exit()` directly. -- Check errors with `errors.As(err, &ExitError{})`. +- Check errors with `var exitErr *ExitError; errors.As(err, &exitErr)`. - Table-driven tests with `t.Run()` subtests. - Use `config.NewTestConfig()` for test configs with captured I/O. - Mock git: `restore := git.SetOps(&git.MockOps{...}); defer restore()`. Always defer restore. diff --git a/AGENTS.md b/AGENTS.md index d6bb388..953438b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,12 @@ Use typed exit codes defined in `cmd/utils.go`: | 9 | `ErrStacksUnavailable` | Stacked PRs not enabled for repository | | 10 | `ErrModifyRecovery` | Modify session interrupted | -Return these from `RunE`. Never call `os.Exit()` directly from commands. Check with `errors.As(err, &ExitError{})`. +Return these from `RunE`. Never call `os.Exit()` directly from commands. Check with: + +```go +var exitErr *ExitError +if errors.As(err, &exitErr) { ... } +``` ### Testing patterns