Files
gastown/internal/beads/beads.go
mayor b316239d12 chore(gastown): scorched-earth SQLite removal from codebase
Remove all bd sync references and SQLite-specific code from gastown:

**Formulas (agent priming):**
- mol-polecat-work: Remove bd sync step from prepare-for-review
- mol-sync-workspace: Replace sync-beads step with verify-beads (Dolt check)
- mol-polecat-conflict-resolve: Remove bd sync from close-beads
- mol-polecat-code-review: Remove bd sync from summarize-review and complete-and-exit
- mol-polecat-review-pr: Remove bd sync from complete-and-exit
- mol-convoy-cleanup: Remove bd sync from archive-convoy
- mol-digest-generate: Remove bd sync from send-digest
- mol-town-shutdown: Replace sync-state step with verify-state
- beads-release: Replace restart-daemons with verify-install (no daemons with Dolt)

**Templates (role priming):**
- mayor.md.tmpl: Update session end checklist to remove bd sync steps
- crew.md.tmpl: Remove bd sync references from workflow and checklist
- polecat.md.tmpl: Update self-cleaning model and session close docs
- spawn.md.tmpl: Remove bd sync from completion steps
- nudge.md.tmpl: Remove bd sync from completion steps

**Go code:**
- session_manager.go: Remove syncBeads function and call
- rig_dock.go: Remove bd sync calls from dock/undock
- crew/manager.go: Remove runBdSync, update Pristine function
- crew_maintenance.go: Remove bd sync status output
- crew.go: Update pristine command help text
- polecat.go: Make sync command a no-op with deprecation message
- daemon/lifecycle.go: Remove bd sync from startup sequence
- doctor/beads_check.go: Update fix hints and Fix to use bd import not bd sync
- doctor/rig_check.go: Remove sync status check, simplify BeadsConfigValidCheck
- beads/beads.go: Update primeContent to remove bd sync references

With Dolt backend, beads changes are persisted immediately to the sql-server.
There is no separate sync step needed.

Part of epic: hq-e4eefc (SQLite removal)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:08:53 -08:00

799 lines
24 KiB
Go

// Package beads provides a wrapper for the bd (beads) CLI.
package beads
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/steveyegge/gastown/internal/runtime"
)
// Common errors
// ZFC: Only define errors that don't require stderr parsing for decisions.
// ErrNotARepo and ErrSyncConflict were removed - agents should handle these directly.
var (
ErrNotInstalled = errors.New("bd not installed: run 'pip install beads-cli' or see https://github.com/anthropics/beads")
ErrNotFound = errors.New("issue not found")
)
// Issue represents a beads issue.
type Issue struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Status string `json:"status"`
Priority int `json:"priority"`
Type string `json:"issue_type"`
CreatedAt string `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
UpdatedAt string `json:"updated_at"`
ClosedAt string `json:"closed_at,omitempty"`
Parent string `json:"parent,omitempty"`
Assignee string `json:"assignee,omitempty"`
Children []string `json:"children,omitempty"`
DependsOn []string `json:"depends_on,omitempty"`
Blocks []string `json:"blocks,omitempty"`
BlockedBy []string `json:"blocked_by,omitempty"`
Labels []string `json:"labels,omitempty"`
// Agent bead slots (type=agent only)
HookBead string `json:"hook_bead,omitempty"` // Current work attached to agent's hook
AgentState string `json:"agent_state,omitempty"` // Agent lifecycle state (spawning, working, done, stuck)
// Note: role_bead field removed - role definitions are now config-based
// Counts from list output
DependencyCount int `json:"dependency_count,omitempty"`
DependentCount int `json:"dependent_count,omitempty"`
BlockedByCount int `json:"blocked_by_count,omitempty"`
// Detailed dependency info from show output
Dependencies []IssueDep `json:"dependencies,omitempty"`
Dependents []IssueDep `json:"dependents,omitempty"`
}
// IssueDep represents a dependency or dependent issue with its relation.
type IssueDep struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
Priority int `json:"priority"`
Type string `json:"issue_type"`
DependencyType string `json:"dependency_type,omitempty"`
}
// ListOptions specifies filters for listing issues.
type ListOptions struct {
Status string // "open", "closed", "all"
Type string // Deprecated: use Label instead. "task", "bug", "feature", "epic"
Label string // Label filter (e.g., "gt:agent", "gt:merge-request")
Priority int // 0-4, -1 for no filter
Parent string // filter by parent ID
Assignee string // filter by assignee (e.g., "gastown/Toast")
NoAssignee bool // filter for issues with no assignee
}
// CreateOptions specifies options for creating an issue.
type CreateOptions struct {
Title string
Type string // "task", "bug", "feature", "epic"
Priority int // 0-4
Description string
Parent string
Actor string // Who is creating this issue (populates created_by)
Ephemeral bool // Create as ephemeral (wisp) - not exported to JSONL
}
// UpdateOptions specifies options for updating an issue.
type UpdateOptions struct {
Title *string
Status *string
Priority *int
Description *string
Assignee *string
AddLabels []string // Labels to add
RemoveLabels []string // Labels to remove
SetLabels []string // Labels to set (replaces all existing)
}
// SyncStatus represents the sync status of the beads repository.
type SyncStatus struct {
Branch string
Ahead int
Behind int
Conflicts []string
}
// Beads wraps bd CLI operations for a working directory.
type Beads struct {
workDir string
beadsDir string // Optional BEADS_DIR override for cross-database access
isolated bool // If true, suppress inherited beads env vars (for test isolation)
// Lazy-cached town root for routing resolution.
// Populated on first call to getTownRoot() to avoid filesystem walk on every operation.
townRoot string
searchedRoot bool
}
// New creates a new Beads wrapper for the given directory.
func New(workDir string) *Beads {
return &Beads{workDir: workDir}
}
// NewIsolated creates a Beads wrapper for test isolation.
// This suppresses inherited beads env vars (BD_ACTOR, BEADS_DB) to prevent
// tests from accidentally routing to production databases.
func NewIsolated(workDir string) *Beads {
return &Beads{workDir: workDir, isolated: true}
}
// NewWithBeadsDir creates a Beads wrapper with an explicit BEADS_DIR.
// This is needed when running from a polecat worktree but accessing town-level beads.
func NewWithBeadsDir(workDir, beadsDir string) *Beads {
return &Beads{workDir: workDir, beadsDir: beadsDir}
}
// getActor returns the BD_ACTOR value for this context.
// Returns empty string when in isolated mode (tests) to prevent
// inherited actors from routing to production databases.
func (b *Beads) getActor() string {
if b.isolated {
return ""
}
return os.Getenv("BD_ACTOR")
}
// getTownRoot returns the Gas Town root directory, using lazy caching.
// The town root is found by walking up from workDir looking for mayor/town.json.
// Returns empty string if not in a Gas Town project.
func (b *Beads) getTownRoot() string {
if !b.searchedRoot {
b.townRoot = FindTownRoot(b.workDir)
b.searchedRoot = true
}
return b.townRoot
}
// getResolvedBeadsDir returns the beads directory this wrapper is operating on.
// This follows any redirects and returns the actual beads directory path.
func (b *Beads) getResolvedBeadsDir() string {
if b.beadsDir != "" {
return b.beadsDir
}
return ResolveBeadsDir(b.workDir)
}
// Init initializes a new beads database in the working directory.
// This uses the same environment isolation as other commands.
func (b *Beads) Init(prefix string) error {
_, err := b.run("init", "--prefix", prefix, "--quiet")
return err
}
// run executes a bd command and returns stdout.
func (b *Beads) run(args ...string) ([]byte, error) {
// Use --no-daemon for faster read operations (avoids daemon IPC overhead)
// The daemon is primarily useful for write coalescing, not reads.
// Use --allow-stale to prevent failures when db is out of sync with JSONL
// (e.g., after daemon is killed during shutdown before syncing).
fullArgs := append([]string{"--no-daemon", "--allow-stale"}, args...)
// Always explicitly set BEADS_DIR to prevent inherited env vars from
// causing prefix mismatches. Use explicit beadsDir if set, otherwise
// resolve from working directory.
beadsDir := b.beadsDir
if beadsDir == "" {
beadsDir = ResolveBeadsDir(b.workDir)
}
// In isolated mode, use --db flag to force specific database path
// This bypasses bd's routing logic that can redirect to .beads-planning
// Skip --db for init command since it creates the database
isInit := len(args) > 0 && args[0] == "init"
if b.isolated && !isInit {
beadsDB := filepath.Join(beadsDir, "beads.db")
fullArgs = append([]string{"--db", beadsDB}, fullArgs...)
}
cmd := exec.Command("bd", fullArgs...) //nolint:gosec // G204: bd is a trusted internal tool
cmd.Dir = b.workDir
// Build environment: filter beads env vars when in isolated mode (tests)
// to prevent routing to production databases.
var env []string
if b.isolated {
env = filterBeadsEnv(os.Environ())
} else {
env = os.Environ()
}
cmd.Env = append(env, "BEADS_DIR="+beadsDir)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return nil, b.wrapError(err, stderr.String(), args)
}
// Handle bd --no-daemon exit code 0 bug: when issue not found,
// --no-daemon exits 0 but writes error to stderr with empty stdout.
// Detect this case and treat as error to avoid JSON parse failures.
if stdout.Len() == 0 && stderr.Len() > 0 {
return nil, b.wrapError(fmt.Errorf("command produced no output"), stderr.String(), args)
}
return stdout.Bytes(), nil
}
// Run executes a bd command and returns stdout.
// This is a public wrapper around the internal run method for cases where
// callers need to run arbitrary bd commands.
func (b *Beads) Run(args ...string) ([]byte, error) {
return b.run(args...)
}
// wrapError wraps bd errors with context.
// ZFC: Avoid parsing stderr to make decisions. Transport errors to agents instead.
// Exception: ErrNotInstalled (exec.ErrNotFound) and ErrNotFound (issue lookup) are
// acceptable as they enable basic error handling without decision-making.
func (b *Beads) wrapError(err error, stderr string, args []string) error {
stderr = strings.TrimSpace(stderr)
// Check for bd not installed
if execErr, ok := err.(*exec.Error); ok && errors.Is(execErr.Err, exec.ErrNotFound) {
return ErrNotInstalled
}
// ErrNotFound is widely used for issue lookups - acceptable exception
// Match various "not found" error patterns from bd
if strings.Contains(stderr, "not found") || strings.Contains(stderr, "Issue not found") ||
strings.Contains(stderr, "no issue found") {
return ErrNotFound
}
if stderr != "" {
return fmt.Errorf("bd %s: %s", strings.Join(args, " "), stderr)
}
return fmt.Errorf("bd %s: %w", strings.Join(args, " "), err)
}
// filterBeadsEnv removes beads-related environment variables from the given
// environment slice. This ensures test isolation by preventing inherited
// BD_ACTOR, BEADS_DB, GT_ROOT, HOME etc. from routing commands to production databases.
func filterBeadsEnv(environ []string) []string {
filtered := make([]string, 0, len(environ))
for _, env := range environ {
// Skip beads-related env vars that could interfere with test isolation
// BD_ACTOR, BEADS_* - direct beads config
// GT_ROOT - causes bd to find global routes file
// HOME - causes bd to find ~/.beads-planning routing
if strings.HasPrefix(env, "BD_ACTOR=") ||
strings.HasPrefix(env, "BEADS_") ||
strings.HasPrefix(env, "GT_ROOT=") ||
strings.HasPrefix(env, "HOME=") {
continue
}
filtered = append(filtered, env)
}
return filtered
}
// List returns issues matching the given options.
func (b *Beads) List(opts ListOptions) ([]*Issue, error) {
args := []string{"list", "--json"}
if opts.Status != "" {
args = append(args, "--status="+opts.Status)
}
// Prefer Label over Type (Type is deprecated)
if opts.Label != "" {
args = append(args, "--label="+opts.Label)
} else if opts.Type != "" {
// Deprecated: convert type to label for backward compatibility
args = append(args, "--label=gt:"+opts.Type)
}
if opts.Priority >= 0 {
args = append(args, fmt.Sprintf("--priority=%d", opts.Priority))
}
if opts.Parent != "" {
args = append(args, "--parent="+opts.Parent)
}
if opts.Assignee != "" {
args = append(args, "--assignee="+opts.Assignee)
}
if opts.NoAssignee {
args = append(args, "--no-assignee")
}
out, err := b.run(args...)
if err != nil {
return nil, err
}
var issues []*Issue
if err := json.Unmarshal(out, &issues); err != nil {
return nil, fmt.Errorf("parsing bd list output: %w", err)
}
return issues, nil
}
// ListByAssignee returns all issues assigned to a specific assignee.
// The assignee is typically in the format "rig/polecatName" (e.g., "gastown/Toast").
func (b *Beads) ListByAssignee(assignee string) ([]*Issue, error) {
return b.List(ListOptions{
Status: "all", // Include both open and closed for state derivation
Assignee: assignee,
Priority: -1, // No priority filter
})
}
// GetAssignedIssue returns the first open issue assigned to the given assignee.
// Returns nil if no open issue is assigned.
func (b *Beads) GetAssignedIssue(assignee string) (*Issue, error) {
issues, err := b.List(ListOptions{
Status: "open",
Assignee: assignee,
Priority: -1,
})
if err != nil {
return nil, err
}
// Also check in_progress status explicitly
if len(issues) == 0 {
issues, err = b.List(ListOptions{
Status: "in_progress",
Assignee: assignee,
Priority: -1,
})
if err != nil {
return nil, err
}
}
if len(issues) == 0 {
return nil, nil
}
return issues[0], nil
}
// Ready returns issues that are ready to work (not blocked).
func (b *Beads) Ready() ([]*Issue, error) {
out, err := b.run("ready", "--json")
if err != nil {
return nil, err
}
var issues []*Issue
if err := json.Unmarshal(out, &issues); err != nil {
return nil, fmt.Errorf("parsing bd ready output: %w", err)
}
return issues, nil
}
// ReadyWithType returns ready issues filtered by label.
// Uses bd ready --label flag for server-side filtering.
// The issueType is converted to a gt:<type> label (e.g., "molecule" -> "gt:molecule").
func (b *Beads) ReadyWithType(issueType string) ([]*Issue, error) {
out, err := b.run("ready", "--json", "--label", "gt:"+issueType, "-n", "100")
if err != nil {
return nil, err
}
var issues []*Issue
if err := json.Unmarshal(out, &issues); err != nil {
return nil, fmt.Errorf("parsing bd ready output: %w", err)
}
return issues, nil
}
// Show returns detailed information about an issue.
func (b *Beads) Show(id string) (*Issue, error) {
out, err := b.run("show", id, "--json")
if err != nil {
return nil, err
}
// bd show --json returns an array with one element
var issues []*Issue
if err := json.Unmarshal(out, &issues); err != nil {
return nil, fmt.Errorf("parsing bd show output: %w", err)
}
if len(issues) == 0 {
return nil, ErrNotFound
}
return issues[0], nil
}
// ShowMultiple fetches multiple issues by ID in a single bd call.
// Returns a map of ID to Issue. Missing IDs are not included in the map.
func (b *Beads) ShowMultiple(ids []string) (map[string]*Issue, error) {
if len(ids) == 0 {
return make(map[string]*Issue), nil
}
// bd show supports multiple IDs
args := append([]string{"show", "--json"}, ids...)
out, err := b.run(args...)
if err != nil {
// If bd fails, return empty map (some IDs might not exist)
return make(map[string]*Issue), nil
}
var issues []*Issue
if err := json.Unmarshal(out, &issues); err != nil {
return nil, fmt.Errorf("parsing bd show output: %w", err)
}
result := make(map[string]*Issue, len(issues))
for _, issue := range issues {
result[issue.ID] = issue
}
return result, nil
}
// Blocked returns issues that are blocked by dependencies.
func (b *Beads) Blocked() ([]*Issue, error) {
out, err := b.run("blocked", "--json")
if err != nil {
return nil, err
}
var issues []*Issue
if err := json.Unmarshal(out, &issues); err != nil {
return nil, fmt.Errorf("parsing bd blocked output: %w", err)
}
return issues, nil
}
// Create creates a new issue and returns it.
// If opts.Actor is empty, it defaults to the BD_ACTOR environment variable.
// This ensures created_by is populated for issue provenance tracking.
func (b *Beads) Create(opts CreateOptions) (*Issue, error) {
args := []string{"create", "--json"}
if opts.Title != "" {
args = append(args, "--title="+opts.Title)
}
// Type is deprecated: convert to gt:<type> label
if opts.Type != "" {
args = append(args, "--labels=gt:"+opts.Type)
}
if opts.Priority >= 0 {
args = append(args, fmt.Sprintf("--priority=%d", opts.Priority))
}
if opts.Description != "" {
args = append(args, "--description="+opts.Description)
}
if opts.Parent != "" {
args = append(args, "--parent="+opts.Parent)
}
if opts.Ephemeral {
args = append(args, "--ephemeral")
}
// Default Actor from BD_ACTOR env var if not specified
// Uses getActor() to respect isolated mode (tests)
actor := opts.Actor
if actor == "" {
actor = b.getActor()
}
if actor != "" {
args = append(args, "--actor="+actor)
}
out, err := b.run(args...)
if err != nil {
return nil, err
}
var issue Issue
if err := json.Unmarshal(out, &issue); err != nil {
return nil, fmt.Errorf("parsing bd create output: %w", err)
}
return &issue, nil
}
// CreateWithID creates an issue with a specific ID.
// This is useful for agent beads, role beads, and other beads that need
// deterministic IDs rather than auto-generated ones.
func (b *Beads) CreateWithID(id string, opts CreateOptions) (*Issue, error) {
args := []string{"create", "--json", "--id=" + id}
if NeedsForceForID(id) {
args = append(args, "--force")
}
if opts.Title != "" {
args = append(args, "--title="+opts.Title)
}
// Type is deprecated: convert to gt:<type> label
if opts.Type != "" {
args = append(args, "--labels=gt:"+opts.Type)
}
if opts.Priority >= 0 {
args = append(args, fmt.Sprintf("--priority=%d", opts.Priority))
}
if opts.Description != "" {
args = append(args, "--description="+opts.Description)
}
if opts.Parent != "" {
args = append(args, "--parent="+opts.Parent)
}
// Default Actor from BD_ACTOR env var if not specified
// Uses getActor() to respect isolated mode (tests)
actor := opts.Actor
if actor == "" {
actor = b.getActor()
}
if actor != "" {
args = append(args, "--actor="+actor)
}
out, err := b.run(args...)
if err != nil {
return nil, err
}
var issue Issue
if err := json.Unmarshal(out, &issue); err != nil {
return nil, fmt.Errorf("parsing bd create output: %w", err)
}
return &issue, nil
}
// Update updates an existing issue.
func (b *Beads) Update(id string, opts UpdateOptions) error {
args := []string{"update", id}
if opts.Title != nil {
args = append(args, "--title="+*opts.Title)
}
if opts.Status != nil {
args = append(args, "--status="+*opts.Status)
}
if opts.Priority != nil {
args = append(args, fmt.Sprintf("--priority=%d", *opts.Priority))
}
if opts.Description != nil {
args = append(args, "--description="+*opts.Description)
}
if opts.Assignee != nil {
args = append(args, "--assignee="+*opts.Assignee)
}
// Label operations: set-labels replaces all, otherwise use add/remove
if len(opts.SetLabels) > 0 {
for _, label := range opts.SetLabels {
args = append(args, "--set-labels="+label)
}
} else {
for _, label := range opts.AddLabels {
args = append(args, "--add-label="+label)
}
for _, label := range opts.RemoveLabels {
args = append(args, "--remove-label="+label)
}
}
_, err := b.run(args...)
return err
}
// Close closes one or more issues.
// If a runtime session ID is set in the environment, it is passed to bd close
// for work attribution tracking (see decision 009-session-events-architecture.md).
func (b *Beads) Close(ids ...string) error {
if len(ids) == 0 {
return nil
}
args := append([]string{"close"}, ids...)
// Pass session ID for work attribution if available
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
args = append(args, "--session="+sessionID)
}
_, err := b.run(args...)
return err
}
// CloseWithReason closes one or more issues with a reason.
// If a runtime session ID is set in the environment, it is passed to bd close
// for work attribution tracking (see decision 009-session-events-architecture.md).
func (b *Beads) CloseWithReason(reason string, ids ...string) error {
if len(ids) == 0 {
return nil
}
args := append([]string{"close"}, ids...)
args = append(args, "--reason="+reason)
// Pass session ID for work attribution if available
if sessionID := runtime.SessionIDFromEnv(); sessionID != "" {
args = append(args, "--session="+sessionID)
}
_, err := b.run(args...)
return err
}
// Release moves an in_progress issue back to open status.
// This is used to recover stuck steps when a worker dies mid-task.
// It clears the assignee so the step can be claimed by another worker.
func (b *Beads) Release(id string) error {
return b.ReleaseWithReason(id, "")
}
// ReleaseWithReason moves an in_progress issue back to open status with a reason.
// The reason is added as a note to the issue for tracking purposes.
func (b *Beads) ReleaseWithReason(id, reason string) error {
args := []string{"update", id, "--status=open", "--assignee="}
// Add reason as a note if provided
if reason != "" {
args = append(args, "--notes=Released: "+reason)
}
_, err := b.run(args...)
return err
}
// AddDependency adds a dependency: issue depends on dependsOn.
func (b *Beads) AddDependency(issue, dependsOn string) error {
_, err := b.run("dep", "add", issue, dependsOn)
return err
}
// RemoveDependency removes a dependency.
func (b *Beads) RemoveDependency(issue, dependsOn string) error {
_, err := b.run("dep", "remove", issue, dependsOn)
return err
}
// Sync syncs beads with remote.
func (b *Beads) Sync() error {
_, err := b.run("sync")
return err
}
// SyncFromMain syncs beads updates from main branch.
func (b *Beads) SyncFromMain() error {
_, err := b.run("sync", "--from-main")
return err
}
// GetSyncStatus returns the sync status without performing a sync.
func (b *Beads) GetSyncStatus() (*SyncStatus, error) {
out, err := b.run("sync", "--status", "--json")
if err != nil {
// If sync branch doesn't exist, return empty status
if strings.Contains(err.Error(), "does not exist") {
return &SyncStatus{}, nil
}
return nil, err
}
var status SyncStatus
if err := json.Unmarshal(out, &status); err != nil {
return nil, fmt.Errorf("parsing bd sync status output: %w", err)
}
return &status, nil
}
// Stats returns repository statistics.
func (b *Beads) Stats() (string, error) {
out, err := b.run("stats")
if err != nil {
return "", err
}
return string(out), nil
}
// IsBeadsRepo checks if the working directory is a beads repository.
// ZFC: Check file existence directly instead of parsing bd errors.
func (b *Beads) IsBeadsRepo() bool {
beadsDir := ResolveBeadsDir(b.workDir)
info, err := os.Stat(beadsDir)
return err == nil && info.IsDir()
}
// primeContent is the Gas Town PRIME.md content that provides essential context
// for crew workers. This is the fallback if the SessionStart hook fails.
const primeContent = `# Gas Town Worker Context
> **Context Recovery**: Run ` + "`gt prime`" + ` for full context after compaction or new session.
## The Propulsion Principle (GUPP)
**If you find work on your hook, YOU RUN IT.**
No confirmation. No waiting. No announcements. The hook having work IS the assignment.
This is physics, not politeness. Gas Town is a steam engine - you are a piston.
**Failure mode we're preventing:**
- Agent starts with work on hook
- Agent announces itself and waits for human to say "ok go"
- Human is AFK / trusting the engine to run
- Work sits idle. The whole system stalls.
## Startup Protocol
1. Check your hook: ` + "`gt mol status`" + `
2. If work is hooked → EXECUTE (no announcement, no waiting)
3. If hook empty → Check mail: ` + "`gt mail inbox`" + `
4. Still nothing? Wait for user instructions
## Key Commands
- ` + "`gt prime`" + ` - Get full role context (run after compaction)
- ` + "`gt mol status`" + ` - Check your hooked work
- ` + "`gt mail inbox`" + ` - Check for messages
- ` + "`bd ready`" + ` - Find available work (no blockers)
## Session Close Protocol
Before signaling completion:
1. git status (check what changed)
2. git add <files> (stage code changes)
3. git commit -m "..." (commit code)
4. git push (push to remote)
5. ` + "`gt done`" + ` (submit to merge queue and exit)
**Polecats MUST call ` + "`gt done`" + ` - this submits work and exits the session.**
`
// ProvisionPrimeMD writes the Gas Town PRIME.md file to the specified beads directory.
// This provides essential Gas Town context (GUPP, startup protocol) as a fallback
// if the SessionStart hook fails. The PRIME.md is read by bd prime.
//
// The beadsDir should be the actual beads directory (after following any redirect).
// Returns nil if PRIME.md already exists (idempotent).
func ProvisionPrimeMD(beadsDir string) error {
primePath := filepath.Join(beadsDir, "PRIME.md")
// Check if already exists - don't overwrite customizations
if _, err := os.Stat(primePath); err == nil {
return nil // Already exists, don't overwrite
}
// Create .beads directory if it doesn't exist
if err := os.MkdirAll(beadsDir, 0755); err != nil {
return fmt.Errorf("creating beads dir: %w", err)
}
// Write PRIME.md
if err := os.WriteFile(primePath, []byte(primeContent), 0644); err != nil {
return fmt.Errorf("writing PRIME.md: %w", err)
}
return nil
}
// ProvisionPrimeMDForWorktree provisions PRIME.md for a worktree by following its redirect.
// This is the main entry point for crew/polecat provisioning.
func ProvisionPrimeMDForWorktree(worktreePath string) error {
// Resolve the beads directory (follows redirect chain)
beadsDir := ResolveBeadsDir(worktreePath)
// Provision PRIME.md in the target directory
return ProvisionPrimeMD(beadsDir)
}