From 928a657d84b4755b02b97549241b63d3d2767aa8 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 10:10:57 -0700 Subject: [PATCH 01/10] docs: add central Dolt server setup guide for macOS Add new section to DOLT-BACKEND.md covering: - Embedded vs central server comparison table - Why 'brew services start dolt' silently ignores config - Step-by-step custom LaunchAgent setup with plist - Environment variables to point beads at central server - Service management (stop/restart/logs) Also fix default port from 3306 to 3307 in the server configuration table to match DefaultSQLPort in code. Closes #2323 Amp-Thread-ID: https://ampcode.com/threads/T-019ced53-6c5c-71de-8092-b834c879a68a Co-authored-by: Amp --- docs/DOLT-BACKEND.md | 127 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/docs/DOLT-BACKEND.md b/docs/DOLT-BACKEND.md index 120acc2a0d..97f7502ecc 100644 --- a/docs/DOLT-BACKEND.md +++ b/docs/DOLT-BACKEND.md @@ -74,7 +74,7 @@ dolt: |---------------------|---------|-------------| | `BEADS_DOLT_SERVER_MODE` | `1` | Enable/disable server mode (`1`/`0`) | | `BEADS_DOLT_SERVER_HOST` | `127.0.0.1` | Server bind address | -| `BEADS_DOLT_SERVER_PORT` | `3306` | Server port (MySQL protocol) | +| `BEADS_DOLT_SERVER_PORT` | `3307` | Server port (MySQL protocol) | | `BEADS_DOLT_SERVER_USER` | `root` | MySQL username | | `BEADS_DOLT_SERVER_PASS` | (empty) | MySQL password | | `BEADS_DOLT_SHARED_SERVER` | (empty) | Shared server mode: `1` or `true` to enable | @@ -112,6 +112,131 @@ Shared server state lives in `~/.beads/shared-server/` and uses port 3308 by def (avoiding conflict with Gas Town on 3307). Each project's data remains isolated in its own database (named by project prefix). See [DOLT.md](DOLT.md) for details. +## Central Dolt Server (macOS) + +If you plan to use Gas Town or manage multiple beads projects from a single +machine, you can run a central persistent Dolt server instead of per-project +embedded instances. + +### Embedded vs Central Server + +| | Embedded (default) | Central Server | +|---|---|---| +| **Setup** | Zero-config — `bd init` handles everything | One-time server setup required | +| **Data location** | `.beads/dolt/` per project | Central directory (e.g. `/opt/homebrew/var/dolt`) | +| **Concurrency** | Single writer per project | Multi-writer via MySQL protocol | +| **Use case** | Solo development, single project | Gas Town, multiple projects, multiple agents | + +For single-project solo use, **embedded mode is recommended** — it requires no +setup. Switch to a central server when you need Gas Town or concurrent access. + +### Why Not `brew services start dolt`? + +After installing Dolt with `brew install dolt`, the natural next step is +`brew services start dolt`. However, this **silently ignores your config file**. + +The Homebrew formula runs `dolt sql-server` without the `--config` flag. Dolt +does **not** auto-discover `config.yaml` from its working directory — the config +file must be passed explicitly via `--config `. Any edits to +`/opt/homebrew/var/dolt/config.yaml` (port, host, etc.) have no effect when +started through `brew services`. + +### Setup with a Custom LaunchAgent + +Instead of `brew services`, create a custom LaunchAgent that passes the config +file explicitly. + +**1. Install Dolt and initialize its data directory:** + +```bash +brew install dolt + +# Initialize the dolt data directory (if not already done) +cd /opt/homebrew/var/dolt && dolt init +``` + +**2. Configure Dolt for port 3307:** + +```yaml +# /opt/homebrew/var/dolt/config.yaml +log_level: info + +listener: + host: 127.0.0.1 + port: 3307 + max_connections: 100 + +behavior: + autocommit: true +``` + +**3. Create the LaunchAgent plist:** + +```bash +cat > ~/Library/LaunchAgents/com.local.dolt-server.plist << 'EOF' + + + + + Label + com.local.dolt-server + ProgramArguments + + /opt/homebrew/opt/dolt/bin/dolt + sql-server + --config + config.yaml + + WorkingDirectory + /opt/homebrew/var/dolt + RunAtLoad + + KeepAlive + + StandardOutPath + /opt/homebrew/var/log/dolt.log + StandardErrorPath + /opt/homebrew/var/log/dolt.error.log + + +EOF +``` + +**4. Load the service:** + +```bash +launchctl load ~/Library/LaunchAgents/com.local.dolt-server.plist + +# Verify it's running +mysql -h 127.0.0.1 -P 3307 -u root -e "SELECT 1" +``` + +**5. Point beads at the central server** — add to `~/.zshrc` (or `~/.bashrc`): + +```bash +export BEADS_DOLT_SERVER_HOST="127.0.0.1" +export BEADS_DOLT_SERVER_PORT="3307" +export BEADS_DOLT_SERVER_MODE=1 +``` + +Now `bd init` in any project will connect to the central server instead of +spawning an embedded instance. + +### Managing the Service + +```bash +# Stop +launchctl unload ~/Library/LaunchAgents/com.local.dolt-server.plist + +# Restart (unload + load) +launchctl unload ~/Library/LaunchAgents/com.local.dolt-server.plist +launchctl load ~/Library/LaunchAgents/com.local.dolt-server.plist + +# Check logs +tail -f /opt/homebrew/var/log/dolt.log +``` + ## Sync Modes Dolt supports multiple sync strategies: From f78b1b244b6033e1cca2489d67b613fc6d61bf69 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 10:28:45 -0700 Subject: [PATCH 02/10] =?UTF-8?q?feat:=20prime=20SSOT=20tasks=201-3=20?= =?UTF-8?q?=E2=80=94=20shared=20render=20API,=20template=20profiles,=20pre?= =?UTF-8?q?fix-aware=20parser=20(#2139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement the foundational pieces for making bd prime the single source of truth for agent instructions: Task 1 - Shared render API (internal/templates/agents/render.go): - Profile type with Full and Minimal constants - RenderSection(profile) generates sections with marker metadata - ParseMarker() extracts profile/hash from begin markers - CurrentHash(profile) for staleness detection - 8-char SHA-256 hash for deterministic freshness Task 2 - Template splitting: - New beads-section-minimal.md for hook-enabled agents (Claude, Gemini) - Minimal profile: pointer to bd prime + quick reference + landing protocol - Existing beads-section.md preserved as full profile body Task 3 - Parser refactor (cmd/bd/setup/agents.go): - Prefix-based marker matching via containsBeadsMarker/findBeginMarker - Legacy markers (no metadata) detected and upgradable - New markers: - agentsIntegration gains profile field (defaults to full) - updateBeadsSectionWithProfile for profile-aware updates - createNewAgentsFileWithProfile for new file creation Backward compatible: EmbeddedBeadsSection/EmbeddedDefault still work. Legacy markers are auto-upgraded on next bd setup run. Amp-Thread-ID: https://ampcode.com/threads/T-019ced5e-0acd-7158-93ad-0695c6d02580 Co-authored-by: Amp --- cmd/bd/setup/agents.go | 62 +++++- cmd/bd/setup/agents_marker_test.go | 172 ++++++++++++++ cmd/bd/setup/factory_test.go | 10 +- .../agents/defaults/beads-section-minimal.md | 44 ++++ internal/templates/agents/render.go | 101 +++++++++ internal/templates/agents/render_test.go | 210 ++++++++++++++++++ 6 files changed, 584 insertions(+), 15 deletions(-) create mode 100644 cmd/bd/setup/agents_marker_test.go create mode 100644 internal/templates/agents/defaults/beads-section-minimal.md create mode 100644 internal/templates/agents/render.go create mode 100644 internal/templates/agents/render_test.go diff --git a/cmd/bd/setup/agents.go b/cmd/bd/setup/agents.go index ecc05d9eed..6d7528448a 100644 --- a/cmd/bd/setup/agents.go +++ b/cmd/bd/setup/agents.go @@ -10,6 +10,9 @@ import ( "github.com/steveyegge/beads/internal/templates/agents" ) +// readFileBytesImpl is used in tests; avoids import cycle. +var readFileBytesImpl = os.ReadFile + // AGENTS.md integration markers for beads section const ( agentsBeginMarker = "" @@ -34,6 +37,7 @@ type agentsIntegration struct { setupCommand string readHint string docsURL string + profile agents.Profile // "full" or "minimal"; empty defaults to "full" } func defaultAgentsEnv() agentsEnv { @@ -44,10 +48,25 @@ func defaultAgentsEnv() agentsEnv { } } +// containsBeadsMarker returns true if content contains a BEGIN BEADS INTEGRATION marker +// (either legacy or new format with metadata). +func containsBeadsMarker(content string) bool { + return strings.Contains(content, "\nContent\n\n" + if !containsBeadsMarker(content) { + t.Error("should detect legacy marker") + } +} + +func TestContainsBeadsMarkerNew(t *testing.T) { + content := "# Header\n\n\nContent\n\n" + if !containsBeadsMarker(content) { + t.Error("should detect new-format marker") + } +} + +func TestContainsBeadsMarkerAbsent(t *testing.T) { + content := "# Header\n\nNo beads here\n" + if containsBeadsMarker(content) { + t.Error("should not detect marker when absent") + } +} + +func TestUpdateBeadsSectionLegacyToNew(t *testing.T) { + // Legacy marker should be replaced with new-format marker + legacy := `# Header + + +Old content here + + +# Footer` + + updated := updateBeadsSection(legacy) + + // Should have new-format marker with profile and hash + if !strings.Contains(updated, "profile:full") { + t.Error("updated section should use new-format marker with profile:full") + } + if !strings.Contains(updated, "hash:") { + t.Error("updated section should use new-format marker with hash") + } + + // Should preserve surrounding content + if !strings.Contains(updated, "# Header") { + t.Error("should preserve header") + } + if !strings.Contains(updated, "# Footer") { + t.Error("should preserve footer") + } + + // Old content should be replaced + if strings.Contains(updated, "Old content here") { + t.Error("old content should be replaced") + } +} + +func TestUpdateBeadsSectionNewFormatUpdate(t *testing.T) { + // New-format marker should also be replaceable + content := `# Header + + +Stale content + + +# Footer` + + updated := updateBeadsSection(content) + + if strings.Contains(updated, "oldoldhash") { + t.Error("old hash should be replaced") + } + if strings.Contains(updated, "Stale content") { + t.Error("stale content should be replaced") + } + if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") { + t.Error("surrounding content should be preserved") + } +} + +func TestRemoveBeadsSectionLegacy(t *testing.T) { + content := "Header\n\nContent\n\nFooter" + result := removeBeadsSection(content) + if strings.Contains(result, "BEGIN BEADS") { + t.Error("markers should be removed") + } + if !strings.Contains(result, "Header") || !strings.Contains(result, "Footer") { + t.Error("surrounding content should be preserved") + } +} + +func TestRemoveBeadsSectionNewFormat(t *testing.T) { + content := "Header\n\nContent\n\nFooter" + result := removeBeadsSection(content) + if strings.Contains(result, "BEGIN BEADS") { + t.Error("markers should be removed") + } + if !strings.Contains(result, "Header") || !strings.Contains(result, "Footer") { + t.Error("surrounding content should be preserved") + } +} + +func TestUpdateBeadsSectionWithProfile(t *testing.T) { + // Test with explicit profile parameter + content := `# Header + + +Old content + + +# Footer` + + updated := updateBeadsSectionWithProfile(content, agents.ProfileMinimal) + if !strings.Contains(updated, "profile:minimal") { + t.Error("should use minimal profile") + } + if !strings.Contains(updated, "# Header") || !strings.Contains(updated, "# Footer") { + t.Error("should preserve surrounding content") + } +} + +func TestInstallAgentsWithProfileCreatesNew(t *testing.T) { + env, _, _ := newFactoryTestEnv(t) + integration := agentsIntegration{ + name: "TestAgent", + setupCommand: "bd setup testagent", + profile: agents.ProfileMinimal, + } + if err := installAgents(env, integration); err != nil { + t.Fatalf("installAgents returned error: %v", err) + } + data, err := readFileBytes(env.agentsPath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + content := string(data) + if !strings.Contains(content, "profile:minimal") { + t.Error("new file should use minimal profile from integration") + } +} + +func TestInstallAgentsDefaultsToFullProfile(t *testing.T) { + env, _, _ := newFactoryTestEnv(t) + integration := agentsIntegration{ + name: "TestAgent", + setupCommand: "bd setup testagent", + // no profile set — should default to full + } + if err := installAgents(env, integration); err != nil { + t.Fatalf("installAgents returned error: %v", err) + } + data, err := readFileBytes(env.agentsPath) + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + content := string(data) + if !strings.Contains(content, "profile:full") { + t.Error("default profile should be full") + } +} + +// readFileBytes is a test helper to read file content +func readFileBytes(path string) ([]byte, error) { + return readFileBytesImpl(path) +} diff --git a/cmd/bd/setup/factory_test.go b/cmd/bd/setup/factory_test.go index f11f9589f2..e62041f11a 100644 --- a/cmd/bd/setup/factory_test.go +++ b/cmd/bd/setup/factory_test.go @@ -12,7 +12,7 @@ import ( ) func TestUpdateBeadsSection(t *testing.T) { - beadsSection := agents.EmbeddedBeadsSection() + beadsSection := agents.RenderSection(agents.ProfileFull) tests := []struct { name string @@ -135,7 +135,7 @@ func TestCreateNewAgentsFile(t *testing.T) { t.Error("Missing header in new agents file") } - if !strings.Contains(content, agentsBeginMarker) { + if !containsBeadsMarker(content) { t.Error("Missing begin marker in new agents file") } @@ -143,6 +143,10 @@ func TestCreateNewAgentsFile(t *testing.T) { t.Error("Missing end marker in new agents file") } + if !strings.Contains(content, "profile:full") { + t.Error("Missing profile metadata in new agents file") + } + if !strings.Contains(content, "## Build & Test") { t.Error("Missing Build & Test section") } @@ -183,7 +187,7 @@ func TestInstallFactoryCreatesNewFile(t *testing.T) { t.Fatalf("failed to read AGENTS.md: %v", err) } content := string(data) - if !strings.Contains(content, agentsBeginMarker) || !strings.Contains(content, agentsEndMarker) { + if !containsBeadsMarker(content) || !strings.Contains(content, agentsEndMarker) { t.Fatal("missing factory markers in new file") } if !strings.Contains(stdout.String(), "Factory.ai (Droid) integration installed") { diff --git a/internal/templates/agents/defaults/beads-section-minimal.md b/internal/templates/agents/defaults/beads-section-minimal.md new file mode 100644 index 0000000000..d20fd25865 --- /dev/null +++ b/internal/templates/agents/defaults/beads-section-minimal.md @@ -0,0 +1,44 @@ +## Beads Issue Tracker + +This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. + +### Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work +bd close # Complete work +``` + +### Rules + +- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists +- Run `bd prime` for detailed command reference and session close protocol +- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files + +## Landing the Plane (Session Completion) + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd dolt push + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds diff --git a/internal/templates/agents/render.go b/internal/templates/agents/render.go new file mode 100644 index 0000000000..958aed9139 --- /dev/null +++ b/internal/templates/agents/render.go @@ -0,0 +1,101 @@ +package agents + +import ( + "crypto/sha256" + _ "embed" + "fmt" + "strings" +) + +// Profile identifies which template variant to render. +type Profile string + +const ( + // ProfileFull is the command-heavy profile for hookless agents (Codex, Factory, Mux, etc.). + ProfileFull Profile = "full" + // ProfileMinimal is the pointer-only profile for hook-enabled agents (Claude, Gemini). + ProfileMinimal Profile = "minimal" +) + +//go:embed defaults/beads-section-minimal.md +var beadsSectionMinimal string + +// SectionMeta holds metadata parsed from a BEGIN BEADS INTEGRATION marker. +type SectionMeta struct { + Profile Profile + Hash string +} + +// RenderSection returns the beads integration section for the given profile, +// wrapped in markers that include profile and hash metadata for freshness detection. +func RenderSection(profile Profile) string { + body := templateBody(profile) + hash := computeHash(body) + beginMarker := fmt.Sprintf("", profile, hash) + return beginMarker + "\n" + body + "\n\n" +} + +// CurrentHash returns the hash of the current template body for a profile. +// Callers can compare this against a parsed marker's hash to detect staleness. +func CurrentHash(profile Profile) string { + return computeHash(templateBody(profile)) +} + +// ParseMarker parses a BEGIN BEADS INTEGRATION marker line and returns its metadata. +// Returns nil if the line is not a valid begin marker. +// Supports both legacy (no metadata) and new (profile + hash) formats. +func ParseMarker(line string) *SectionMeta { + line = strings.TrimSpace(line) + if !strings.HasPrefix(line, "" + inner := strings.TrimPrefix(line, "") + inner = strings.TrimSpace(inner) + + if inner == "" { + // Legacy format: + return meta + } + + // Parse key:value pairs + for _, part := range strings.Fields(inner) { + k, v, ok := strings.Cut(part, ":") + if !ok { + continue + } + switch k { + case "profile": + meta.Profile = Profile(v) + case "hash": + meta.Hash = v + } + } + + return meta +} + +// templateBody returns the raw body content (without markers) for a profile. +func templateBody(profile Profile) string { + switch profile { + case ProfileMinimal: + return strings.TrimRight(beadsSectionMinimal, "\n") + default: + // Full profile uses the same body as the legacy beads-section.md + // Strip the existing markers from the embedded content + body := strings.TrimRight(beadsSection, "\n") + body = strings.TrimPrefix(body, "\n") + body = strings.TrimSuffix(body, "\n") + return body + } +} + +// computeHash returns the first 8 hex chars of the SHA-256 of the body. +func computeHash(body string) string { + h := sha256.Sum256([]byte(body)) + return fmt.Sprintf("%x", h[:4]) +} diff --git a/internal/templates/agents/render_test.go b/internal/templates/agents/render_test.go new file mode 100644 index 0000000000..ce0060f0c6 --- /dev/null +++ b/internal/templates/agents/render_test.go @@ -0,0 +1,210 @@ +package agents + +import ( + "strings" + "testing" +) + +func TestProfileConstants(t *testing.T) { + if ProfileFull != "full" { + t.Errorf("ProfileFull = %q, want %q", ProfileFull, "full") + } + if ProfileMinimal != "minimal" { + t.Errorf("ProfileMinimal = %q, want %q", ProfileMinimal, "minimal") + } +} + +func TestRenderSectionFull(t *testing.T) { + section := RenderSection(ProfileFull) + if section == "" { + t.Fatal("RenderSection(full) returned empty string") + } + + // Must start with begin marker containing profile and hash metadata + if !strings.HasPrefix(section, "") { + t.Error("section should end with end marker") + } + + // Full profile must contain command references + for _, want := range []string{"bd create", "bd update", "bd close", "bd ready", "discovered-from"} { + if !strings.Contains(section, want) { + t.Errorf("full profile missing %q", want) + } + } +} + +func TestRenderSectionMinimal(t *testing.T) { + section := RenderSection(ProfileMinimal) + if section == "" { + t.Fatal("RenderSection(minimal) returned empty string") + } + + // Must start with begin marker containing profile and hash metadata + if !strings.HasPrefix(section, "", + want: SectionMeta{Profile: ProfileFull, Hash: "a1b2c3d4"}, + wantOK: true, + }, + { + name: "new format minimal profile", + line: "", + want: SectionMeta{Profile: ProfileMinimal, Hash: "deadbeef"}, + wantOK: true, + }, + { + name: "legacy format (no metadata)", + line: "", + want: SectionMeta{Profile: "", Hash: ""}, + wantOK: true, + }, + { + name: "not a marker", + line: "## Some heading", + wantOK: false, + }, + { + name: "end marker (not begin)", + line: "", + wantOK: false, + }, + { + name: "empty string", + line: "", + wantOK: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseMarker(tt.line) + if tt.wantOK { + if got == nil { + t.Fatal("ParseMarker returned nil, expected non-nil") + } + if got.Profile != tt.want.Profile { + t.Errorf("Profile = %q, want %q", got.Profile, tt.want.Profile) + } + if got.Hash != tt.want.Hash { + t.Errorf("Hash = %q, want %q", got.Hash, tt.want.Hash) + } + } else { + if got != nil { + t.Errorf("ParseMarker returned %+v, expected nil", got) + } + } + }) + } +} + +func TestIsStaleFreshness(t *testing.T) { + // Render a section and parse its marker — should not be stale + section := RenderSection(ProfileFull) + firstLine := strings.SplitN(section, "\n", 2)[0] + meta := ParseMarker(firstLine) + if meta == nil { + t.Fatal("ParseMarker returned nil for rendered section") + } + + currentHash := CurrentHash(ProfileFull) + if meta.Hash != currentHash { + t.Errorf("rendered hash %q != current hash %q (should be fresh)", meta.Hash, currentHash) + } + + // Legacy marker (no hash) should be considered stale + legacyMeta := ParseMarker("") + if legacyMeta == nil { + t.Fatal("ParseMarker returned nil for legacy marker") + } + if legacyMeta.Hash == currentHash { + t.Error("legacy marker with empty hash should not match current hash") + } +} From d115e930d1566c258f59d1841bf9f1897f0bd73f Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 10:29:39 -0700 Subject: [PATCH 03/10] feat: explicit profile:full on all AGENTS.md integrations (#2139, task 4) Set profile: agents.ProfileFull explicitly on codex, factory, mux (all three layers), and opencode integrations. This makes the profile intent clear and prepares for adding minimal-profile integrations. Amp-Thread-ID: https://ampcode.com/threads/T-019ced5e-0acd-7158-93ad-0695c6d02580 Co-authored-by: Amp --- cmd/bd/setup/codex.go | 3 +++ cmd/bd/setup/factory.go | 3 +++ cmd/bd/setup/mux.go | 5 +++++ cmd/bd/setup/opencode.go | 3 +++ 4 files changed, 14 insertions(+) diff --git a/cmd/bd/setup/codex.go b/cmd/bd/setup/codex.go index 8d06c512f6..843a73462a 100644 --- a/cmd/bd/setup/codex.go +++ b/cmd/bd/setup/codex.go @@ -1,9 +1,12 @@ package setup +import "github.com/steveyegge/beads/internal/templates/agents" + 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.", + profile: agents.ProfileFull, } var codexEnvProvider = defaultAgentsEnv diff --git a/cmd/bd/setup/factory.go b/cmd/bd/setup/factory.go index d45a41a13b..c84e2c75e4 100644 --- a/cmd/bd/setup/factory.go +++ b/cmd/bd/setup/factory.go @@ -1,9 +1,12 @@ package setup +import "github.com/steveyegge/beads/internal/templates/agents" + var factoryIntegration = agentsIntegration{ name: "Factory.ai (Droid)", setupCommand: "bd setup factory", readHint: "Factory Droid will automatically read AGENTS.md on session start.", + profile: agents.ProfileFull, } type factoryEnv = agentsEnv diff --git a/cmd/bd/setup/mux.go b/cmd/bd/setup/mux.go index 77fe7ef670..c2718bd68e 100644 --- a/cmd/bd/setup/mux.go +++ b/cmd/bd/setup/mux.go @@ -6,6 +6,8 @@ import ( "os" "path/filepath" "strings" + + "github.com/steveyegge/beads/internal/templates/agents" ) const ( @@ -54,6 +56,7 @@ var ( setupCommand: "bd setup mux", readHint: "Mux reads AGENTS.md in workspace and global contexts. Restart the workspace session if it is already running.", docsURL: muxAgentInstructionsURL, + profile: agents.ProfileFull, } muxProjectIntegration = agentsIntegration{ @@ -61,6 +64,7 @@ var ( setupCommand: "bd setup mux --project", readHint: "Mux also supports layered workspace instructions via .mux/AGENTS.md.", docsURL: muxAgentInstructionsURL, + profile: agents.ProfileFull, } muxGlobalIntegration = agentsIntegration{ @@ -68,6 +72,7 @@ var ( setupCommand: "bd setup mux --global", readHint: "Mux global defaults can be stored in ~/.mux/AGENTS.md.", docsURL: muxAgentInstructionsURL, + profile: agents.ProfileFull, } muxEnvProvider = defaultAgentsEnv diff --git a/cmd/bd/setup/opencode.go b/cmd/bd/setup/opencode.go index f91ac05a28..324861e287 100644 --- a/cmd/bd/setup/opencode.go +++ b/cmd/bd/setup/opencode.go @@ -1,9 +1,12 @@ package setup +import "github.com/steveyegge/beads/internal/templates/agents" + var opencodeIntegration = agentsIntegration{ name: "OpenCode", setupCommand: "bd setup opencode", readHint: "OpenCode reads AGENTS.md at the start of each session. Restart OpenCode if it is already running.", + profile: agents.ProfileFull, } var opencodeEnvProvider = defaultAgentsEnv From 93b85295d422295c880b166818925187520e12a2 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 10:30:52 -0700 Subject: [PATCH 04/10] feat: Claude and Gemini setup install minimal AGENTS.md section (#2139, tasks 5-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When running 'bd setup claude --project' or 'bd setup gemini --project', after installing hooks in settings.json, also install a minimal-profile beads section in AGENTS.md. This provides a quick reference pointer to 'bd prime' for the full workflow, while hooks handle the heavy lifting. On removal, the beads section is cleaned up from AGENTS.md as well. The minimal profile contains: quick reference commands, pointer to bd prime, and the landing-the-plane session close protocol — without the full issue types, priorities, and sync details that hook-enabled agents get dynamically from bd prime. Amp-Thread-ID: https://ampcode.com/threads/T-019ced5e-0acd-7158-93ad-0695c6d02580 Co-authored-by: Amp --- cmd/bd/setup/claude.go | 38 ++++++++++++++++++++++++++++++++++++++ cmd/bd/setup/gemini.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/cmd/bd/setup/claude.go b/cmd/bd/setup/claude.go index 166c494c4b..e29b9c9a6a 100644 --- a/cmd/bd/setup/claude.go +++ b/cmd/bd/setup/claude.go @@ -7,6 +7,8 @@ import ( "io" "os" "path/filepath" + + "github.com/steveyegge/beads/internal/templates/agents" ) var ( @@ -127,6 +129,25 @@ func installClaude(env claudeEnv, project bool, stealth bool) error { return err } + // Install minimal beads section in AGENTS.md (Claude reads this on session start). + // Hooks handle the heavy lifting via bd prime; AGENTS.md just needs a pointer. + agentsEnv := agentsEnv{ + agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), + stdout: env.stdout, + stderr: env.stderr, + } + claudeAgentsIntegration := agentsIntegration{ + name: "Claude Code", + setupCommand: "bd setup claude", + profile: agents.ProfileMinimal, + } + if project { + if err := installAgents(agentsEnv, claudeAgentsIntegration); err != nil { + // Non-fatal: hooks are already installed + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) + } + } + _, _ = fmt.Fprintln(env.stdout, "\n✓ Claude Code integration installed") _, _ = fmt.Fprintf(env.stdout, " Settings: %s\n", settingsPath) _, _ = fmt.Fprintln(env.stdout, "\nRestart Claude Code for changes to take effect.") @@ -221,6 +242,23 @@ func removeClaude(env claudeEnv, project bool) error { return err } + // Also remove beads section from AGENTS.md if project removal + if project { + agentsEnv := agentsEnv{ + agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), + stdout: env.stdout, + stderr: env.stderr, + } + claudeAgentsIntegration := agentsIntegration{ + name: "Claude Code", + setupCommand: "bd setup claude", + } + if err := removeAgents(agentsEnv, claudeAgentsIntegration); err != nil { + // Non-fatal + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) + } + } + _, _ = fmt.Fprintln(env.stdout, "✓ Claude hooks removed") return nil } diff --git a/cmd/bd/setup/gemini.go b/cmd/bd/setup/gemini.go index a9123029f1..3a1f71ae39 100644 --- a/cmd/bd/setup/gemini.go +++ b/cmd/bd/setup/gemini.go @@ -7,6 +7,8 @@ import ( "io" "os" "path/filepath" + + "github.com/steveyegge/beads/internal/templates/agents" ) var ( @@ -120,6 +122,25 @@ func installGemini(env geminiEnv, project bool, stealth bool) error { return err } + // Install minimal beads section in AGENTS.md (Gemini reads this on session start). + // Hooks handle the heavy lifting via bd prime; AGENTS.md just needs a pointer. + agentsEnv := agentsEnv{ + agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), + stdout: env.stdout, + stderr: env.stderr, + } + geminiAgentsIntegration := agentsIntegration{ + name: "Gemini CLI", + setupCommand: "bd setup gemini", + profile: agents.ProfileMinimal, + } + if project { + if err := installAgents(agentsEnv, geminiAgentsIntegration); err != nil { + // Non-fatal: hooks are already installed + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) + } + } + _, _ = fmt.Fprintln(env.stdout, "\n✓ Gemini CLI integration installed") _, _ = fmt.Fprintf(env.stdout, " Settings: %s\n", settingsPath) _, _ = fmt.Fprintln(env.stdout, "\nRestart Gemini CLI for changes to take effect.") @@ -215,6 +236,23 @@ func removeGemini(env geminiEnv, project bool) error { return err } + // Also remove beads section from AGENTS.md if project removal + if project { + agentsEnv := agentsEnv{ + agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), + stdout: env.stdout, + stderr: env.stderr, + } + geminiAgentsIntegration := agentsIntegration{ + name: "Gemini CLI", + setupCommand: "bd setup gemini", + } + if err := removeAgents(agentsEnv, geminiAgentsIntegration); err != nil { + // Non-fatal + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) + } + } + _, _ = fmt.Fprintln(env.stdout, "✓ Gemini CLI hooks removed") return nil } From 8e7da25030a82b49d0a8684e947f222c38c59f88 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 10:31:24 -0700 Subject: [PATCH 05/10] refactor: init uses RenderSection with profile metadata (#2139, task 7) Update init_agent.go to use agents.RenderSection(ProfileFull) instead of agents.EmbeddedBeadsSection() when appending beads sections to existing AGENTS.md files. This ensures new installs get the versioned marker format with profile and hash metadata. Amp-Thread-ID: https://ampcode.com/threads/T-019ced5e-0acd-7158-93ad-0695c6d02580 Co-authored-by: Amp --- cmd/bd/init_agent.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/bd/init_agent.go b/cmd/bd/init_agent.go index 92834bbef7..2603eafc92 100644 --- a/cmd/bd/init_agent.go +++ b/cmd/bd/init_agent.go @@ -66,13 +66,13 @@ func updateAgentFile(filename string, verbose bool, templatePath string) error { return nil } - // Append beads section (includes landing-the-plane) + // Append beads section with profile metadata (includes landing-the-plane) newContent := contentStr if !strings.HasSuffix(newContent, "\n") { newContent += "\n" } - newContent += "\n" + agents.EmbeddedBeadsSection() + newContent += "\n" + agents.RenderSection(agents.ProfileFull) // #nosec G306 - markdown needs to be readable if err := os.WriteFile(filename, []byte(newContent), 0644); err != nil { From 809bd43b0e9b178aaba3c769c11565adaf7565a4 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 11:04:13 -0700 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20prime=20SSOT=20tasks=208-12=20?= =?UTF-8?q?=E2=80=94=20init=20upgrade,=20recipe=20cleanup,=20symlink=20saf?= =?UTF-8?q?ety,=20staleness=20detection=20(#2139)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task 8: bd init now upgrades legacy beads sections to versioned format instead of silently skipping them. New files created via EmbeddedDefault() also get versioned markers. Task 9: Replace stale bd sync references with bd dolt push in recipes template. Add bd prime SSOT messaging. Task 10: installAgents resolves symlinks via ResolveForWrite before reading content. Profile precedence: if file already has full profile and minimal is requested, full is preserved to avoid information loss. Task 11: Added tests for staleness detection, profile extraction, profile precedence, symlink safety, and legacy-to-new migration. checkAgents now reports stale/current/missing status. Task 12: Updated SETUP.md with prime SSOT, profiles, and profile precedence documentation. Updated CLI_REFERENCE.md with --check behavior and Gemini integration description. Amp-Thread-ID: https://ampcode.com/threads/T-019ced7a-0c86-708d-930c-ee35d00eda8d Co-authored-by: Amp --- cmd/bd/init_agent.go | 53 ++++++++- cmd/bd/setup/agents.go | 79 +++++++++++-- cmd/bd/setup/agents_marker_test.go | 182 +++++++++++++++++++++++++++++ cmd/bd/setup/factory_test.go | 18 ++- docs/CLI_REFERENCE.md | 11 +- docs/SETUP.md | 21 +++- internal/recipes/template.go | 13 ++- 7 files changed, 354 insertions(+), 23 deletions(-) diff --git a/cmd/bd/init_agent.go b/cmd/bd/init_agent.go index 2603eafc92..c7b6445bfe 100644 --- a/cmd/bd/init_agent.go +++ b/cmd/bd/init_agent.go @@ -25,6 +25,8 @@ func addAgentsInstructions(verbose bool, templatePath string) { } // updateAgentFile creates or updates an agent instructions file with embedded template content. +// When a beads section already exists (legacy or current), it is updated to the latest +// versioned format so that `bd init` never silently locks in stale sections. func updateAgentFile(filename string, verbose bool, templatePath string) error { // Check if file exists //nolint:gosec // G304: filename comes from hardcoded list in addAgentsInstructions @@ -43,6 +45,12 @@ func updateAgentFile(filename string, verbose bool, templatePath string) error { newContent = agents.EmbeddedDefault() } + // Ensure the beads section uses versioned markers even in new files. + // EmbeddedDefault() may contain legacy markers; upgrade them. + if strings.Contains(newContent, "BEGIN BEADS INTEGRATION") && !strings.Contains(newContent, "profile:") { + newContent = upgradeBeadsSection(newContent, agents.ProfileFull) + } + // #nosec G306 - markdown needs to be readable if err := os.WriteFile(filename, []byte(newContent), 0644); err != nil { return fmt.Errorf("failed to create %s: %w", filename, err) @@ -60,8 +68,18 @@ func updateAgentFile(filename string, verbose bool, templatePath string) error { hasBeads := strings.Contains(contentStr, "BEGIN BEADS INTEGRATION") if hasBeads { - if verbose { - fmt.Printf(" %s already has agent instructions\n", filename) + // Update existing section to latest versioned format (upgrades legacy markers) + updated := upgradeBeadsSection(contentStr, agents.ProfileFull) + if updated != contentStr { + // #nosec G306 - markdown needs to be readable + if err := os.WriteFile(filename, []byte(updated), 0644); err != nil { + return fmt.Errorf("failed to update %s: %w", filename, err) + } + if verbose { + fmt.Printf(" %s Updated beads section in %s to latest format\n", ui.RenderPass("✓"), filename) + } + } else if verbose { + fmt.Printf(" %s already has current agent instructions\n", filename) } return nil } @@ -84,6 +102,37 @@ func updateAgentFile(filename string, verbose bool, templatePath string) error { return nil } +// upgradeBeadsSection replaces the beads section markers and content with the +// latest versioned format for the given profile. This handles both legacy +// markers (no metadata) and stale versioned markers (wrong hash). +func upgradeBeadsSection(content string, profile agents.Profile) string { + beginIdx := strings.Index(content, "" + endIdx := strings.Index(content, endMarker) + + if beginIdx == -1 || endIdx == -1 || beginIdx > endIdx { + return content + } + + // Check if already current + firstLine := content[beginIdx:] + if nl := strings.Index(firstLine, "\n"); nl != -1 { + firstLine = firstLine[:nl] + } + meta := agents.ParseMarker(firstLine) + if meta != nil && meta.Hash == agents.CurrentHash(profile) && meta.Profile == profile { + return content // already up to date + } + + // Replace section + endOfEndMarker := endIdx + len(endMarker) + if endOfEndMarker < len(content) && content[endOfEndMarker] == '\n' { + endOfEndMarker++ + } + + return content[:beginIdx] + agents.RenderSection(profile) + content[endOfEndMarker:] +} + // setupClaudeSettings creates or updates .claude/settings.local.json with onboard instruction func setupClaudeSettings(verbose bool) error { claudeDir := ".claude" diff --git a/cmd/bd/setup/agents.go b/cmd/bd/setup/agents.go index 6d7528448a..9f649dcd02 100644 --- a/cmd/bd/setup/agents.go +++ b/cmd/bd/setup/agents.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/steveyegge/beads/internal/templates/agents" + "github.com/steveyegge/beads/internal/utils" ) // readFileBytesImpl is used in tests; avoids import cycle. @@ -22,6 +23,7 @@ const ( var ( errAgentsFileMissing = errors.New("agents file not found") errBeadsSectionMissing = errors.New("beads section missing") + errBeadsSectionStale = errors.New("beads section is stale") ) const muxAgentInstructionsURL = "https://mux.coder.com/AGENTS.md" @@ -66,10 +68,18 @@ func installAgents(env agentsEnv, integration agentsIntegration) error { _, _ = fmt.Fprintf(env.stdout, "Installing %s integration...\n", integration.name) profile := resolveProfile(integration) - beadsSection := agents.RenderSection(profile) + + // Resolve symlinks so that e.g. CLAUDE.md -> AGENTS.md writes to the real target. + // This uses the existing atomicWriteFile path which also calls ResolveForWrite, + // but we need the resolved path here to read the current content from the right place. + resolvedPath, err := utils.ResolveForWrite(env.agentsPath) + if err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: resolve path %s: %v\n", env.agentsPath, err) + return err + } var currentContent string - data, err := os.ReadFile(env.agentsPath) + data, err := os.ReadFile(resolvedPath) if err == nil { currentContent = string(data) } else if !os.IsNotExist(err) { @@ -77,6 +87,19 @@ func installAgents(env agentsEnv, integration agentsIntegration) error { return err } + // Profile precedence: if the file already has a full profile and we're + // requesting minimal, preserve full to avoid information loss (e.g. when + // CLAUDE.md is a symlink to AGENTS.md and both Claude and Codex target it). + if currentContent != "" && containsBeadsMarker(currentContent) { + existingProfile := existingBeadsProfile(currentContent) + if existingProfile == agents.ProfileFull && profile == agents.ProfileMinimal { + _, _ = fmt.Fprintf(env.stdout, " ℹ File already has full profile; preserving (higher-information) content\n") + profile = agents.ProfileFull + } + } + + beadsSection := agents.RenderSection(profile) + if currentContent != "" { if containsBeadsMarker(currentContent) { newContent := updateBeadsSectionWithProfile(currentContent, profile) @@ -126,15 +149,34 @@ func checkAgents(env agentsEnv, integration agentsIntegration) error { } content := string(data) - if containsBeadsMarker(content) { - _, _ = fmt.Fprintf(env.stdout, "✓ %s integration installed: %s\n", integration.name, env.agentsPath) - _, _ = fmt.Fprintln(env.stdout, " Beads section found in AGENTS.md") + if !containsBeadsMarker(content) { + _, _ = 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 + } + + // Section exists — check freshness via profile and hash + profile := resolveProfile(integration) + existingProf := existingBeadsProfile(content) + + // Extract hash from marker + idx := findBeginMarker(content) + line := content[idx:] + if nl := strings.Index(line, "\n"); nl != -1 { + line = line[:nl] + } + meta := agents.ParseMarker(line) + + currentHash := agents.CurrentHash(profile) + if meta != nil && meta.Hash == currentHash && existingProf == profile { + _, _ = fmt.Fprintf(env.stdout, "✓ %s integration installed: %s (current)\n", integration.name, env.agentsPath) 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 + // Stale or legacy section + _, _ = fmt.Fprintf(env.stdout, "⚠ %s integration installed but stale: %s\n", integration.name, env.agentsPath) + _, _ = fmt.Fprintf(env.stdout, " Run: %s (to update)\n", integration.setupCommand) + return errBeadsSectionStale } func removeAgents(env agentsEnv, integration agentsIntegration) error { @@ -229,6 +271,27 @@ func findBeginMarker(content string) int { return strings.Index(content, "\nContent\n\n" + got := existingBeadsProfile(content) + if got != agents.ProfileFull { + t.Errorf("legacy marker should return ProfileFull, got %q", got) + } +} + +func TestExistingBeadsProfileFull(t *testing.T) { + content := "\nContent\n\n" + got := existingBeadsProfile(content) + if got != agents.ProfileFull { + t.Errorf("expected ProfileFull, got %q", got) + } +} + +func TestExistingBeadsProfileMinimal(t *testing.T) { + content := "\nContent\n\n" + got := existingBeadsProfile(content) + if got != agents.ProfileMinimal { + t.Errorf("expected ProfileMinimal, got %q", got) + } +} + +func TestExistingBeadsProfileNoMarker(t *testing.T) { + content := "# Just a file\nNo markers\n" + got := existingBeadsProfile(content) + if got != agents.ProfileFull { + t.Errorf("no marker should default to ProfileFull, got %q", got) + } +} + +func TestCheckAgentsDetectsStale(t *testing.T) { + env, stdout, _ := newFactoryTestEnv(t) + // Write a section with a bogus hash so it's stale + content := "\nOld content\n\n" + if err := os.WriteFile(env.agentsPath, []byte(content), 0644); err != nil { + t.Fatalf("write: %v", err) + } + integration := agentsIntegration{ + name: "TestAgent", + setupCommand: "bd setup testagent", + profile: agents.ProfileFull, + } + err := checkAgents(env, integration) + if !errors.Is(err, errBeadsSectionStale) { + t.Fatalf("expected errBeadsSectionStale, got %v", err) + } + if !strings.Contains(stdout.String(), "stale") { + t.Error("expected stale message in stdout") + } +} + +func TestCheckAgentsCurrent(t *testing.T) { + env, stdout, _ := newFactoryTestEnv(t) + section := agents.RenderSection(agents.ProfileFull) + if err := os.WriteFile(env.agentsPath, []byte(section), 0644); err != nil { + t.Fatalf("write: %v", err) + } + integration := agentsIntegration{ + name: "TestAgent", + setupCommand: "bd setup testagent", + profile: agents.ProfileFull, + } + if err := checkAgents(env, integration); err != nil { + t.Fatalf("expected nil error for current section, got %v", err) + } + if !strings.Contains(stdout.String(), "current") { + t.Error("expected (current) in output") + } +} + +func TestInstallAgentsPreservesFullProfile(t *testing.T) { + // Simulate: file already has full profile, requesting minimal install + env, stdout, _ := newFactoryTestEnv(t) + fullSection := agents.RenderSection(agents.ProfileFull) + if err := os.WriteFile(env.agentsPath, []byte(fullSection), 0644); err != nil { + t.Fatalf("write: %v", err) + } + integration := agentsIntegration{ + name: "MinimalAgent", + setupCommand: "bd setup minimalagent", + profile: agents.ProfileMinimal, + } + if err := installAgents(env, integration); err != nil { + t.Fatalf("installAgents: %v", err) + } + data, err := readFileBytes(env.agentsPath) + if err != nil { + t.Fatalf("read: %v", err) + } + content := string(data) + // Should preserve full profile, not downgrade to minimal + if !strings.Contains(content, "profile:full") { + t.Error("should preserve full profile when minimal requested on file with full") + } + if !strings.Contains(stdout.String(), "preserving") { + t.Error("expected informational message about preserving full profile") + } +} + +func TestInstallAgentsSymlinkSafety(t *testing.T) { + dir := t.TempDir() + realFile := filepath.Join(dir, "AGENTS.md") + linkPath := filepath.Join(dir, "CLAUDE.md") + + // Write full profile content to the real file + fullSection := agents.RenderSection(agents.ProfileFull) + if err := os.WriteFile(realFile, []byte(fullSection), 0644); err != nil { + t.Fatalf("write: %v", err) + } + + // Create symlink + if err := os.Symlink(realFile, linkPath); err != nil { + t.Fatalf("symlink: %v", err) + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + env := agentsEnv{ + agentsPath: linkPath, // install targets the symlink + stdout: stdout, + stderr: stderr, + } + integration := agentsIntegration{ + name: "ClaudeCode", + setupCommand: "bd setup claude", + profile: agents.ProfileMinimal, + } + if err := installAgents(env, integration); err != nil { + t.Fatalf("installAgents via symlink: %v", err) + } + + // Read the real file — should still have full profile + data, err := os.ReadFile(realFile) + if err != nil { + t.Fatalf("read real file: %v", err) + } + if !strings.Contains(string(data), "profile:full") { + t.Error("symlink target should preserve full profile") + } +} + +func TestLegacyToNewMigrationViaInstall(t *testing.T) { + env, _, _ := newFactoryTestEnv(t) + // Seed with legacy markers + legacy := "# Header\n\n\nOld content\n\n\n# Footer" + if err := os.WriteFile(env.agentsPath, []byte(legacy), 0644); err != nil { + t.Fatalf("write: %v", err) + } + integration := agentsIntegration{ + name: "Factory.ai", + setupCommand: "bd setup factory", + profile: agents.ProfileFull, + } + if err := installAgents(env, integration); err != nil { + t.Fatalf("installAgents: %v", err) + } + data, err := readFileBytes(env.agentsPath) + if err != nil { + t.Fatalf("read: %v", err) + } + content := string(data) + // Should now have versioned markers + if !strings.Contains(content, "profile:full") { + t.Error("legacy markers should be upgraded to versioned format") + } + if !strings.Contains(content, "hash:") { + t.Error("upgraded section should contain hash") + } + if strings.Contains(content, "Old content") { + t.Error("old legacy content should be replaced") + } + if !strings.Contains(content, "# Header") || !strings.Contains(content, "# Footer") { + t.Error("surrounding content should be preserved") + } +} diff --git a/cmd/bd/setup/factory_test.go b/cmd/bd/setup/factory_test.go index e62041f11a..59340e0c6a 100644 --- a/cmd/bd/setup/factory_test.go +++ b/cmd/bd/setup/factory_test.go @@ -264,7 +264,8 @@ func TestCheckFactoryScenarios(t *testing.T) { t.Run("success", func(t *testing.T) { env, stdout, _ := newFactoryTestEnv(t) - beadsSection := agents.EmbeddedBeadsSection() + // Use current rendered section (not legacy EmbeddedBeadsSection) so check reports "current" + beadsSection := agents.RenderSection(agents.ProfileFull) if err := os.WriteFile(env.agentsPath, []byte(beadsSection), 0644); err != nil { t.Fatalf("failed to seed file: %v", err) } @@ -275,6 +276,21 @@ func TestCheckFactoryScenarios(t *testing.T) { t.Error("expected success output") } }) + + t.Run("stale legacy section", func(t *testing.T) { + env, stdout, _ := newFactoryTestEnv(t) + beadsSection := agents.EmbeddedBeadsSection() + if err := os.WriteFile(env.agentsPath, []byte(beadsSection), 0644); err != nil { + t.Fatalf("failed to seed file: %v", err) + } + err := checkFactory(env) + if !errors.Is(err, errBeadsSectionStale) { + t.Fatalf("expected errBeadsSectionStale, got %v", err) + } + if !strings.Contains(stdout.String(), "stale") { + t.Error("expected stale output") + } + }) } func TestRemoveFactoryScenarios(t *testing.T) { diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index a7d1f44e98..d9281d53f9 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -893,13 +893,16 @@ bd setup mux --global # Also install ~/.mux/AGENTS.md global layer ``` **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) -- **Codex CLI** (`bd setup codex`): Creates or updates AGENTS.md with beads workflow instructions for Codex -- **Mux** (`bd setup mux`): Creates or updates AGENTS.md with beads workflow instructions for Mux workspaces -- **Claude Code** (`bd setup claude`): Adds hooks to Claude Code's settings.json that run `bd prime` on SessionStart and PreCompact events +- **Factory.ai** (`bd setup factory`): Creates or updates AGENTS.md with beads workflow instructions (full profile — 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 (full profile) +- **Mux** (`bd setup mux`): Creates or updates AGENTS.md with beads workflow instructions for Mux workspaces (full profile) +- **Claude Code** (`bd setup claude`): Adds hooks to Claude Code's settings.json that run `bd prime` on SessionStart and PreCompact events; project mode also adds minimal-profile beads section to AGENTS.md +- **Gemini CLI** (`bd setup gemini`): Adds hooks to Gemini's settings.json that run `bd prime` on SessionStart and PreCompress events; project mode also adds minimal-profile beads section to AGENTS.md - **Cursor** (`bd setup cursor`): Creates `.cursor/rules/beads.mdc` with workflow instructions - **Aider** (`bd setup aider`): Creates `.aider.conf.yml` with bd workflow instructions +**`--check` behavior:** Reports section status as `current` (up to date), `stale` (legacy or hash mismatch — run setup to update), or `missing` (no beads section). Stale and missing return non-zero exit codes. + See also: - [INSTALLING.md](INSTALLING.md#ide-and-editor-integrations) - Installation guide - [AIDER_INTEGRATION.md](AIDER_INTEGRATION.md) - Detailed Aider guide diff --git a/docs/SETUP.md b/docs/SETUP.md index c320095d53..345d54073d 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -7,6 +7,23 @@ The `bd setup` command uses a **recipe-based architecture** to configure beads integration with AI coding tools. Recipes define where workflow instructions are written—built-in recipes handle popular tools, and you can add custom recipes for any tool. +### `bd prime` as SSOT + +`bd prime` is the **single source of truth** for operational workflow commands. The AGENTS.md beads section provides a pointer to `bd prime` for hook-enabled agents (Claude, Gemini) or the full command reference for hookless agents (Factory, Codex, Mux). + +### Profiles + +Each integration uses one of two **profiles** that control how much content is written to AGENTS.md: + +| Profile | Used By | Content | +|---------|---------|---------| +| `full` | Factory, Codex, Mux, OpenCode | Complete command reference, issue types, priorities, workflow | +| `minimal` | Claude Code, Gemini CLI | Pointer to `bd prime`, quick reference only (~60% smaller) | + +Hook-enabled agents (Claude, Gemini) use the `minimal` profile because `bd prime` injects full context at session start. Hookless agents need the `full` profile because AGENTS.md is their only source of instructions. + +**Profile precedence:** If a file already has a `full` profile section and a `minimal` profile tool installs to the same file (e.g., via symlinks), the `full` profile is preserved to avoid information loss. + ### Built-in Recipes | Recipe | Path | Integration Type | @@ -74,7 +91,7 @@ Creates or updates `AGENTS.md` in your project root with: - Auto-sync explanation - Important rules for AI agents -The beads section is wrapped in HTML comments (``) for safe updates. +The beads section is wrapped in HTML comments (``) with metadata for safe updates. The begin marker includes profile and hash metadata (e.g., ``) for freshness detection. Legacy markers without metadata are auto-upgraded on the next install or update. ### AGENTS.md Standard @@ -94,7 +111,7 @@ Using AGENTS.md means one configuration file works across your entire AI tool ec | Flag | Description | |------|-------------| -| `--check` | Check if beads section exists in AGENTS.md | +| `--check` | Check if beads section exists and is current (reports `missing`, `stale`, or `current`) | | `--remove` | Remove beads section from AGENTS.md | ### Examples diff --git a/internal/recipes/template.go b/internal/recipes/template.go index 39389f9727..4d8259abdc 100644 --- a/internal/recipes/template.go +++ b/internal/recipes/template.go @@ -11,20 +11,20 @@ This project uses [Beads (bd)](https://github.com/steveyegge/beads) for issue tr - Track ALL work in bd (never use markdown TODOs or comment-based task lists) - Use ` + "`bd ready`" + ` to find available work - Use ` + "`bd create`" + ` to track new issues/tasks/bugs -- Use ` + "`bd sync`" + ` at end of session to sync with git remote -- Git hooks auto-sync on commit/merge +- Use ` + "`bd dolt push`" + ` at end of session to sync with remote +- Run ` + "`bd prime`" + ` for complete workflow context (SSOT for operational commands) ## Quick Reference ` + "```bash" + ` -bd prime # Load complete workflow context +bd prime # Load complete workflow context (SSOT) bd ready # Show issues ready to work (no blockers) bd list --status=open # List all open issues -bd create --title="..." --type=task # Create new issue +bd create "title" -t task -p 2 # Create new issue bd update --claim # Claim work atomically bd close # Mark complete bd dep add # Add dependency -bd sync # Sync with git remote +bd dolt push # Sync with remote ` + "```" + ` ## Workflow @@ -33,7 +33,7 @@ bd sync # Sync with git remote 2. Claim an issue atomically: ` + "`bd update --claim`" + ` 3. Do the work 4. Mark complete: ` + "`bd close `" + ` -5. Sync: ` + "`bd sync`" + ` (or let git hooks handle it) +5. Push changes: ` + "`bd dolt push`" + ` ## Issue Types @@ -54,6 +54,7 @@ bd sync # Sync with git remote ## Context Loading Run ` + "`bd prime`" + ` to get complete workflow documentation in AI-optimized format. +` + "`bd prime`" + ` is the single source of truth for operational commands and session workflow. For detailed docs: see AGENTS.md, QUICKSTART.md, or run ` + "`bd --help`" + ` ` From 0e00e8db8e9bfdda160b70d49923373992a0f3d9 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 11:17:00 -0700 Subject: [PATCH 07/10] fix: sync agents.md.tmpl beads section with beads-section.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update agents.md.tmpl to match canonical beads-section.md content: - 'Version-controlled: Built on Dolt with cell-level merge' → 'Git-friendly: Dolt-powered version control with native sync' - JSONL-based sync description → Dolt push/pull sync model - Legacy markers → versioned markers (profile:full hash:d4f96305) This ensures EmbeddedDefault() output has a beads section body identical to beads-section.md, so upgradeBeadsSection() in bd init is a no-op for freshly created files. Ref: #2139 Amp-Thread-ID: https://ampcode.com/threads/T-019ced8e-4a10-7404-b1f6-83cc78d1996c Co-authored-by: Amp --- internal/templates/agents/defaults/agents.md.tmpl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/templates/agents/defaults/agents.md.tmpl b/internal/templates/agents/defaults/agents.md.tmpl index 1efabff9ef..2bff688adb 100644 --- a/internal/templates/agents/defaults/agents.md.tmpl +++ b/internal/templates/agents/defaults/agents.md.tmpl @@ -36,7 +36,7 @@ cp -rf source dest # NOT: cp -r source dest - `apt-get` - use `-y` flag - `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var - + ## 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. @@ -44,7 +44,7 @@ cp -rf source dest # NOT: cp -r source dest ### Why bd? - Dependency-aware: Track blockers and relationships between issues -- Version-controlled: Built on Dolt with cell-level merge +- Git-friendly: Dolt-powered version control with native sync - Agent-optimized: JSON output, ready work detection, discovered-from links - Prevents duplicate tracking systems and confusion @@ -103,10 +103,10 @@ bd close bd-42 --reason "Completed" --json ### Auto-Sync -bd automatically syncs with git: +bd automatically syncs via Dolt: -- Exports to `.beads/issues.jsonl` after changes (5s debounce) -- Imports from JSONL when newer (e.g., after `git pull`) +- Each write auto-commits to Dolt history +- Use `bd dolt push`/`bd dolt pull` for remote sync - No manual export/import needed! ### Important Rules From 8b2df91eb2a356c904f298a00d41b2b1d033b8e3 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 11:18:10 -0700 Subject: [PATCH 08/10] style: fix gofmt formatting Amp-Thread-ID: https://ampcode.com/threads/T-019ced8f-f8dc-76ca-b8b1-7650879f3633 Co-authored-by: Amp --- cmd/bd/import.go | 2 +- internal/storage/dolt/store.go | 1 - internal/templates/agents/render_test.go | 8 ++++---- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/cmd/bd/import.go b/cmd/bd/import.go index 0c0c298e32..1f57cdd4dd 100644 --- a/cmd/bd/import.go +++ b/cmd/bd/import.go @@ -26,7 +26,7 @@ EXAMPLES: bd import backup.jsonl # Import from a specific file bd import --dry-run # Show what would be imported`, GroupID: "sync", - RunE: runImport, + RunE: runImport, } var ( diff --git a/internal/storage/dolt/store.go b/internal/storage/dolt/store.go index 02417fe8b4..3e9f22d4af 100644 --- a/internal/storage/dolt/store.go +++ b/internal/storage/dolt/store.go @@ -1844,7 +1844,6 @@ func (s *DoltStore) tryAutoResolveMetadataConflicts(ctx context.Context, tx *sql return true, nil } - // Branch creates a new branch func (s *DoltStore) Branch(ctx context.Context, name string) (retErr error) { ctx, span := doltTracer.Start(ctx, "dolt.branch", diff --git a/internal/templates/agents/render_test.go b/internal/templates/agents/render_test.go index ce0060f0c6..b2d5d67240 100644 --- a/internal/templates/agents/render_test.go +++ b/internal/templates/agents/render_test.go @@ -123,10 +123,10 @@ func TestRenderSectionFullBackcompat(t *testing.T) { func TestParseMarker(t *testing.T) { tests := []struct { - name string - line string - want SectionMeta - wantOK bool + name string + line string + want SectionMeta + wantOK bool }{ { name: "new format with profile and hash", From def866d7402e61e5a66ef087c0c4447da915e794 Mon Sep 17 00:00:00 2001 From: maphew Date: Sat, 14 Mar 2026 11:20:55 -0700 Subject: [PATCH 09/10] fix: suppress gosec G304 for resolved agent path Amp-Thread-ID: https://ampcode.com/threads/T-019ced8f-f8dc-76ca-b8b1-7650879f3633 Co-authored-by: Amp --- cmd/bd/setup/agents.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/bd/setup/agents.go b/cmd/bd/setup/agents.go index 9f649dcd02..a9e0abca19 100644 --- a/cmd/bd/setup/agents.go +++ b/cmd/bd/setup/agents.go @@ -79,7 +79,7 @@ func installAgents(env agentsEnv, integration agentsIntegration) error { } var currentContent string - data, err := os.ReadFile(resolvedPath) + data, err := os.ReadFile(resolvedPath) // #nosec G304 -- resolvedPath is derived from env.agentsPath via ResolveForWrite if err == nil { currentContent = string(data) } else if !os.IsNotExist(err) { From 7267cbbbbb45cf2dd59701efac8861dd32c88a70 Mon Sep 17 00:00:00 2001 From: maphew Date: Sun, 15 Mar 2026 11:33:51 -0700 Subject: [PATCH 10/10] feat(setup): target CLAUDE.md and GEMINI.md for hooks integrations Amp-Thread-ID: https://ampcode.com/threads/T-019cf2bd-bf1b-7269-abd8-ec65edb98bd5 Co-authored-by: Amp --- cmd/bd/setup/agents.go | 40 +++++++--- cmd/bd/setup/agents_marker_test.go | 33 +++++++++ cmd/bd/setup/claude.go | 112 +++++++++++++--------------- cmd/bd/setup/claude_test.go | 53 ++++++++++++++ cmd/bd/setup/gemini.go | 114 +++++++++++++---------------- cmd/bd/setup/gemini_test.go | 48 ++++++++++++ docs/CLI_REFERENCE.md | 14 +++- docs/SETUP.md | 36 +++++---- 8 files changed, 300 insertions(+), 150 deletions(-) diff --git a/cmd/bd/setup/agents.go b/cmd/bd/setup/agents.go index a9e0abca19..97b4f33932 100644 --- a/cmd/bd/setup/agents.go +++ b/cmd/bd/setup/agents.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "github.com/steveyegge/beads/internal/templates/agents" @@ -64,8 +65,17 @@ func resolveProfile(integration agentsIntegration) agents.Profile { return agents.ProfileFull } +func agentsFileName(path string) string { + base := filepath.Base(path) + if base == "" || base == "." { + return path + } + return base +} + func installAgents(env agentsEnv, integration agentsIntegration) error { _, _ = fmt.Fprintf(env.stdout, "Installing %s integration...\n", integration.name) + agentsFile := agentsFileName(env.agentsPath) profile := resolveProfile(integration) @@ -107,14 +117,14 @@ func installAgents(env agentsEnv, integration agentsIntegration) error { _, _ = 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") + _, _ = fmt.Fprintf(env.stdout, "✓ Updated existing beads section in %s\n", agentsFile) } else { newContent := currentContent + "\n\n" + beadsSection 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") + _, _ = fmt.Fprintf(env.stdout, "✓ Added beads section to existing %s\n", agentsFile) } } else { newContent := createNewAgentsFileWithProfile(profile) @@ -122,7 +132,7 @@ func installAgents(env agentsEnv, integration agentsIntegration) error { _, _ = 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, "✓ Created new %s with beads integration\n", agentsFile) } _, _ = fmt.Fprintf(env.stdout, "\n✓ %s integration installed\n", integration.name) @@ -138,9 +148,11 @@ func installAgents(env agentsEnv, integration agentsIntegration) error { } func checkAgents(env agentsEnv, integration agentsIntegration) error { + agentsFile := agentsFileName(env.agentsPath) + data, err := os.ReadFile(env.agentsPath) if os.IsNotExist(err) { - _, _ = fmt.Fprintln(env.stdout, "✗ AGENTS.md not found") + _, _ = fmt.Fprintf(env.stdout, "✗ %s not found\n", agentsFile) _, _ = fmt.Fprintf(env.stdout, " Run: %s\n", integration.setupCommand) return errAgentsFileMissing } else if err != nil { @@ -150,7 +162,7 @@ func checkAgents(env agentsEnv, integration agentsIntegration) error { content := string(data) if !containsBeadsMarker(content) { - _, _ = fmt.Fprintln(env.stdout, "⚠ AGENTS.md exists but no beads section found") + _, _ = fmt.Fprintf(env.stdout, "⚠ %s exists but no beads section found\n", agentsFile) _, _ = fmt.Fprintf(env.stdout, " Run: %s (to add beads section)\n", integration.setupCommand) return errBeadsSectionMissing } @@ -167,8 +179,15 @@ func checkAgents(env agentsEnv, integration agentsIntegration) error { } meta := agents.ParseMarker(line) - currentHash := agents.CurrentHash(profile) - if meta != nil && meta.Hash == currentHash && existingProf == profile { + checkProfile := profile + if profile == agents.ProfileMinimal && existingProf == agents.ProfileFull { + // Accept full profile as current when a minimal integration targets the same + // file (typically via symlinks like CLAUDE.md -> AGENTS.md). + checkProfile = agents.ProfileFull + } + + currentHash := agents.CurrentHash(checkProfile) + if meta != nil && meta.Hash == currentHash && existingProf == checkProfile { _, _ = fmt.Fprintf(env.stdout, "✓ %s integration installed: %s (current)\n", integration.name, env.agentsPath) return nil } @@ -181,9 +200,10 @@ func checkAgents(env agentsEnv, integration agentsIntegration) error { func removeAgents(env agentsEnv, integration agentsIntegration) error { _, _ = fmt.Fprintf(env.stdout, "Removing %s integration...\n", integration.name) + agentsFile := agentsFileName(env.agentsPath) data, err := os.ReadFile(env.agentsPath) if os.IsNotExist(err) { - _, _ = fmt.Fprintln(env.stdout, "No AGENTS.md file found") + _, _ = fmt.Fprintf(env.stdout, "No %s file found\n", agentsFile) return nil } else if err != nil { _, _ = fmt.Fprintf(env.stderr, "Error: failed to read %s: %v\n", env.agentsPath, err) @@ -192,7 +212,7 @@ func removeAgents(env agentsEnv, integration agentsIntegration) error { content := string(data) if !containsBeadsMarker(content) { - _, _ = fmt.Fprintln(env.stdout, "No beads section found in AGENTS.md") + _, _ = fmt.Fprintf(env.stdout, "No beads section found in %s\n", agentsFile) return nil } @@ -202,7 +222,7 @@ func removeAgents(env agentsEnv, integration agentsIntegration) error { _, _ = fmt.Fprintf(env.stderr, "Error: write %s: %v\n", env.agentsPath, err) return err } - _, _ = fmt.Fprintln(env.stdout, "✓ Removed beads section from AGENTS.md") + _, _ = fmt.Fprintf(env.stdout, "✓ Removed beads section from %s\n", agentsFile) return nil } diff --git a/cmd/bd/setup/agents_marker_test.go b/cmd/bd/setup/agents_marker_test.go index 7eb81466a7..0674846ffa 100644 --- a/cmd/bd/setup/agents_marker_test.go +++ b/cmd/bd/setup/agents_marker_test.go @@ -247,6 +247,39 @@ func TestCheckAgentsCurrent(t *testing.T) { } } +func TestCheckAgentsMinimalAcceptsFullProfile(t *testing.T) { + env, _, _ := newFactoryTestEnv(t) + section := agents.RenderSection(agents.ProfileFull) + if err := os.WriteFile(env.agentsPath, []byte(section), 0644); err != nil { + t.Fatalf("write: %v", err) + } + integration := agentsIntegration{ + name: "ClaudeCode", + setupCommand: "bd setup claude", + profile: agents.ProfileMinimal, + } + if err := checkAgents(env, integration); err != nil { + t.Fatalf("expected full profile to be accepted for minimal integration, got %v", err) + } +} + +func TestCheckAgentsMissingUsesTargetFileName(t *testing.T) { + stdout := &bytes.Buffer{} + env := agentsEnv{ + agentsPath: filepath.Join(t.TempDir(), "CLAUDE.md"), + stdout: stdout, + stderr: &bytes.Buffer{}, + } + integration := agentsIntegration{name: "ClaudeCode", setupCommand: "bd setup claude", profile: agents.ProfileMinimal} + err := checkAgents(env, integration) + if !errors.Is(err, errAgentsFileMissing) { + t.Fatalf("expected errAgentsFileMissing, got %v", err) + } + if !strings.Contains(stdout.String(), "CLAUDE.md not found") { + t.Fatalf("expected target filename in output, got: %s", stdout.String()) + } +} + func TestInstallAgentsPreservesFullProfile(t *testing.T) { // Simulate: file already has full profile, requesting minimal install env, stdout, _ := newFactoryTestEnv(t) diff --git a/cmd/bd/setup/claude.go b/cmd/bd/setup/claude.go index e29b9c9a6a..0d50dd22ef 100644 --- a/cmd/bd/setup/claude.go +++ b/cmd/bd/setup/claude.go @@ -16,6 +16,14 @@ var ( errClaudeHooksMissing = errors.New("claude hooks not installed") ) +const claudeInstructionsFile = "CLAUDE.md" + +var claudeAgentsIntegration = agentsIntegration{ + name: "Claude Code", + setupCommand: "bd setup claude", + profile: agents.ProfileMinimal, +} + type claudeEnv struct { stdout io.Writer stderr io.Writer @@ -56,6 +64,14 @@ func globalSettingsPath(home string) string { return filepath.Join(home, ".claude", "settings.json") } +func claudeAgentsEnv(env claudeEnv) agentsEnv { + return agentsEnv{ + agentsPath: filepath.Join(env.projectDir, claudeInstructionsFile), + stdout: env.stdout, + stderr: env.stderr, + } +} + // InstallClaude installs Claude Code hooks func InstallClaude(project bool, stealth bool) { env, err := claudeEnvProvider() @@ -129,23 +145,11 @@ func installClaude(env claudeEnv, project bool, stealth bool) error { return err } - // Install minimal beads section in AGENTS.md (Claude reads this on session start). - // Hooks handle the heavy lifting via bd prime; AGENTS.md just needs a pointer. - agentsEnv := agentsEnv{ - agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), - stdout: env.stdout, - stderr: env.stderr, - } - claudeAgentsIntegration := agentsIntegration{ - name: "Claude Code", - setupCommand: "bd setup claude", - profile: agents.ProfileMinimal, - } - if project { - if err := installAgents(agentsEnv, claudeAgentsIntegration); err != nil { - // Non-fatal: hooks are already installed - _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) - } + // Install minimal beads section in CLAUDE.md. + // Hooks handle the heavy lifting via bd prime; CLAUDE.md just needs a pointer. + if err := installAgents(claudeAgentsEnv(env), claudeAgentsIntegration); err != nil { + // Non-fatal: hooks are already installed + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update %s: %v\n", claudeInstructionsFile, err) } _, _ = fmt.Fprintln(env.stdout, "\n✓ Claude Code integration installed") @@ -174,15 +178,15 @@ func checkClaude(env claudeEnv) error { switch { case hasBeadsHooks(globalSettings): _, _ = fmt.Fprintf(env.stdout, "✓ Global hooks installed: %s\n", globalSettings) - return nil case hasBeadsHooks(projectSettings): _, _ = fmt.Fprintf(env.stdout, "✓ Project hooks installed: %s\n", projectSettings) - return nil default: _, _ = fmt.Fprintln(env.stdout, "✗ No hooks installed") _, _ = fmt.Fprintln(env.stdout, " Run: bd setup claude") return errClaudeHooksMissing } + + return checkAgents(claudeAgentsEnv(env), claudeAgentsIntegration) } // RemoveClaude removes Claude Code hooks @@ -211,52 +215,38 @@ func removeClaude(env claudeEnv, project bool) error { data, err := env.readFile(settingsPath) if err != nil { _, _ = fmt.Fprintln(env.stdout, "No settings file found") - return nil - } - - var settings map[string]interface{} - if err := json.Unmarshal(data, &settings); err != nil { - _, _ = fmt.Fprintf(env.stderr, "Error: failed to parse settings.json: %v\n", err) - return err - } - - hooks, ok := settings["hooks"].(map[string]interface{}) - if !ok { - _, _ = fmt.Fprintln(env.stdout, "No hooks found") - return nil - } - - removeHookCommand(hooks, "SessionStart", "bd prime") - removeHookCommand(hooks, "PreCompact", "bd prime") - removeHookCommand(hooks, "SessionStart", "bd prime --stealth") - removeHookCommand(hooks, "PreCompact", "bd prime --stealth") + } else { + var settings map[string]interface{} + if err := json.Unmarshal(data, &settings); err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: failed to parse settings.json: %v\n", err) + return err + } - data, err = json.MarshalIndent(settings, "", " ") - if err != nil { - _, _ = fmt.Fprintf(env.stderr, "Error: marshal settings: %v\n", err) - return err - } + hooks, ok := settings["hooks"].(map[string]interface{}) + if !ok { + _, _ = fmt.Fprintln(env.stdout, "No hooks found") + } else { + removeHookCommand(hooks, "SessionStart", "bd prime") + removeHookCommand(hooks, "PreCompact", "bd prime") + removeHookCommand(hooks, "SessionStart", "bd prime --stealth") + removeHookCommand(hooks, "PreCompact", "bd prime --stealth") + + data, err = json.MarshalIndent(settings, "", " ") + if err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: marshal settings: %v\n", err) + return err + } - if err := env.writeFile(settingsPath, data); err != nil { - _, _ = fmt.Fprintf(env.stderr, "Error: write settings: %v\n", err) - return err + if err := env.writeFile(settingsPath, data); err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: write settings: %v\n", err) + return err + } + } } - // Also remove beads section from AGENTS.md if project removal - if project { - agentsEnv := agentsEnv{ - agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), - stdout: env.stdout, - stderr: env.stderr, - } - claudeAgentsIntegration := agentsIntegration{ - name: "Claude Code", - setupCommand: "bd setup claude", - } - if err := removeAgents(agentsEnv, claudeAgentsIntegration); err != nil { - // Non-fatal - _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) - } + if err := removeAgents(claudeAgentsEnv(env), claudeAgentsIntegration); err != nil { + // Non-fatal + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update %s: %v\n", claudeInstructionsFile, err) } _, _ = fmt.Fprintln(env.stdout, "✓ Claude hooks removed") diff --git a/cmd/bd/setup/claude_test.go b/cmd/bd/setup/claude_test.go index 4586af61ab..5f6d6300fc 100644 --- a/cmd/bd/setup/claude_test.go +++ b/cmd/bd/setup/claude_test.go @@ -8,6 +8,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/steveyegge/beads/internal/templates/agents" ) func newClaudeTestEnv(t *testing.T) (claudeEnv, *bytes.Buffer, *bytes.Buffer) { @@ -567,6 +569,14 @@ func TestInstallClaudeProject(t *testing.T) { if !hasBeadsHooks(projectSettingsPath(env.projectDir)) { t.Fatal("project hooks not detected") } + instructionsPath := filepath.Join(env.projectDir, claudeInstructionsFile) + instructions, err := os.ReadFile(instructionsPath) + if err != nil { + t.Fatalf("read %s: %v", claudeInstructionsFile, err) + } + if !strings.Contains(string(instructions), "profile:minimal") { + t.Fatalf("expected minimal profile in %s", claudeInstructionsFile) + } if !strings.Contains(stdout.String(), "project") { t.Error("expected project installation message") } @@ -587,6 +597,10 @@ func TestInstallClaudeGlobalStealth(t *testing.T) { if !strings.Contains(string(data), "bd prime --stealth") { t.Error("expected stealth command in settings") } + instructionsPath := filepath.Join(env.projectDir, claudeInstructionsFile) + if _, err := os.Stat(instructionsPath); err != nil { + t.Fatalf("expected %s to be created: %v", claudeInstructionsFile, err) + } if !strings.Contains(stdout.String(), "globally") { t.Error("expected global installation message") } @@ -634,6 +648,9 @@ func TestCheckClaudeScenarios(t *testing.T) { }, }, }) + if err := os.WriteFile(filepath.Join(env.projectDir, claudeInstructionsFile), []byte(agents.RenderSection(agents.ProfileMinimal)), 0o644); err != nil { + t.Fatalf("write %s: %v", claudeInstructionsFile, err) + } if err := checkClaude(env); err != nil { t.Fatalf("checkClaude: %v", err) } @@ -656,6 +673,9 @@ func TestCheckClaudeScenarios(t *testing.T) { }, }, }) + if err := os.WriteFile(filepath.Join(env.projectDir, claudeInstructionsFile), []byte(agents.RenderSection(agents.ProfileMinimal)), 0o644); err != nil { + t.Fatalf("write %s: %v", claudeInstructionsFile, err) + } if err := checkClaude(env); err != nil { t.Fatalf("checkClaude: %v", err) } @@ -673,6 +693,28 @@ func TestCheckClaudeScenarios(t *testing.T) { t.Error("expected guidance message") } }) + + t.Run("missing instructions", func(t *testing.T) { + env, stdout, _ := newClaudeTestEnv(t) + writeSettings(t, globalSettingsPath(env.homeDir), map[string]interface{}{ + "hooks": map[string]interface{}{ + "SessionStart": []interface{}{ + map[string]interface{}{ + "matcher": "", + "hooks": []interface{}{ + map[string]interface{}{"type": "command", "command": "bd prime"}, + }, + }, + }, + }, + }) + if err := checkClaude(env); !errors.Is(err, errAgentsFileMissing) { + t.Fatalf("expected errAgentsFileMissing, got %v", err) + } + if !strings.Contains(stdout.String(), claudeInstructionsFile+" not found") { + t.Fatalf("expected missing %s message, got: %s", claudeInstructionsFile, stdout.String()) + } + }) } func TestRemoveClaudeScenarios(t *testing.T) { @@ -692,6 +734,10 @@ func TestRemoveClaudeScenarios(t *testing.T) { }, }, }) + instructionsPath := filepath.Join(env.projectDir, claudeInstructionsFile) + if err := os.WriteFile(instructionsPath, []byte(agents.RenderSection(agents.ProfileMinimal)), 0o644); err != nil { + t.Fatalf("seed %s: %v", claudeInstructionsFile, err) + } if err := removeClaude(env, false); err != nil { t.Fatalf("removeClaude: %v", err) } @@ -702,6 +748,13 @@ func TestRemoveClaudeScenarios(t *testing.T) { if strings.Contains(string(data), "bd prime") { t.Error("expected bd prime hooks removed") } + instructions, err := os.ReadFile(instructionsPath) + if err != nil { + t.Fatalf("read %s: %v", claudeInstructionsFile, err) + } + if strings.Contains(string(instructions), "BEGIN BEADS INTEGRATION") { + t.Fatalf("expected beads section removed from %s", claudeInstructionsFile) + } if !strings.Contains(stdout.String(), "hooks removed") { t.Error("expected success message") } diff --git a/cmd/bd/setup/gemini.go b/cmd/bd/setup/gemini.go index 3a1f71ae39..af36100ab9 100644 --- a/cmd/bd/setup/gemini.go +++ b/cmd/bd/setup/gemini.go @@ -16,6 +16,14 @@ var ( errGeminiHooksMissing = errors.New("gemini hooks not installed") ) +const geminiInstructionsFile = "GEMINI.md" + +var geminiAgentsIntegration = agentsIntegration{ + name: "Gemini CLI", + setupCommand: "bd setup gemini", + profile: agents.ProfileMinimal, +} + type geminiEnv struct { stdout io.Writer stderr io.Writer @@ -56,6 +64,14 @@ func geminiGlobalSettingsPath(home string) string { return filepath.Join(home, ".gemini", "settings.json") } +func geminiAgentsEnv(env geminiEnv) agentsEnv { + return agentsEnv{ + agentsPath: filepath.Join(env.projectDir, geminiInstructionsFile), + stdout: env.stdout, + stderr: env.stderr, + } +} + // InstallGemini installs Gemini CLI hooks func InstallGemini(project bool, stealth bool) { env, err := geminiEnvProvider() @@ -122,23 +138,11 @@ func installGemini(env geminiEnv, project bool, stealth bool) error { return err } - // Install minimal beads section in AGENTS.md (Gemini reads this on session start). - // Hooks handle the heavy lifting via bd prime; AGENTS.md just needs a pointer. - agentsEnv := agentsEnv{ - agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), - stdout: env.stdout, - stderr: env.stderr, - } - geminiAgentsIntegration := agentsIntegration{ - name: "Gemini CLI", - setupCommand: "bd setup gemini", - profile: agents.ProfileMinimal, - } - if project { - if err := installAgents(agentsEnv, geminiAgentsIntegration); err != nil { - // Non-fatal: hooks are already installed - _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) - } + // Install minimal beads section in GEMINI.md. + // Hooks handle the heavy lifting via bd prime; GEMINI.md just needs a pointer. + if err := installAgents(geminiAgentsEnv(env), geminiAgentsIntegration); err != nil { + // Non-fatal: hooks are already installed + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update %s: %v\n", geminiInstructionsFile, err) } _, _ = fmt.Fprintln(env.stdout, "\n✓ Gemini CLI integration installed") @@ -167,15 +171,15 @@ func checkGemini(env geminiEnv) error { switch { case hasGeminiBeadsHooks(globalSettings): _, _ = fmt.Fprintf(env.stdout, "✓ Global hooks installed: %s\n", globalSettings) - return nil case hasGeminiBeadsHooks(projectSettings): _, _ = fmt.Fprintf(env.stdout, "✓ Project hooks installed: %s\n", projectSettings) - return nil default: _, _ = fmt.Fprintln(env.stdout, "✗ No hooks installed") _, _ = fmt.Fprintln(env.stdout, " Run: bd setup gemini") return errGeminiHooksMissing } + + return checkAgents(geminiAgentsEnv(env), geminiAgentsIntegration) } // RemoveGemini removes Gemini CLI hooks @@ -204,53 +208,39 @@ func removeGemini(env geminiEnv, project bool) error { data, err := env.readFile(settingsPath) if err != nil { _, _ = fmt.Fprintln(env.stdout, "No settings file found") - return nil - } - - var settings map[string]interface{} - if err := json.Unmarshal(data, &settings); err != nil { - _, _ = fmt.Fprintf(env.stderr, "Error: failed to parse settings.json: %v\n", err) - return err - } - - hooks, ok := settings["hooks"].(map[string]interface{}) - if !ok { - _, _ = fmt.Fprintln(env.stdout, "No hooks found") - return nil - } - - // Remove both variants from both events - removeHookCommand(hooks, "SessionStart", "bd prime") - removeHookCommand(hooks, "PreCompress", "bd prime") - removeHookCommand(hooks, "SessionStart", "bd prime --stealth") - removeHookCommand(hooks, "PreCompress", "bd prime --stealth") + } else { + var settings map[string]interface{} + if err := json.Unmarshal(data, &settings); err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: failed to parse settings.json: %v\n", err) + return err + } - data, err = json.MarshalIndent(settings, "", " ") - if err != nil { - _, _ = fmt.Fprintf(env.stderr, "Error: marshal settings: %v\n", err) - return err - } + hooks, ok := settings["hooks"].(map[string]interface{}) + if !ok { + _, _ = fmt.Fprintln(env.stdout, "No hooks found") + } else { + // Remove both variants from both events + removeHookCommand(hooks, "SessionStart", "bd prime") + removeHookCommand(hooks, "PreCompress", "bd prime") + removeHookCommand(hooks, "SessionStart", "bd prime --stealth") + removeHookCommand(hooks, "PreCompress", "bd prime --stealth") + + data, err = json.MarshalIndent(settings, "", " ") + if err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: marshal settings: %v\n", err) + return err + } - if err := env.writeFile(settingsPath, data); err != nil { - _, _ = fmt.Fprintf(env.stderr, "Error: write settings: %v\n", err) - return err + if err := env.writeFile(settingsPath, data); err != nil { + _, _ = fmt.Fprintf(env.stderr, "Error: write settings: %v\n", err) + return err + } + } } - // Also remove beads section from AGENTS.md if project removal - if project { - agentsEnv := agentsEnv{ - agentsPath: filepath.Join(env.projectDir, "AGENTS.md"), - stdout: env.stdout, - stderr: env.stderr, - } - geminiAgentsIntegration := agentsIntegration{ - name: "Gemini CLI", - setupCommand: "bd setup gemini", - } - if err := removeAgents(agentsEnv, geminiAgentsIntegration); err != nil { - // Non-fatal - _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update AGENTS.md: %v\n", err) - } + if err := removeAgents(geminiAgentsEnv(env), geminiAgentsIntegration); err != nil { + // Non-fatal + _, _ = fmt.Fprintf(env.stderr, "Warning: failed to update %s: %v\n", geminiInstructionsFile, err) } _, _ = fmt.Fprintln(env.stdout, "✓ Gemini CLI hooks removed") diff --git a/cmd/bd/setup/gemini_test.go b/cmd/bd/setup/gemini_test.go index dd675d30b6..83f5948b9b 100644 --- a/cmd/bd/setup/gemini_test.go +++ b/cmd/bd/setup/gemini_test.go @@ -7,6 +7,8 @@ import ( "path/filepath" "strings" "testing" + + "github.com/steveyegge/beads/internal/templates/agents" ) func newGeminiTestEnv(t *testing.T) (geminiEnv, *bytes.Buffer, *bytes.Buffer) { @@ -113,6 +115,14 @@ func TestInstallGemini_Global(t *testing.T) { if !strings.Contains(out, "Gemini CLI integration installed") { t.Errorf("expected success message, got: %s", out) } + instructionsPath := filepath.Join(env.projectDir, geminiInstructionsFile) + instructions, err := os.ReadFile(instructionsPath) + if err != nil { + t.Fatalf("read %s: %v", geminiInstructionsFile, err) + } + if !strings.Contains(string(instructions), "profile:minimal") { + t.Fatalf("expected minimal profile in %s", geminiInstructionsFile) + } } func TestInstallGemini_Project(t *testing.T) { @@ -133,6 +143,10 @@ func TestInstallGemini_Project(t *testing.T) { if !strings.Contains(out, "Installing Gemini CLI hooks for this project") { t.Errorf("expected project install message, got: %s", out) } + instructionsPath := filepath.Join(env.projectDir, geminiInstructionsFile) + if _, err := os.Stat(instructionsPath); err != nil { + t.Fatalf("expected %s to be created: %v", geminiInstructionsFile, err) + } } func TestInstallGemini_Stealth(t *testing.T) { @@ -268,6 +282,29 @@ func TestCheckGemini_ProjectInstalled(t *testing.T) { } } +func TestCheckGemini_MissingInstructions(t *testing.T) { + env, stdout, _ := newGeminiTestEnv(t) + + if err := installGemini(env, false, false); err != nil { + t.Fatalf("installGemini: %v", err) + } + + if err := os.Remove(filepath.Join(env.projectDir, geminiInstructionsFile)); err != nil { + t.Fatalf("remove %s: %v", geminiInstructionsFile, err) + } + + stdout.Reset() + err := checkGemini(env) + if err != errAgentsFileMissing { + t.Fatalf("expected errAgentsFileMissing, got: %v", err) + } + + out := stdout.String() + if !strings.Contains(out, geminiInstructionsFile+" not found") { + t.Fatalf("expected missing %s message, got: %s", geminiInstructionsFile, out) + } +} + func TestRemoveGemini_Global(t *testing.T) { env, stdout, _ := newGeminiTestEnv(t) @@ -275,6 +312,10 @@ func TestRemoveGemini_Global(t *testing.T) { if err := installGemini(env, false, false); err != nil { t.Fatalf("installGemini: %v", err) } + instructionsPath := filepath.Join(env.projectDir, geminiInstructionsFile) + if err := os.WriteFile(instructionsPath, []byte(agents.RenderSection(agents.ProfileMinimal)), 0o644); err != nil { + t.Fatalf("seed %s: %v", geminiInstructionsFile, err) + } stdout.Reset() @@ -292,6 +333,13 @@ func TestRemoveGemini_Global(t *testing.T) { if ok && len(sessionStart) > 0 { t.Error("SessionStart hooks should be empty") } + instructions, err := os.ReadFile(instructionsPath) + if err != nil { + t.Fatalf("read %s: %v", geminiInstructionsFile, err) + } + if strings.Contains(string(instructions), "BEGIN BEADS INTEGRATION") { + t.Fatalf("expected beads section removed from %s", geminiInstructionsFile) + } out := stdout.String() if !strings.Contains(out, "Gemini CLI hooks removed") { diff --git a/docs/CLI_REFERENCE.md b/docs/CLI_REFERENCE.md index d9281d53f9..9f55d21aae 100644 --- a/docs/CLI_REFERENCE.md +++ b/docs/CLI_REFERENCE.md @@ -862,7 +862,8 @@ bd dolt push # Force immediate sync, bypass debounce bd setup factory # Factory.ai Droid - creates/updates AGENTS.md (universal standard) bd setup codex # Codex CLI - creates/updates AGENTS.md bd setup mux # Mux - creates/updates AGENTS.md -bd setup claude # Claude Code - installs SessionStart/PreCompact hooks +bd setup claude # Claude Code - installs hooks + manages CLAUDE.md (minimal profile) +bd setup gemini # Gemini CLI - installs hooks + manages GEMINI.md (minimal profile) bd setup cursor # Cursor IDE - creates .cursor/rules/beads.mdc bd setup aider # Aider - creates .aider.conf.yml @@ -871,6 +872,7 @@ bd setup factory --check bd setup codex --check bd setup mux --check bd setup claude --check +bd setup gemini --check bd setup cursor --check bd setup aider --check @@ -879,6 +881,7 @@ bd setup factory --remove bd setup codex --remove bd setup mux --remove bd setup claude --remove +bd setup gemini --remove bd setup cursor --remove bd setup aider --remove ``` @@ -888,6 +891,9 @@ bd setup aider --remove bd setup claude # Install globally (~/.claude/settings.json) bd setup claude --project # Install for this project only bd setup claude --stealth # Use stealth mode (flush only, no git operations) +bd setup gemini # Install globally (~/.gemini/settings.json) +bd setup gemini --project # Install for this project only +bd setup gemini --stealth # Use stealth mode (flush only, no git operations) bd setup mux --project # Also install .mux/AGENTS.md workspace layer bd setup mux --global # Also install ~/.mux/AGENTS.md global layer ``` @@ -896,12 +902,12 @@ bd setup mux --global # Also install ~/.mux/AGENTS.md global layer - **Factory.ai** (`bd setup factory`): Creates or updates AGENTS.md with beads workflow instructions (full profile — 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 (full profile) - **Mux** (`bd setup mux`): Creates or updates AGENTS.md with beads workflow instructions for Mux workspaces (full profile) -- **Claude Code** (`bd setup claude`): Adds hooks to Claude Code's settings.json that run `bd prime` on SessionStart and PreCompact events; project mode also adds minimal-profile beads section to AGENTS.md -- **Gemini CLI** (`bd setup gemini`): Adds hooks to Gemini's settings.json that run `bd prime` on SessionStart and PreCompress events; project mode also adds minimal-profile beads section to AGENTS.md +- **Claude Code** (`bd setup claude`): Adds hooks to Claude Code's settings.json that run `bd prime` on SessionStart and PreCompact events and manages a minimal-profile beads section in `CLAUDE.md` +- **Gemini CLI** (`bd setup gemini`): Adds hooks to Gemini's settings.json that run `bd prime` on SessionStart and PreCompress events and manages a minimal-profile beads section in `GEMINI.md` - **Cursor** (`bd setup cursor`): Creates `.cursor/rules/beads.mdc` with workflow instructions - **Aider** (`bd setup aider`): Creates `.aider.conf.yml` with bd workflow instructions -**`--check` behavior:** Reports section status as `current` (up to date), `stale` (legacy or hash mismatch — run setup to update), or `missing` (no beads section). Stale and missing return non-zero exit codes. +**`--check` behavior:** For section-based integrations (including Claude/Gemini instruction files), reports status as `current` (up to date), `stale` (legacy or hash mismatch — run setup to update), or `missing` (no beads section). Stale and missing return non-zero exit codes. See also: - [INSTALLING.md](INSTALLING.md#ide-and-editor-integrations) - Installation guide diff --git a/docs/SETUP.md b/docs/SETUP.md index 345d54073d..daab33787e 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -9,18 +9,18 @@ The `bd setup` command uses a **recipe-based architecture** to configure beads i ### `bd prime` as SSOT -`bd prime` is the **single source of truth** for operational workflow commands. The AGENTS.md beads section provides a pointer to `bd prime` for hook-enabled agents (Claude, Gemini) or the full command reference for hookless agents (Factory, Codex, Mux). +`bd prime` is the **single source of truth** for operational workflow commands. The beads section in each tool's instruction file provides a pointer to `bd prime` for hook-enabled agents (Claude, Gemini) or the full command reference for hookless agents (Factory, Codex, Mux). ### Profiles -Each integration uses one of two **profiles** that control how much content is written to AGENTS.md: +Each integration uses one of two **profiles** that control how much content is written to tool instruction files (`AGENTS.md`, `CLAUDE.md`, or `GEMINI.md`): | Profile | Used By | Content | |---------|---------|---------| | `full` | Factory, Codex, Mux, OpenCode | Complete command reference, issue types, priorities, workflow | | `minimal` | Claude Code, Gemini CLI | Pointer to `bd prime`, quick reference only (~60% smaller) | -Hook-enabled agents (Claude, Gemini) use the `minimal` profile because `bd prime` injects full context at session start. Hookless agents need the `full` profile because AGENTS.md is their only source of instructions. +Hook-enabled agents (Claude, Gemini) use the `minimal` profile because `bd prime` injects full context at session start. Hookless agents need the `full` profile because their instruction file is their only source of instructions. **Profile precedence:** If a file already has a `full` profile section and a `minimal` profile tool installs to the same file (e.g., via symlinks), the `full` profile is preserved to avoid information loss. @@ -32,8 +32,8 @@ Hook-enabled agents (Claude, Gemini) use the `minimal` profile because `bd prime | `windsurf` | `.windsurf/rules/beads.md` | Rules file | | `cody` | `.cody/rules/beads.md` | Rules file | | `kilocode` | `.kilocode/rules/beads.md` | Rules file | -| `claude` | `~/.claude/settings.json` | SessionStart/PreCompact hooks | -| `gemini` | `~/.gemini/settings.json` | SessionStart/PreCompress hooks | +| `claude` | `~/.claude/settings.json` + `CLAUDE.md` | SessionStart/PreCompact hooks + minimal section | +| `gemini` | `~/.gemini/settings.json` + `GEMINI.md` | SessionStart/PreCompress hooks + minimal section | | `factory` | `AGENTS.md` | Marked section | | `codex` | `AGENTS.md` | Marked section | | `mux` | `AGENTS.md` | Marked section | @@ -234,21 +234,26 @@ bd setup claude --stealth **Project installation** (`.claude/settings.local.json`): - Same hooks, but only active for this project +**Instruction file** (`CLAUDE.md` in project root): +- Minimal-profile beads section pointing to `bd prime` +- Managed with hash/version markers for safe updates and `--check` freshness detection + ### Flags | Flag | Description | |------|-------------| -| `--check` | Check if integration is installed | -| `--remove` | Remove beads hooks | +| `--check` | Check both hooks and the managed `CLAUDE.md` beads section | +| `--remove` | Remove beads hooks and managed `CLAUDE.md` beads section | | `--project` | Install for this project only (not globally) | | `--stealth` | Use `bd prime --stealth` (flush only, no git operations) | ### Examples ```bash -# Check if hooks are installed +# Check hooks + CLAUDE.md beads section bd setup claude --check # Output: ✓ Global hooks installed: /Users/you/.claude/settings.json +# ✓ Claude Code integration installed: /path/to/CLAUDE.md (current) # Remove hooks bd setup claude --remove @@ -292,21 +297,26 @@ bd setup gemini --stealth **Project installation** (`.gemini/settings.json`): - Same hooks, but only active for this project +**Instruction file** (`GEMINI.md` in project root): +- Minimal-profile beads section pointing to `bd prime` +- Managed with hash/version markers for safe updates and `--check` freshness detection + ### Flags | Flag | Description | |------|-------------| -| `--check` | Check if integration is installed | -| `--remove` | Remove beads hooks | +| `--check` | Check both hooks and the managed `GEMINI.md` beads section | +| `--remove` | Remove beads hooks and managed `GEMINI.md` beads section | | `--project` | Install for this project only (not globally) | | `--stealth` | Use `bd prime --stealth` (flush only, no git operations) | ### Examples ```bash -# Check if hooks are installed +# Check hooks + GEMINI.md beads section bd setup gemini --check # Output: ✓ Global hooks installed: /Users/you/.gemini/settings.json +# ✓ Gemini CLI integration installed: /path/to/GEMINI.md (current) # Remove hooks bd setup gemini --remove @@ -427,7 +437,7 @@ This respects Aider's philosophy of keeping humans in control while still levera | Feature | Factory.ai | Codex | Mux | Claude Code | Gemini CLI | Cursor | Aider | |---------|-----------|-------|-----|-------------|------------|--------|-------| | Command execution | Automatic | Automatic | Automatic | Automatic | Automatic | Automatic | Manual (/run) | -| Context injection | AGENTS.md | AGENTS.md | AGENTS.md | Hooks | Hooks | Rules file | Config file | +| Context injection | AGENTS.md | AGENTS.md | AGENTS.md | Hooks + CLAUDE.md | Hooks + GEMINI.md | Rules file | Config file | | Global install | No (per-project) | No (per-project) | No (per-project) | Yes | Yes | No (per-project) | No (per-project) | | Stealth mode | N/A | N/A | N/A | Yes | Yes | N/A | N/A | | Standard format | Yes (AGENTS.md) | Yes (AGENTS.md) | Yes (AGENTS.md) | No (proprietary) | No (proprietary) | No (proprietary) | No (proprietary) | @@ -446,7 +456,7 @@ This respects Aider's philosophy of keeping humans in control while still levera 4. **Use stealth mode in CI/CD** - `bd setup claude --stealth` or `bd setup gemini --stealth` avoids git operations that might fail in automated environments -5. **Commit AGENTS.md to git** - This ensures all team members and AI tools get the same instructions +5. **Commit instruction files to git** - This ensures all team members and AI tools get the same instructions (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, as applicable) 6. **Run `bd doctor` after setup** - Verifies the integration is working: ```bash