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

View File

@@ -30,7 +30,7 @@ var setupCmd = &cobra.Command{
Long: `Setup integration files for AI editors and coding assistants.
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:
bd setup cursor # Install Cursor IDE integration
@@ -166,6 +166,9 @@ func runRecipe(name string) {
case "factory":
runFactoryRecipe()
return
case "codex":
runCodexRecipe()
return
case "aider":
runAiderRecipe()
return
@@ -287,6 +290,18 @@ func runFactoryRecipe() {
setup.InstallFactory()
}
func runCodexRecipe() {
if setupCheck {
setup.CheckCodex()
return
}
if setupRemove {
setup.RemoveCodex()
return
}
setup.InstallCodex()
}
func runAiderRecipe() {
if setupCheck {
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
import (
"errors"
"fmt"
"io"
"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 factoryIntegration = agentsIntegration{
name: "Factory.ai (Droid)",
setupCommand: "bd setup factory",
readHint: "Factory Droid will automatically read AGENTS.md on session start.",
}
var factoryEnvProvider = defaultFactoryEnv
type factoryEnv = agentsEnv
func defaultFactoryEnv() factoryEnv {
return factoryEnv{
agentsPath: "AGENTS.md",
stdout: os.Stdout,
stderr: os.Stderr,
}
}
var factoryEnvProvider = defaultAgentsEnv
// InstallFactory installs Factory.ai/Droid integration
// InstallFactory installs Factory.ai/Droid integration.
func InstallFactory() {
env := factoryEnvProvider()
if err := installFactory(env); err != nil {
if err := installAgents(env, factoryIntegration); err != nil {
setupExit(1)
}
}
func installFactory(env factoryEnv) error {
_, _ = fmt.Fprintln(env.stdout, "Installing Factory.ai (Droid) integration...")
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
return installAgents(env, factoryIntegration)
}
// CheckFactory checks if Factory.ai integration is installed
// CheckFactory checks if Factory.ai integration is installed.
func CheckFactory() {
env := factoryEnvProvider()
if err := checkFactory(env); err != nil {
if err := checkAgents(env, factoryIntegration); err != nil {
setupExit(1)
}
}
func checkFactory(env factoryEnv) error {
data, err := os.ReadFile(env.agentsPath)
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
return checkAgents(env, factoryIntegration)
}
// RemoveFactory removes Factory.ai integration
// RemoveFactory removes Factory.ai integration.
func RemoveFactory() {
env := factoryEnvProvider()
if err := removeFactory(env); err != nil {
if err := removeAgents(env, factoryIntegration); err != nil {
setupExit(1)
}
}
func removeFactory(env factoryEnv) error {
_, _ = fmt.Fprintln(env.stdout, "Removing Factory.ai (Droid) integration...")
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_
`
return removeAgents(env, factoryIntegration)
}

View File

@@ -30,13 +30,13 @@ More content after`,
Some content
` + factoryBeadsSection + `
` + agentsBeadsSection + `
More content after`,
},
{
name: "append when no markers exist",
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",
@@ -47,7 +47,7 @@ Old content
<!-- END BEADS INTEGRATION -->`,
expected: `# My Project
` + factoryBeadsSection,
` + agentsBeadsSection,
},
}
@@ -122,11 +122,11 @@ func TestCreateNewAgentsFile(t *testing.T) {
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")
}
if !strings.Contains(content, factoryEndMarker) {
if !strings.Contains(content, agentsEndMarker) {
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)
}
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")
}
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) {
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)
}
if err := checkFactory(env); err != nil {
@@ -262,7 +262,7 @@ func TestCheckFactoryScenarios(t *testing.T) {
func TestRemoveFactoryScenarios(t *testing.T) {
t.Run("remove section and keep file", func(t *testing.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 {
t.Fatalf("failed to seed AGENTS.md: %v", err)
}
@@ -273,7 +273,7 @@ func TestRemoveFactoryScenarios(t *testing.T) {
if err != nil {
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")
}
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) {
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)
}
if err := removeFactory(env); err != nil {
@@ -335,7 +335,7 @@ func TestWrapperExitsOnError(t *testing.T) {
t.Run("RemoveFactory", func(t *testing.T) {
cap := stubSetupExit(t)
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)
}
if err := os.Chmod(env.agentsPath, 0o000); err != nil {
@@ -350,36 +350,36 @@ func TestWrapperExitsOnError(t *testing.T) {
}
func TestFactoryBeadsSectionContent(t *testing.T) {
section := factoryBeadsSection
section := agentsBeadsSection
required := []string{"bd create", "bd update", "bd close", "bd ready", "discovered-from"}
for _, token := range required {
if !strings.Contains(section, token) {
t.Errorf("factoryBeadsSection missing %q", token)
t.Errorf("agentsBeadsSection missing %q", token)
}
}
}
func TestFactoryMarkers(t *testing.T) {
if !strings.Contains(factoryBeginMarker, "BEGIN") {
if !strings.Contains(agentsBeginMarker, "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")
}
}
func TestMarkersMatch(t *testing.T) {
if !strings.HasPrefix(factoryBeadsSection, factoryBeginMarker) {
if !strings.HasPrefix(agentsBeadsSection, agentsBeginMarker) {
t.Error("section should start with begin marker")
}
trimmed := strings.TrimSpace(factoryBeadsSection)
if !strings.HasSuffix(trimmed, factoryEndMarker) {
trimmed := strings.TrimSpace(agentsBeadsSection)
if !strings.HasSuffix(trimmed, agentsEndMarker) {
t.Error("section should end with end marker")
}
}
func TestUpdateBeadsSectionPreservesWhitespace(t *testing.T) {
content := "# Header\n\n" + factoryBeadsSection + "\n\n# Footer"
content := "# Header\n\n" + agentsBeadsSection + "\n\n# Footer"
updated := updateBeadsSection(content)
if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") {
t.Error("update should preserve surrounding content")