When bd --no-daemon show <id> does not find an issue, it incorrectly exits with code 0 (success) but writes the error to stderr and leaves stdout empty. This causes JSON parse failures throughout gt when code tries to unmarshal the empty stdout. This PR handles the bug defensively in all affected code paths: - beads.go run(): Detect empty stdout + non-empty stderr as error - beads.go wrapError(): Add 'no issue found' to ErrNotFound patterns - sling.go: Check len(out) == 0 in multiple functions - convoy.go getIssueDetails(): Check stdout.Len() == 0 - prime_molecule.go: Check stdout.Len() == 0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
700 lines
21 KiB
Go
700 lines
21 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
|
|
RoleBead string `json:"role_bead,omitempty"` // Role definition bead (shared)
|
|
AgentState string `json:"agent_state,omitempty"` // Agent lifecycle state (spawning, working, done, stuck)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// New creates a new Beads wrapper for the given directory.
|
|
func New(workDir string) *Beads {
|
|
return &Beads{workDir: workDir}
|
|
}
|
|
|
|
// 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}
|
|
}
|
|
|
|
// 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
|
|
fullArgs := append([]string{"--no-daemon"}, args...)
|
|
cmd := exec.Command("bd", fullArgs...) //nolint:gosec // G204: bd is a trusted internal tool
|
|
cmd.Dir = b.workDir
|
|
|
|
// 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)
|
|
}
|
|
cmd.Env = append(os.Environ(), "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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
// Default Actor from BD_ACTOR env var if not specified
|
|
actor := opts.Actor
|
|
if actor == "" {
|
|
actor = os.Getenv("BD_ACTOR")
|
|
}
|
|
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 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
|
|
actor := opts.Actor
|
|
if actor == "" {
|
|
actor = os.Getenv("BD_ACTOR")
|
|
}
|
|
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)
|
|
- ` + "`bd sync`" + ` - Sync beads changes
|
|
|
|
## Session Close Protocol
|
|
|
|
Before saying "done":
|
|
1. git status (check what changed)
|
|
2. git add <files> (stage code changes)
|
|
3. bd sync (commit beads changes)
|
|
4. git commit -m "..." (commit code)
|
|
5. bd sync (commit any new beads changes)
|
|
6. git push (push to remote)
|
|
|
|
**Work is not done until pushed.**
|
|
`
|
|
|
|
// 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)
|
|
}
|