diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..5593dab --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -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" + } + ] +} diff --git a/README.md b/README.md index 2309696..e5860aa 100644 --- a/README.md +++ b/README.md @@ -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 ` - Rename branch preserving stack relationships -- `stack reparent ` - Change the parent of the current branch +- `stack parent ` - Change the parent of the current branch - `stack worktree ` - Create a worktree for a branch ## Configuration @@ -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 diff --git a/cmd/skill.go b/cmd/skill.go index b0e3a07..51d0893 100644 --- a/cmd/skill.go +++ b/cmd/skill.go @@ -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") @@ -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 } @@ -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 { diff --git a/cmd/skill_test.go b/cmd/skill_test.go index 86f3692..8f30bb8 100644 --- a/cmd/skill_test.go +++ b/cmd/skill_test.go @@ -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) { @@ -45,7 +48,7 @@ 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") } @@ -53,7 +56,13 @@ func TestInstallCursor(t *testing.T) { 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) { diff --git a/decision-log.md b/decision-log.md index 138279a..1a573ad 100644 --- a/decision-log.md +++ b/decision-log.md @@ -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. diff --git a/docs/commands.md b/docs/commands.md index 71a9b55..803287b 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -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 diff --git a/plugins/stack/.codex-plugin/plugin.json b/plugins/stack/.codex-plugin/plugin.json new file mode 100644 index 0000000..3cd9f47 --- /dev/null +++ b/plugins/stack/.codex-plugin/plugin.json @@ -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." + } +} diff --git a/plugins/stack/skills/stack/SKILL.md b/plugins/stack/skills/stack/SKILL.md index 4e190af..5f4320e 100644 --- a/plugins/stack/skills/stack/SKILL.md +++ b/plugins/stack/skills/stack/SKILL.md @@ -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 `** - Creates a separate git worktree directory at `~/.stack/worktrees//`. 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 [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 [base]` to create a separate worktree under `~/.stack/worktrees/`. For a fresh branch from `main`, run `stack worktree 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 `** - 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 ` changes it and updates an existing PR base. +- `stack rename ` renames the current branch while preserving stack relationships. -- `stack new ` - 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 ` - Change the parent branch -- `stack rename ` - 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 ` 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 --help` for complete, version-specific flags before unusual or destructive operations.