feat(setup): add Codex CLI setup recipe (#1243)

* Add Codex setup recipe

* Sync beads issues (bd-1zo)

---------

Co-authored-by: Amp <amp@example.com>
This commit is contained in:
matt wilkie
2026-01-21 22:50:01 -07:00
committed by GitHub
parent be306b6c66
commit ce622f5688
11 changed files with 537 additions and 1646 deletions

File diff suppressed because one or more lines are too long

View File

@@ -30,7 +30,7 @@ var setupCmd = &cobra.Command{
Long: `Setup integration files for AI editors and coding assistants. Long: `Setup integration files for AI editors and coding assistants.
Recipes define where beads workflow instructions are written. Built-in recipes Recipes define where beads workflow instructions are written. Built-in recipes
include cursor, claude, gemini, aider, factory, windsurf, cody, and kilocode. include cursor, claude, gemini, aider, factory, codex, windsurf, cody, and kilocode.
Examples: Examples:
bd setup cursor # Install Cursor IDE integration bd setup cursor # Install Cursor IDE integration
@@ -166,6 +166,9 @@ func runRecipe(name string) {
case "factory": case "factory":
runFactoryRecipe() runFactoryRecipe()
return return
case "codex":
runCodexRecipe()
return
case "aider": case "aider":
runAiderRecipe() runAiderRecipe()
return return
@@ -287,6 +290,18 @@ func runFactoryRecipe() {
setup.InstallFactory() setup.InstallFactory()
} }
func runCodexRecipe() {
if setupCheck {
setup.CheckCodex()
return
}
if setupRemove {
setup.RemoveCodex()
return
}
setup.InstallCodex()
}
func runAiderRecipe() { func runAiderRecipe() {
if setupCheck { if setupCheck {
setup.CheckAider() setup.CheckAider()

307
cmd/bd/setup/agents.go Normal file
View File

@@ -0,0 +1,307 @@
package setup
import (
"errors"
"fmt"
"io"
"os"
"strings"
)
// AGENTS.md integration markers for beads section
const (
agentsBeginMarker = "<!-- BEGIN BEADS INTEGRATION -->"
agentsEndMarker = "<!-- END BEADS INTEGRATION -->"
)
const agentsBeadsSection = `<!-- BEGIN BEADS INTEGRATION -->
## Issue Tracking with bd (beads)
**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.
### Why bd?
- Dependency-aware: Track blockers and relationships between issues
- Git-friendly: Auto-syncs to JSONL for version control
- Agent-optimized: JSON output, ready work detection, discovered-from links
- Prevents duplicate tracking systems and confusion
### Quick Start
**Check for ready work:**
` + "```bash" + `
bd ready --json
` + "```" + `
**Create new issues:**
` + "```bash" + `
bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json
bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:bd-123 --json
` + "```" + `
**Claim and update:**
` + "```bash" + `
bd update bd-42 --status in_progress --json
bd update bd-42 --priority 1 --json
` + "```" + `
**Complete work:**
` + "```bash" + `
bd close bd-42 --reason "Completed" --json
` + "```" + `
### Issue Types
- ` + "`bug`" + ` - Something broken
- ` + "`feature`" + ` - New functionality
- ` + "`task`" + ` - Work item (tests, docs, refactoring)
- ` + "`epic`" + ` - Large feature with subtasks
- ` + "`chore`" + ` - Maintenance (dependencies, tooling)
### Priorities
- ` + "`0`" + ` - Critical (security, data loss, broken builds)
- ` + "`1`" + ` - High (major features, important bugs)
- ` + "`2`" + ` - Medium (default, nice-to-have)
- ` + "`3`" + ` - Low (polish, optimization)
- ` + "`4`" + ` - Backlog (future ideas)
### Workflow for AI Agents
1. **Check ready work**: ` + "`bd ready`" + ` shows unblocked issues
2. **Claim your task**: ` + "`bd update <id> --status in_progress`" + `
3. **Work on it**: Implement, test, document
4. **Discover new work?** Create linked issue:
- ` + "`bd create \"Found bug\" --description=\"Details about what was found\" -p 1 --deps discovered-from:<parent-id>`" + `
5. **Complete**: ` + "`bd close <id> --reason \"Done\"`" + `
### Auto-Sync
bd automatically syncs with git:
- Exports to ` + "`.beads/issues.jsonl`" + ` after changes (5s debounce)
- Imports from JSONL when newer (e.g., after ` + "`git pull`" + `)
- No manual export/import needed!
### Important Rules
- ✅ Use bd for ALL task tracking
- ✅ Always use ` + "`--json`" + ` flag for programmatic use
- ✅ Link discovered work with ` + "`discovered-from`" + ` dependencies
- ✅ Check ` + "`bd ready`" + ` before asking "what should I work on?"
- ❌ Do NOT create markdown TODO lists
- ❌ Do NOT use external issue trackers
- ❌ Do NOT duplicate tracking systems
For more details, see README.md and docs/QUICKSTART.md.
<!-- END BEADS INTEGRATION -->
`
var (
errAgentsFileMissing = errors.New("agents file not found")
errBeadsSectionMissing = errors.New("beads section missing")
)
type agentsEnv struct {
agentsPath string
stdout io.Writer
stderr io.Writer
}
type agentsIntegration struct {
name string
setupCommand string
readHint string
}
func defaultAgentsEnv() agentsEnv {
return agentsEnv{
agentsPath: "AGENTS.md",
stdout: os.Stdout,
stderr: os.Stderr,
}
}
func installAgents(env agentsEnv, integration agentsIntegration) error {
_, _ = fmt.Fprintf(env.stdout, "Installing %s integration...\n", integration.name)
var currentContent string
data, err := os.ReadFile(env.agentsPath)
if err == nil {
currentContent = string(data)
} else if !os.IsNotExist(err) {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
return err
}
if currentContent != "" {
if strings.Contains(currentContent, agentsBeginMarker) {
newContent := updateBeadsSection(currentContent)
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Updated existing beads section in AGENTS.md")
} else {
newContent := currentContent + "\n\n" + agentsBeadsSection
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Added beads section to existing AGENTS.md")
}
} else {
newContent := createNewAgentsFile()
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Created new AGENTS.md with beads integration")
}
_, _ = fmt.Fprintf(env.stdout, "\n✓ %s integration installed\n", integration.name)
_, _ = fmt.Fprintf(env.stdout, " File: %s\n", env.agentsPath)
if integration.readHint != "" {
_, _ = fmt.Fprintf(env.stdout, "\n%s\n", integration.readHint)
}
_, _ = fmt.Fprintln(env.stdout, "No additional configuration needed!")
return nil
}
func checkAgents(env agentsEnv, integration agentsIntegration) error {
data, err := os.ReadFile(env.agentsPath)
if os.IsNotExist(err) {
_, _ = fmt.Fprintln(env.stdout, "✗ AGENTS.md not found")
_, _ = fmt.Fprintf(env.stdout, " Run: %s\n", integration.setupCommand)
return errAgentsFileMissing
} else if err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
return err
}
content := string(data)
if strings.Contains(content, agentsBeginMarker) {
_, _ = fmt.Fprintf(env.stdout, "✓ %s integration installed: %s\n", integration.name, env.agentsPath)
_, _ = fmt.Fprintln(env.stdout, " Beads section found in AGENTS.md")
return nil
}
_, _ = fmt.Fprintln(env.stdout, "⚠ AGENTS.md exists but no beads section found")
_, _ = fmt.Fprintf(env.stdout, " Run: %s (to add beads section)\n", integration.setupCommand)
return errBeadsSectionMissing
}
func removeAgents(env agentsEnv, integration agentsIntegration) error {
_, _ = fmt.Fprintf(env.stdout, "Removing %s integration...\n", integration.name)
data, err := os.ReadFile(env.agentsPath)
if os.IsNotExist(err) {
_, _ = fmt.Fprintln(env.stdout, "No AGENTS.md file found")
return nil
} else if err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
return err
}
content := string(data)
if !strings.Contains(content, agentsBeginMarker) {
_, _ = fmt.Fprintln(env.stdout, "No beads section found in AGENTS.md")
return nil
}
newContent := removeBeadsSection(content)
trimmed := strings.TrimSpace(newContent)
if trimmed == "" {
if err := os.Remove(env.agentsPath); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to remove %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintf(env.stdout, "✓ Removed %s (file was empty after removing beads section)\n", env.agentsPath)
return nil
}
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Removed beads section from AGENTS.md")
return nil
}
// updateBeadsSection replaces the beads section in existing content
func updateBeadsSection(content string) string {
start := strings.Index(content, agentsBeginMarker)
end := strings.Index(content, agentsEndMarker)
if start == -1 || end == -1 || start > end {
// Markers not found or invalid, append instead
return content + "\n\n" + agentsBeadsSection
}
// Replace section between markers (including end marker line)
endOfEndMarker := end + len(agentsEndMarker)
// Find the next newline after end marker
nextNewline := strings.Index(content[endOfEndMarker:], "\n")
if nextNewline != -1 {
endOfEndMarker += nextNewline + 1
}
return content[:start] + agentsBeadsSection + content[endOfEndMarker:]
}
// removeBeadsSection removes the beads section from content
func removeBeadsSection(content string) string {
start := strings.Index(content, agentsBeginMarker)
end := strings.Index(content, agentsEndMarker)
if start == -1 || end == -1 || start > end {
return content
}
// Find the next newline after end marker
endOfEndMarker := end + len(agentsEndMarker)
nextNewline := strings.Index(content[endOfEndMarker:], "\n")
if nextNewline != -1 {
endOfEndMarker += nextNewline + 1
}
// Also remove leading blank lines before the section
trimStart := start
for trimStart > 0 && (content[trimStart-1] == '\n' || content[trimStart-1] == '\r') {
trimStart--
}
return content[:trimStart] + content[endOfEndMarker:]
}
// createNewAgentsFile creates a new AGENTS.md with a basic template
func createNewAgentsFile() string {
return `# Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
` + agentsBeadsSection + `
## Build & Test
_Add your build and test commands here_
` + "```bash" + `
# Example:
# npm install
# npm test
` + "```" + `
## Architecture Overview
_Add a brief overview of your project architecture_
## Conventions & Patterns
_Add your project-specific conventions here_
`
}

45
cmd/bd/setup/codex.go Normal file
View File

@@ -0,0 +1,45 @@
package setup
var codexIntegration = agentsIntegration{
name: "Codex CLI",
setupCommand: "bd setup codex",
readHint: "Codex reads AGENTS.md at the start of each run or session. Restart Codex if it is already running.",
}
var codexEnvProvider = defaultAgentsEnv
// InstallCodex installs Codex integration.
func InstallCodex() {
env := codexEnvProvider()
if err := installCodex(env); err != nil {
setupExit(1)
}
}
func installCodex(env agentsEnv) error {
return installAgents(env, codexIntegration)
}
// CheckCodex checks if Codex integration is installed.
func CheckCodex() {
env := codexEnvProvider()
if err := checkCodex(env); err != nil {
setupExit(1)
}
}
func checkCodex(env agentsEnv) error {
return checkAgents(env, codexIntegration)
}
// RemoveCodex removes Codex integration.
func RemoveCodex() {
env := codexEnvProvider()
if err := removeCodex(env); err != nil {
setupExit(1)
}
}
func removeCodex(env agentsEnv) error {
return removeAgents(env, codexIntegration)
}

View File

@@ -0,0 +1,36 @@
package setup
import (
"strings"
"testing"
)
func stubCodexEnvProvider(t *testing.T, env agentsEnv) {
t.Helper()
orig := codexEnvProvider
codexEnvProvider = func() agentsEnv {
return env
}
t.Cleanup(func() { codexEnvProvider = orig })
}
func TestInstallCodexCreatesNewFile(t *testing.T) {
env, stdout, _ := newFactoryTestEnv(t)
if err := installCodex(env); err != nil {
t.Fatalf("installCodex returned error: %v", err)
}
if !strings.Contains(stdout.String(), "Codex CLI integration installed") {
t.Error("expected Codex install success message")
}
}
func TestCheckCodexMissingFile(t *testing.T) {
env, stdout, _ := newFactoryTestEnv(t)
err := checkCodex(env)
if err == nil {
t.Fatal("expected error for missing AGENTS.md")
}
if !strings.Contains(stdout.String(), "bd setup codex") {
t.Error("expected setup guidance for codex")
}
}

View File

@@ -1,325 +1,47 @@
package setup package setup
import ( var factoryIntegration = agentsIntegration{
"errors" name: "Factory.ai (Droid)",
"fmt" setupCommand: "bd setup factory",
"io" readHint: "Factory Droid will automatically read AGENTS.md on session start.",
"os"
"strings"
)
// Factory/Droid integration markers for AGENTS.md
const (
factoryBeginMarker = "<!-- BEGIN BEADS INTEGRATION -->"
factoryEndMarker = "<!-- END BEADS INTEGRATION -->"
)
const factoryBeadsSection = `<!-- BEGIN BEADS INTEGRATION -->
## Issue Tracking with bd (beads)
**IMPORTANT**: This project uses **bd (beads)** for ALL issue tracking. Do NOT use markdown TODOs, task lists, or other tracking methods.
### Why bd?
- Dependency-aware: Track blockers and relationships between issues
- Git-friendly: Auto-syncs to JSONL for version control
- Agent-optimized: JSON output, ready work detection, discovered-from links
- Prevents duplicate tracking systems and confusion
### Quick Start
**Check for ready work:**
` + "```bash" + `
bd ready --json
` + "```" + `
**Create new issues:**
` + "```bash" + `
bd create "Issue title" --description="Detailed context" -t bug|feature|task -p 0-4 --json
bd create "Issue title" --description="What this issue is about" -p 1 --deps discovered-from:bd-123 --json
` + "```" + `
**Claim and update:**
` + "```bash" + `
bd update bd-42 --status in_progress --json
bd update bd-42 --priority 1 --json
` + "```" + `
**Complete work:**
` + "```bash" + `
bd close bd-42 --reason "Completed" --json
` + "```" + `
### Issue Types
- ` + "`bug`" + ` - Something broken
- ` + "`feature`" + ` - New functionality
- ` + "`task`" + ` - Work item (tests, docs, refactoring)
- ` + "`epic`" + ` - Large feature with subtasks
- ` + "`chore`" + ` - Maintenance (dependencies, tooling)
### Priorities
- ` + "`0`" + ` - Critical (security, data loss, broken builds)
- ` + "`1`" + ` - High (major features, important bugs)
- ` + "`2`" + ` - Medium (default, nice-to-have)
- ` + "`3`" + ` - Low (polish, optimization)
- ` + "`4`" + ` - Backlog (future ideas)
### Workflow for AI Agents
1. **Check ready work**: ` + "`bd ready`" + ` shows unblocked issues
2. **Claim your task**: ` + "`bd update <id> --status in_progress`" + `
3. **Work on it**: Implement, test, document
4. **Discover new work?** Create linked issue:
- ` + "`bd create \"Found bug\" --description=\"Details about what was found\" -p 1 --deps discovered-from:<parent-id>`" + `
5. **Complete**: ` + "`bd close <id> --reason \"Done\"`" + `
### Auto-Sync
bd automatically syncs with git:
- Exports to ` + "`.beads/issues.jsonl`" + ` after changes (5s debounce)
- Imports from JSONL when newer (e.g., after ` + "`git pull`" + `)
- No manual export/import needed!
### Important Rules
- ✅ Use bd for ALL task tracking
- ✅ Always use ` + "`--json`" + ` flag for programmatic use
- ✅ Link discovered work with ` + "`discovered-from`" + ` dependencies
- ✅ Check ` + "`bd ready`" + ` before asking "what should I work on?"
- ❌ Do NOT create markdown TODO lists
- ❌ Do NOT use external issue trackers
- ❌ Do NOT duplicate tracking systems
For more details, see README.md and docs/QUICKSTART.md.
<!-- END BEADS INTEGRATION -->
`
var (
errAgentsFileMissing = errors.New("agents file not found")
errBeadsSectionMissing = errors.New("beads section missing")
)
type factoryEnv struct {
agentsPath string
stdout io.Writer
stderr io.Writer
} }
var factoryEnvProvider = defaultFactoryEnv type factoryEnv = agentsEnv
func defaultFactoryEnv() factoryEnv { var factoryEnvProvider = defaultAgentsEnv
return factoryEnv{
agentsPath: "AGENTS.md",
stdout: os.Stdout,
stderr: os.Stderr,
}
}
// InstallFactory installs Factory.ai/Droid integration // InstallFactory installs Factory.ai/Droid integration.
func InstallFactory() { func InstallFactory() {
env := factoryEnvProvider() env := factoryEnvProvider()
if err := installFactory(env); err != nil { if err := installAgents(env, factoryIntegration); err != nil {
setupExit(1) setupExit(1)
} }
} }
func installFactory(env factoryEnv) error { func installFactory(env factoryEnv) error {
_, _ = fmt.Fprintln(env.stdout, "Installing Factory.ai (Droid) integration...") return installAgents(env, factoryIntegration)
var currentContent string
data, err := os.ReadFile(env.agentsPath)
if err == nil {
currentContent = string(data)
} else if !os.IsNotExist(err) {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
return err
}
if currentContent != "" {
if strings.Contains(currentContent, factoryBeginMarker) {
newContent := updateBeadsSection(currentContent)
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Updated existing beads section in AGENTS.md")
} else {
newContent := currentContent + "\n\n" + factoryBeadsSection
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Added beads section to existing AGENTS.md")
}
} else {
newContent := createNewAgentsFile()
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Created new AGENTS.md with beads integration")
}
_, _ = fmt.Fprintln(env.stdout, "\n✓ Factory.ai (Droid) integration installed")
_, _ = fmt.Fprintf(env.stdout, " File: %s\n", env.agentsPath)
_, _ = fmt.Fprintln(env.stdout, "\nFactory Droid will automatically read AGENTS.md on session start.")
_, _ = fmt.Fprintln(env.stdout, "No additional configuration needed!")
return nil
} }
// CheckFactory checks if Factory.ai integration is installed // CheckFactory checks if Factory.ai integration is installed.
func CheckFactory() { func CheckFactory() {
env := factoryEnvProvider() env := factoryEnvProvider()
if err := checkFactory(env); err != nil { if err := checkAgents(env, factoryIntegration); err != nil {
setupExit(1) setupExit(1)
} }
} }
func checkFactory(env factoryEnv) error { func checkFactory(env factoryEnv) error {
data, err := os.ReadFile(env.agentsPath) return checkAgents(env, factoryIntegration)
if os.IsNotExist(err) {
_, _ = fmt.Fprintln(env.stdout, "✗ AGENTS.md not found")
_, _ = fmt.Fprintln(env.stdout, " Run: bd setup factory")
return errAgentsFileMissing
} else if err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
return err
}
content := string(data)
if strings.Contains(content, factoryBeginMarker) {
_, _ = fmt.Fprintf(env.stdout, "✓ Factory.ai integration installed: %s\n", env.agentsPath)
_, _ = fmt.Fprintln(env.stdout, " Beads section found in AGENTS.md")
return nil
}
_, _ = fmt.Fprintln(env.stdout, "⚠ AGENTS.md exists but no beads section found")
_, _ = fmt.Fprintln(env.stdout, " Run: bd setup factory (to add beads section)")
return errBeadsSectionMissing
} }
// RemoveFactory removes Factory.ai integration // RemoveFactory removes Factory.ai integration.
func RemoveFactory() { func RemoveFactory() {
env := factoryEnvProvider() env := factoryEnvProvider()
if err := removeFactory(env); err != nil { if err := removeAgents(env, factoryIntegration); err != nil {
setupExit(1) setupExit(1)
} }
} }
func removeFactory(env factoryEnv) error { func removeFactory(env factoryEnv) error {
_, _ = fmt.Fprintln(env.stdout, "Removing Factory.ai (Droid) integration...") return removeAgents(env, factoryIntegration)
data, err := os.ReadFile(env.agentsPath)
if os.IsNotExist(err) {
_, _ = fmt.Fprintln(env.stdout, "No AGENTS.md file found")
return nil
} else if err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err)
return err
}
content := string(data)
if !strings.Contains(content, factoryBeginMarker) {
_, _ = fmt.Fprintln(env.stdout, "No beads section found in AGENTS.md")
return nil
}
newContent := removeBeadsSection(content)
trimmed := strings.TrimSpace(newContent)
if trimmed == "" {
if err := os.Remove(env.agentsPath); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: failed to remove %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintf(env.stdout, "✓ Removed %s (file was empty after removing beads section)\n", env.agentsPath)
return nil
}
if err := atomicWriteFile(env.agentsPath, []byte(newContent)); err != nil {
_, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err)
return err
}
_, _ = fmt.Fprintln(env.stdout, "✓ Removed beads section from AGENTS.md")
return nil
}
// updateBeadsSection replaces the beads section in existing content
func updateBeadsSection(content string) string {
start := strings.Index(content, factoryBeginMarker)
end := strings.Index(content, factoryEndMarker)
if start == -1 || end == -1 || start > end {
// Markers not found or invalid, append instead
return content + "\n\n" + factoryBeadsSection
}
// Replace section between markers (including end marker line)
endOfEndMarker := end + len(factoryEndMarker)
// Find the next newline after end marker
nextNewline := strings.Index(content[endOfEndMarker:], "\n")
if nextNewline != -1 {
endOfEndMarker += nextNewline + 1
}
return content[:start] + factoryBeadsSection + content[endOfEndMarker:]
}
// removeBeadsSection removes the beads section from content
func removeBeadsSection(content string) string {
start := strings.Index(content, factoryBeginMarker)
end := strings.Index(content, factoryEndMarker)
if start == -1 || end == -1 || start > end {
return content
}
// Find the next newline after end marker
endOfEndMarker := end + len(factoryEndMarker)
nextNewline := strings.Index(content[endOfEndMarker:], "\n")
if nextNewline != -1 {
endOfEndMarker += nextNewline + 1
}
// Also remove leading blank lines before the section
trimStart := start
for trimStart > 0 && (content[trimStart-1] == '\n' || content[trimStart-1] == '\r') {
trimStart--
}
return content[:trimStart] + content[endOfEndMarker:]
}
// createNewAgentsFile creates a new AGENTS.md with a basic template
func createNewAgentsFile() string {
return `# Project Instructions for AI Agents
This file provides instructions and context for AI coding agents working on this project.
` + factoryBeadsSection + `
## Build & Test
_Add your build and test commands here_
` + "```bash" + `
# Example:
# npm install
# npm test
` + "```" + `
## Architecture Overview
_Add a brief overview of your project architecture_
## Conventions & Patterns
_Add your project-specific conventions here_
`
} }

View File

@@ -30,13 +30,13 @@ More content after`,
Some content Some content
` + factoryBeadsSection + ` ` + agentsBeadsSection + `
More content after`, More content after`,
}, },
{ {
name: "append when no markers exist", name: "append when no markers exist",
content: "# My Project\n\nSome content", content: "# My Project\n\nSome content",
expected: "# My Project\n\nSome content\n\n" + factoryBeadsSection, expected: "# My Project\n\nSome content\n\n" + agentsBeadsSection,
}, },
{ {
name: "handle section at end of file", name: "handle section at end of file",
@@ -47,7 +47,7 @@ Old content
<!-- END BEADS INTEGRATION -->`, <!-- END BEADS INTEGRATION -->`,
expected: `# My Project expected: `# My Project
` + factoryBeadsSection, ` + agentsBeadsSection,
}, },
} }
@@ -122,11 +122,11 @@ func TestCreateNewAgentsFile(t *testing.T) {
t.Error("Missing header in new agents file") t.Error("Missing header in new agents file")
} }
if !strings.Contains(content, factoryBeginMarker) { if !strings.Contains(content, agentsBeginMarker) {
t.Error("Missing begin marker in new agents file") t.Error("Missing begin marker in new agents file")
} }
if !strings.Contains(content, factoryEndMarker) { if !strings.Contains(content, agentsEndMarker) {
t.Error("Missing end marker in new agents file") t.Error("Missing end marker in new agents file")
} }
@@ -170,7 +170,7 @@ func TestInstallFactoryCreatesNewFile(t *testing.T) {
t.Fatalf("failed to read AGENTS.md: %v", err) t.Fatalf("failed to read AGENTS.md: %v", err)
} }
content := string(data) content := string(data)
if !strings.Contains(content, factoryBeginMarker) || !strings.Contains(content, factoryEndMarker) { if !strings.Contains(content, agentsBeginMarker) || !strings.Contains(content, agentsEndMarker) {
t.Fatal("missing factory markers in new file") t.Fatal("missing factory markers in new file")
} }
if !strings.Contains(stdout.String(), "Factory.ai (Droid) integration installed") { if !strings.Contains(stdout.String(), "Factory.ai (Droid) integration installed") {
@@ -247,7 +247,7 @@ func TestCheckFactoryScenarios(t *testing.T) {
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
env, stdout, _ := newFactoryTestEnv(t) env, stdout, _ := newFactoryTestEnv(t)
if err := os.WriteFile(env.agentsPath, []byte(factoryBeadsSection), 0644); err != nil { if err := os.WriteFile(env.agentsPath, []byte(agentsBeadsSection), 0644); err != nil {
t.Fatalf("failed to seed file: %v", err) t.Fatalf("failed to seed file: %v", err)
} }
if err := checkFactory(env); err != nil { if err := checkFactory(env); err != nil {
@@ -262,7 +262,7 @@ func TestCheckFactoryScenarios(t *testing.T) {
func TestRemoveFactoryScenarios(t *testing.T) { func TestRemoveFactoryScenarios(t *testing.T) {
t.Run("remove section and keep file", func(t *testing.T) { t.Run("remove section and keep file", func(t *testing.T) {
env, stdout, _ := newFactoryTestEnv(t) env, stdout, _ := newFactoryTestEnv(t)
content := "# Top\n\n" + factoryBeadsSection + "\n\n# Bottom" content := "# Top\n\n" + agentsBeadsSection + "\n\n# Bottom"
if err := os.WriteFile(env.agentsPath, []byte(content), 0644); err != nil { if err := os.WriteFile(env.agentsPath, []byte(content), 0644); err != nil {
t.Fatalf("failed to seed AGENTS.md: %v", err) t.Fatalf("failed to seed AGENTS.md: %v", err)
} }
@@ -273,7 +273,7 @@ func TestRemoveFactoryScenarios(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to read AGENTS.md: %v", err) t.Fatalf("failed to read AGENTS.md: %v", err)
} }
if strings.Contains(string(data), factoryBeginMarker) { if strings.Contains(string(data), agentsBeginMarker) {
t.Error("beads section should be removed") t.Error("beads section should be removed")
} }
if !strings.Contains(stdout.String(), "Removed beads section") { if !strings.Contains(stdout.String(), "Removed beads section") {
@@ -283,7 +283,7 @@ func TestRemoveFactoryScenarios(t *testing.T) {
t.Run("delete file when only beads", func(t *testing.T) { t.Run("delete file when only beads", func(t *testing.T) {
env, stdout, _ := newFactoryTestEnv(t) env, stdout, _ := newFactoryTestEnv(t)
if err := os.WriteFile(env.agentsPath, []byte(factoryBeadsSection), 0644); err != nil { if err := os.WriteFile(env.agentsPath, []byte(agentsBeadsSection), 0644); err != nil {
t.Fatalf("failed to seed AGENTS.md: %v", err) t.Fatalf("failed to seed AGENTS.md: %v", err)
} }
if err := removeFactory(env); err != nil { if err := removeFactory(env); err != nil {
@@ -335,7 +335,7 @@ func TestWrapperExitsOnError(t *testing.T) {
t.Run("RemoveFactory", func(t *testing.T) { t.Run("RemoveFactory", func(t *testing.T) {
cap := stubSetupExit(t) cap := stubSetupExit(t)
env := factoryEnv{agentsPath: filepath.Join(t.TempDir(), "AGENTS.md"), stdout: &bytes.Buffer{}, stderr: &bytes.Buffer{}} env := factoryEnv{agentsPath: filepath.Join(t.TempDir(), "AGENTS.md"), stdout: &bytes.Buffer{}, stderr: &bytes.Buffer{}}
if err := os.WriteFile(env.agentsPath, []byte(factoryBeadsSection), 0644); err != nil { if err := os.WriteFile(env.agentsPath, []byte(agentsBeadsSection), 0644); err != nil {
t.Fatalf("failed to seed file: %v", err) t.Fatalf("failed to seed file: %v", err)
} }
if err := os.Chmod(env.agentsPath, 0o000); err != nil { if err := os.Chmod(env.agentsPath, 0o000); err != nil {
@@ -350,36 +350,36 @@ func TestWrapperExitsOnError(t *testing.T) {
} }
func TestFactoryBeadsSectionContent(t *testing.T) { func TestFactoryBeadsSectionContent(t *testing.T) {
section := factoryBeadsSection section := agentsBeadsSection
required := []string{"bd create", "bd update", "bd close", "bd ready", "discovered-from"} required := []string{"bd create", "bd update", "bd close", "bd ready", "discovered-from"}
for _, token := range required { for _, token := range required {
if !strings.Contains(section, token) { if !strings.Contains(section, token) {
t.Errorf("factoryBeadsSection missing %q", token) t.Errorf("agentsBeadsSection missing %q", token)
} }
} }
} }
func TestFactoryMarkers(t *testing.T) { func TestFactoryMarkers(t *testing.T) {
if !strings.Contains(factoryBeginMarker, "BEGIN") { if !strings.Contains(agentsBeginMarker, "BEGIN") {
t.Error("begin marker should mention BEGIN") t.Error("begin marker should mention BEGIN")
} }
if !strings.Contains(factoryEndMarker, "END") { if !strings.Contains(agentsEndMarker, "END") {
t.Error("end marker should mention END") t.Error("end marker should mention END")
} }
} }
func TestMarkersMatch(t *testing.T) { func TestMarkersMatch(t *testing.T) {
if !strings.HasPrefix(factoryBeadsSection, factoryBeginMarker) { if !strings.HasPrefix(agentsBeadsSection, agentsBeginMarker) {
t.Error("section should start with begin marker") t.Error("section should start with begin marker")
} }
trimmed := strings.TrimSpace(factoryBeadsSection) trimmed := strings.TrimSpace(agentsBeadsSection)
if !strings.HasSuffix(trimmed, factoryEndMarker) { if !strings.HasSuffix(trimmed, agentsEndMarker) {
t.Error("section should end with end marker") t.Error("section should end with end marker")
} }
} }
func TestUpdateBeadsSectionPreservesWhitespace(t *testing.T) { func TestUpdateBeadsSectionPreservesWhitespace(t *testing.T) {
content := "# Header\n\n" + factoryBeadsSection + "\n\n# Footer" content := "# Header\n\n" + agentsBeadsSection + "\n\n# Footer"
updated := updateBeadsSection(content) updated := updateBeadsSection(content)
if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") { if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") {
t.Error("update should preserve surrounding content") t.Error("update should preserve surrounding content")

View File

@@ -766,18 +766,21 @@ bd sync # Force immediate sync, bypass debounce
```bash ```bash
# Setup editor integration (choose based on your editor) # Setup editor integration (choose based on your editor)
bd setup factory # Factory.ai Droid - creates/updates AGENTS.md (universal standard) bd setup factory # Factory.ai Droid - creates/updates AGENTS.md (universal standard)
bd setup codex # Codex CLI - creates/updates AGENTS.md
bd setup claude # Claude Code - installs SessionStart/PreCompact hooks bd setup claude # Claude Code - installs SessionStart/PreCompact hooks
bd setup cursor # Cursor IDE - creates .cursor/rules/beads.mdc bd setup cursor # Cursor IDE - creates .cursor/rules/beads.mdc
bd setup aider # Aider - creates .aider.conf.yml bd setup aider # Aider - creates .aider.conf.yml
# Check if integration is installed # Check if integration is installed
bd setup factory --check bd setup factory --check
bd setup codex --check
bd setup claude --check bd setup claude --check
bd setup cursor --check bd setup cursor --check
bd setup aider --check bd setup aider --check
# Remove integration # Remove integration
bd setup factory --remove bd setup factory --remove
bd setup codex --remove
bd setup claude --remove bd setup claude --remove
bd setup cursor --remove bd setup cursor --remove
bd setup aider --remove bd setup aider --remove
@@ -792,6 +795,7 @@ bd setup claude --stealth # Use stealth mode (flush only, no git operations)
**What each setup does:** **What each setup does:**
- **Factory.ai** (`bd setup factory`): Creates or updates AGENTS.md with beads workflow instructions (works with multiple AI tools using the AGENTS.md standard) - **Factory.ai** (`bd setup factory`): Creates or updates AGENTS.md with beads workflow instructions (works with multiple AI tools using the AGENTS.md standard)
- **Codex CLI** (`bd setup codex`): Creates or updates AGENTS.md with beads workflow instructions for Codex
- **Claude Code** (`bd setup claude`): Adds hooks to Claude Code's settings.json that run `bd prime` on SessionStart and PreCompact events - **Claude Code** (`bd setup claude`): Adds hooks to Claude Code's settings.json that run `bd prime` on SessionStart and PreCompact events
- **Cursor** (`bd setup cursor`): Creates `.cursor/rules/beads.mdc` with workflow instructions - **Cursor** (`bd setup cursor`): Creates `.cursor/rules/beads.mdc` with workflow instructions
- **Aider** (`bd setup aider`): Creates `.aider.conf.yml` with bd workflow instructions - **Aider** (`bd setup aider`): Creates `.aider.conf.yml` with bd workflow instructions

View File

@@ -189,6 +189,7 @@ bd init --quiet
bd setup claude # Claude Code - installs SessionStart/PreCompact hooks bd setup claude # Claude Code - installs SessionStart/PreCompact hooks
bd setup cursor # Cursor IDE - creates .cursor/rules/beads.mdc bd setup cursor # Cursor IDE - creates .cursor/rules/beads.mdc
bd setup aider # Aider - creates .aider.conf.yml bd setup aider # Aider - creates .aider.conf.yml
bd setup codex # Codex CLI - creates/updates AGENTS.md
``` ```
**How it works:** **How it works:**
@@ -208,6 +209,7 @@ bd setup aider # Aider - creates .aider.conf.yml
bd setup claude --check # Check Claude Code integration bd setup claude --check # Check Claude Code integration
bd setup cursor --check # Check Cursor integration bd setup cursor --check # Check Cursor integration
bd setup aider --check # Check Aider integration bd setup aider --check # Check Aider integration
bd setup codex --check # Check Codex integration
``` ```
### Claude Code Plugin (Optional) ### Claude Code Plugin (Optional)

View File

@@ -18,6 +18,7 @@ The `bd setup` command uses a **recipe-based architecture** to configure beads i
| `claude` | `~/.claude/settings.json` | SessionStart/PreCompact hooks | | `claude` | `~/.claude/settings.json` | SessionStart/PreCompact hooks |
| `gemini` | `~/.gemini/settings.json` | SessionStart/PreCompress hooks | | `gemini` | `~/.gemini/settings.json` | SessionStart/PreCompress hooks |
| `factory` | `AGENTS.md` | Marked section | | `factory` | `AGENTS.md` | Marked section |
| `codex` | `AGENTS.md` | Marked section |
| `aider` | `.aider.conf.yml` + `.aider/` | Multi-file config | | `aider` | `.aider.conf.yml` + `.aider/` | Multi-file config |
## Quick Start ## Quick Start
@@ -33,6 +34,7 @@ bd setup kilocode # Kilo Code
bd setup claude # Claude Code bd setup claude # Claude Code
bd setup gemini # Gemini CLI bd setup gemini # Gemini CLI
bd setup factory # Factory.ai Droid bd setup factory # Factory.ai Droid
bd setup codex # Codex CLI
bd setup aider # Aider bd setup aider # Aider
# Verify installation # Verify installation
@@ -139,6 +141,24 @@ If you already have an AGENTS.md file with other project instructions:
You can use multiple integrations simultaneously - they complement each other! You can use multiple integrations simultaneously - they complement each other!
## Codex CLI
Codex reads `AGENTS.md` instructions at the start of each run/session. Adding the beads section is enough to get Codex and beads working together.
### Installation
```bash
bd setup codex
```
### What Gets Installed
Creates or updates `AGENTS.md` with the beads integration section (same markers as Factory.ai).
### Notes
- Restart Codex if it's already running to pick up the new instructions.
## Claude Code ## Claude Code
Claude Code integration uses hooks to automatically inject beads workflow context at session start and before context compaction. Claude Code integration uses hooks to automatically inject beads workflow context at session start and before context compaction.

View File

@@ -84,6 +84,12 @@ var BuiltinRecipes = map[string]Recipe{
Type: TypeSection, Type: TypeSection,
Description: "Factory Droid AGENTS.md section", Description: "Factory Droid AGENTS.md section",
}, },
"codex": {
Name: "Codex CLI",
Path: "AGENTS.md",
Type: TypeSection,
Description: "Codex CLI AGENTS.md section",
},
"aider": { "aider": {
Name: "Aider", Name: "Aider",
Type: TypeMultiFile, Type: TypeMultiFile,