Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "stackinator",
"interface": {
"displayName": "Stackinator"
},
"plugins": [
{
"name": "stack",
"source": {
"source": "local",
"path": "./plugins/stack"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ See [Commands Reference](docs/commands.md) for full documentation.
- `stack parent` - Show the parent of the current branch
- `stack prune` - Clean up branches with merged PRs
- `stack rename <new-name>` - Rename branch preserving stack relationships
- `stack reparent <new-parent>` - Change the parent of the current branch
- `stack parent <new-parent>` - Change the parent of the current branch
- `stack worktree <branch-name>` - Create a worktree for a branch

## Configuration
Expand Down Expand Up @@ -124,6 +124,13 @@ For Claude Code, run:
/plugin install stack@stackinator
```

For Codex, run:

```bash
codex plugin marketplace add javoire/stackinator
codex plugin add stack@stackinator
```

## Documentation

- [How It Works](docs/how-it-works.md) - Stack tracking and sync algorithm
Expand Down
37 changes: 23 additions & 14 deletions cmd/skill.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ func skillBody() string {
return content
}

// skillDescription returns the description from the embedded SKILL.md frontmatter.
func skillDescription() string {
for _, line := range strings.Split(skillcontent.SkillMD, "\n") {
if description, found := strings.CutPrefix(line, "description:"); found {
return strings.TrimSpace(description)
}
}
return "Manage stacked branches with the stack CLI."
}

func installClaude() error {
fmt.Println("Adding stackinator marketplace...")
addCmd := exec.Command("claude", "plugin", "marketplace", "add", "javoire/stackinator")
Expand All @@ -108,22 +118,21 @@ func installClaude() error {
}

func installCodex() error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("failed to get home directory: %w", err)
}

dir := filepath.Join(home, ".agents", "skills", "stack")
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
fmt.Println("Adding stackinator marketplace...")
addCmd := exec.Command("codex", "plugin", "marketplace", "add", "javoire/stackinator")
addCmd.Stdout = os.Stdout
addCmd.Stderr = os.Stderr
if err := addCmd.Run(); err != nil {
return fmt.Errorf("failed to add marketplace: %w", err)
}

dest := filepath.Join(dir, "SKILL.md")
if err := os.WriteFile(dest, []byte(skillcontent.SkillMD), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", dest, err)
fmt.Println("Installing stack skill...")
installCmd := exec.Command("codex", "plugin", "add", "stack@stackinator")
installCmd.Stdout = os.Stdout
installCmd.Stderr = os.Stderr
if err := installCmd.Run(); err != nil {
return fmt.Errorf("failed to install skill: %w", err)
}

fmt.Printf("Wrote %s\n", dest)
return nil
}

Expand All @@ -139,7 +148,7 @@ func installCursor() error {
}

body := skillBody()
mdc := "---\ndescription: Manage stacked branches with the stack CLI. Covers branch creation, navigation, syncing, and PR management.\nalwaysApply: true\n---\n\n" + body
mdc := fmt.Sprintf("---\ndescription: %s\nalwaysApply: true\n---\n\n%s", skillDescription(), body)

dest := filepath.Join(dir, "stack.mdc")
if err := os.WriteFile(dest, []byte(mdc), 0644); err != nil {
Expand Down
25 changes: 17 additions & 8 deletions cmd/skill_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,20 @@ func TestRunSkillInstall_NoToolsFound(t *testing.T) {
}

func TestInstallCodex(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
binDir := t.TempDir()
logPath := filepath.Join(t.TempDir(), "codex.log")
t.Setenv("CODEX_TEST_LOG", logPath)
t.Setenv("PATH", binDir)

codexPath := filepath.Join(binDir, "codex")
require.NoError(t, os.WriteFile(codexPath, []byte("#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"$CODEX_TEST_LOG\"\n"), 0755))

err := installCodex()
require.NoError(t, err)

dest := filepath.Join(home, ".agents", "skills", "stack", "SKILL.md")
content, err := os.ReadFile(dest)
content, err := os.ReadFile(logPath)
require.NoError(t, err)
assert.Contains(t, string(content), "name: stack")
assert.Contains(t, string(content), "## Common Commands")
assert.Equal(t, "plugin marketplace add javoire/stackinator\nplugin add stack@stackinator\n", string(content))
}

func TestInstallCursor(t *testing.T) {
Expand All @@ -45,15 +48,21 @@ func TestInstallCursor(t *testing.T) {
require.NoError(t, err)
assert.Contains(t, string(content), "alwaysApply: true")
assert.Contains(t, string(content), "description:")
assert.Contains(t, string(content), "## Common Commands")
assert.Contains(t, string(content), "## Inspect and navigate")
// Should not contain SKILL.md frontmatter
assert.NotContains(t, string(content), "name: stack")
}

func TestSkillBody(t *testing.T) {
body := skillBody()
assert.False(t, strings.HasPrefix(body, "---"))
assert.Contains(t, body, "## Common Commands")
assert.Contains(t, body, "## Inspect and navigate")
}

func TestSkillDescription(t *testing.T) {
description := skillDescription()
assert.Contains(t, description, "Manage stacked Git branches")
assert.NotContains(t, description, "description:")
}

func TestDetectCursor_WithDir(t *testing.T) {
Expand Down
8 changes: 8 additions & 0 deletions decision-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

Architectural and design decisions for Stackinator.

## 2026-08-04 — Distribute the Codex skill through a plugin marketplace

**Decision**: Publish the existing Stackinator skill as a Codex plugin from the Stackinator repository.

**Context**: `stack skill install` copied `SKILL.md` directly into `~/.agents/skills/stack`, while Claude Code installed the same skill from the repository marketplace. Direct copies could drift from the CLI and lacked Codex plugin lifecycle support.

**Resolution**: Add a repository-local Codex marketplace and plugin manifest around the shared `plugins/stack/skills/stack/SKILL.md`. Change the Codex installer to register `javoire/stackinator` and install `stack@stackinator`; keep the existing Claude marketplace and Cursor rule installation.

## 2026-05-20 — Add `--all` flag to `stack sync`

**Decision**: Allow `stack sync --all` to sync the full stack, not just the ancestor chain.
Expand Down
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Install the stack skill for AI coding tools so they know how to use the stack CL
Automatically detects which supported tools are installed and installs for all of them:

- **Claude Code** - via plugin marketplace (`claude` CLI required)
- **Codex** - writes `SKILL.md` to `~/.agents/skills/stack/` (`codex` CLI required)
- **Codex** - installs the plugin from the Stackinator marketplace (`codex` CLI required)
- **Cursor** - writes `stack.mdc` to `~/.cursor/rules/` (`cursor` CLI or `~/.cursor/` directory required)

```bash
Expand Down
30 changes: 30 additions & 0 deletions plugins/stack/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "stack",
"version": "1.0.0",
"description": "Stacked branch management for the stack CLI",
"author": {
"name": "javoire",
"url": "https://github.com/javoire"
},
"homepage": "https://github.com/javoire/stackinator",
"repository": "https://github.com/javoire/stackinator",
"license": "MIT",
"keywords": [
"git",
"github",
"stacked-branches",
"pull-requests"
],
"skills": "./skills/",
"interface": {
"displayName": "Stack",
"shortDescription": "Manage stacked Git branches and pull requests.",
"longDescription": "Create, navigate, synchronize, and clean up stacked Git branches with the Stackinator CLI.",
"developerName": "javoire",
"category": "Productivity",
"capabilities": [
"Interactive"
],
"defaultPrompt": "Help me manage my stacked branches with the stack CLI."
}
}
55 changes: 27 additions & 28 deletions plugins/stack/skills/stack/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,39 @@
---
name: stack
description: Manage stacked branches with the stack CLI. Covers branch creation, navigation, syncing, and PR management.
description: Manage stacked Git branches with the stack CLI. Use when creating or navigating stack branches and worktrees, changing parent relationships, syncing branches and pull-request bases, recovering an interrupted sync, or pruning merged branches.
---

The `stack` CLI manages stacked branches and syncs them to GitHub PRs. Use this for working with dependent branches.
The `stack` CLI stores branch parent relationships in Git config and syncs them with GitHub pull requests.

## Choosing Between `stack worktree` vs `stack new`
## Create branches

- **`stack worktree <branch>`** - Creates a separate git worktree directory at `~/.stack/worktrees/<repo>/<branch>`. Use when:
- Starting a new feature from master
- Want to work on multiple things in parallel without stashing
- The current worktree has uncommitted changes you want to keep
- Use `stack new <branch> [parent]` to create and check out a branch in the current worktree. Without an explicit parent, it uses the current stack branch or the configured base branch.
- Use `stack worktree <branch> [base]` to create a separate worktree under `~/.stack/worktrees/<repo>`. For a fresh branch from `main`, run `stack worktree <branch> main`; without `[base]`, a new branch starts from the current branch.
- Prefer a worktree when working in parallel or preserving changes in the current worktree.

- **`stack new <branch>`** - Creates a branch in the current worktree. Use when:
- Building on top of the current feature branch (stacked PRs)
- Already in a worktree and want to add dependent branches
## Inspect and navigate

## Common Commands
- `stack show` displays the local stack without network access.
- `stack status` includes pull-request state and sync issues; use `--no-pr` for a faster local-only view.
- `stack up` checks out the parent branch. `stack down` checks out a child and prompts when multiple children exist.
- `stack switch [branch]` prints a command for changing to a branch's worktree. Use the installed shell wrapper or `eval "$(stack switch [branch])"` so the current shell changes directory.
- `stack parent` shows the current parent; `stack parent <new-parent>` changes it and updates an existing PR base.
- `stack rename <new-name>` renames the current branch while preserving stack relationships.

- `stack new <branch>` - Create new branch in the stack (use instead of `git checkout -b`)
- `stack status` - Show stack structure with PR status (fetches from GitHub)
- `stack show` - Show local stack structure (fast, no network)
- `stack sync` - Sync branches and update PRs on GitHub
- `stack up` / `stack down` - Navigate up/down the stack
- `stack prune` - Clean up merged branches
- `stack reparent <parent>` - Change the parent branch
- `stack rename <name>` - Rename the current branch
- `stack worktree` - Create a git worktree for parallel work
## Sync

## Workflow
Run `stack sync [remote]` to fetch the base remote, rebase branches bottom-to-top, force-push them to `origin` with lease protection, and update the bases of existing PRs. It does not create missing PRs; use `gh pr create` for those.

1. Start a new feature from master: `stack worktree feature-name`
- Or use `stack new feature-name` if building stacked PRs on a feature branch
2. Make changes and commit
3. Sync to GitHub: `stack sync` (creates/updates PR)
4. Check status: `stack status`
5. After merge: `stack prune` to clean up
- Use `stack sync --dry-run` to preview mutations.
- Use `stack sync --all` to include descendants below the current branch.
- Use `stack sync --cross-worktree` to include branches checked out in other worktrees.
- Use `stack sync <remote>` in fork workflows; the fetch remote otherwise comes from `stack.fetchRemote`, then `upstream`, then `origin`. Branches are always pushed to `origin`.
- After resolving rebase conflicts, run `stack sync --resume`. Run `stack sync --abort` to abandon an interrupted sync and restore saved state.
- Use `--force` only when intentionally bypassing force-with-lease protection.

Run `stack --help` for full documentation.
## Clean up

- `stack prune` deletes local stack branches whose PRs are merged. Preview with `--dry-run`; `--all` also checks non-stack branches.
- `stack worktree --prune` removes worktrees for merged branches. `stack worktree --list` lists worktrees.

Run `stack <command> --help` for complete, version-specific flags before unusual or destructive operations.
Loading