diff --git a/.beads/config.yaml b/.beads/config.yaml index 07a1b152..f55759c7 100644 --- a/.beads/config.yaml +++ b/.beads/config.yaml @@ -62,12 +62,6 @@ # - github.repo sync-branch: beads-sync -# Gas Town custom types (bd-t5o8i) -# These types are used by Gas Town infrastructure but are not core beads types. -# They will be removed from beads built-in types (bd-find4) and configured here instead. -types: - custom: "molecule,gate,convoy,merge-request,slot,agent,role,rig,event,message" - # Cross-project dependencies (gt-o3is) # Maps project names to paths for external dependency resolution # Format: external:: in bd dep commands diff --git a/.beads/formulas/mol-backoff-test.formula.toml b/.beads/formulas/mol-backoff-test.formula.toml deleted file mode 100644 index a8245adf..00000000 --- a/.beads/formulas/mol-backoff-test.formula.toml +++ /dev/null @@ -1,95 +0,0 @@ -description = """ -Test molecule for verifying exponential backoff behavior. - -Uses SHORT intervals (2s base, 10s max) so you can observe multiple -backoff cycles in under a minute. - -Expected backoff progression: -- Cycle 0: 2s -- Cycle 1: 4s -- Cycle 2: 8s -- Cycle 3+: 10s (capped) - -Full backoff cycle observable in ~24 seconds. - -## Running This Test - -1. Create a test agent bead (if not exists): - ```bash - bd create --type=agent --title="Backoff Test Agent" --id=hq-backoff-test - ``` - -2. Run the deacon with this formula, or execute steps manually: - ```bash - gt mol step await-signal --agent-bead hq-backoff-test \ - --backoff-base 2s --backoff-mult 2 --backoff-max 10s - ``` - -3. Watch the idle counter increment on each timeout: - ```bash - bd show hq-backoff-test --json | jq '.[] | .labels' - ``` - -4. Trigger activity to test signal wake: - ```bash - bd create --title="Wake signal" --silent # Any bd command triggers activity - ``` - -5. Reset idle counter after signal (caller responsibility): - ```bash - bd update hq-backoff-test --set-labels=idle:0 - ``` -""" -formula = "mol-backoff-test" -version = 1 - -[[steps]] -id = "heartbeat" -title = "Touch heartbeat" -description = """ -Touch heartbeat to show we're alive. - -```bash -gt deacon heartbeat "backoff test cycle" -``` - -This updates the deacon's heartbeat file to signal liveness. -""" - -[[steps]] -id = "await-signal" -title = "Wait with exponential backoff (SHORT intervals)" -needs = ["heartbeat"] -description = """ -Wait for activity with exponential backoff using TEST intervals. - -**IMPORTANT**: This uses SHORT intervals for testing: -- Base: 2s (production: 60s) -- Multiplier: 2 -- Max: 10s (production: 10m) - -```bash -gt mol step await-signal --agent-bead hq-backoff-test \ - --backoff-base 2s --backoff-mult 2 --backoff-max 10s -``` - -**Expected behavior:** - -| Idle Cycles | Timeout | -|-------------|---------| -| 0 | 2s | -| 1 | 4s | -| 2 | 8s | -| 3+ | 10s | - -**On timeout**: The command auto-increments the `idle:N` label on the agent bead. -Continue to the next patrol cycle (loop back to heartbeat). - -**On signal**: Activity was detected. Reset the idle counter: -```bash -bd update hq-backoff-test --set-labels=idle:0 -``` -Then loop back to heartbeat for the next cycle. - -**To exit**: When context is high or testing is complete, exit cleanly. -""" diff --git a/CHANGELOG.md b/CHANGELOG.md index aac01ef8..05df8245 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.4.0] - 2026-01-17 + +### Fixed + +- **Orphan cleanup skips valid tmux sessions** - `gt orphans kill` and automatic orphan cleanup now check for Claude processes belonging to valid Gas Town tmux sessions (gt-*/hq-*) before killing. This prevents false kills of witnesses, refineries, and deacon during startup when they may temporarily show TTY "?" + ## [0.3.1] - 2026-01-17 ### Fixed diff --git a/internal/cmd/info.go b/internal/cmd/info.go index 66a7f6f6..998804e5 100644 --- a/internal/cmd/info.go +++ b/internal/cmd/info.go @@ -74,6 +74,13 @@ type VersionChange struct { // versionChanges contains agent-actionable changes for recent versions var versionChanges = []VersionChange{ + { + Version: "0.4.0", + Date: "2026-01-17", + Changes: []string{ + "FIX: Orphan cleanup skips valid tmux sessions - Prevents false kills of witnesses/refineries/deacon during startup by checking gt-*/hq-* session membership", + }, + }, { Version: "0.3.1", Date: "2026-01-17", diff --git a/internal/cmd/molecule_await_signal.go b/internal/cmd/molecule_await_signal.go index b26bd2ac..696a1c33 100644 --- a/internal/cmd/molecule_await_signal.go +++ b/internal/cmd/molecule_await_signal.go @@ -237,10 +237,8 @@ func calculateEffectiveTimeout(idleCycles int) (time.Duration, error) { // waitForActivitySignal starts bd activity --follow and waits for any output. // Returns immediately when a line is received, or when context is canceled. func waitForActivitySignal(ctx context.Context, workDir string) (*AwaitSignalResult, error) { - // Start bd activity --follow --since 1s - // The --since flag ensures we only wait for NEW events, not historical ones. - // Without this, the feed would immediately return old activity and never timeout. - cmd := exec.CommandContext(ctx, "bd", "activity", "--follow", "--since", "1s") + // Start bd activity --follow + cmd := exec.CommandContext(ctx, "bd", "activity", "--follow") cmd.Dir = workDir stdout, err := cmd.StdoutPipe() diff --git a/internal/cmd/statusline.go b/internal/cmd/statusline.go index 6a086f50..987a1259 100644 --- a/internal/cmd/statusline.go +++ b/internal/cmd/statusline.go @@ -184,10 +184,9 @@ func runMayorStatusLine(t *tmux.Tmux) error { // Track per-rig status for LED indicators and sorting type rigStatus struct { - hasWitness bool - hasRefinery bool - polecatCount int - opState string // "OPERATIONAL", "PARKED", or "DOCKED" + hasWitness bool + hasRefinery bool + opState string // "OPERATIONAL", "PARKED", or "DOCKED" } rigStatuses := make(map[string]*rigStatus) @@ -202,10 +201,8 @@ func runMayorStatusLine(t *tmux.Tmux) error { working int } healthByType := map[AgentType]*agentHealth{ - AgentPolecat: {}, AgentWitness: {}, AgentRefinery: {}, - AgentDeacon: {}, } // Single pass: track rig status AND agent health @@ -215,7 +212,8 @@ func runMayorStatusLine(t *tmux.Tmux) error { continue } - // Track rig-level status (witness/refinery/polecat presence) + // Track rig-level status (witness/refinery presence) + // Polecats are not tracked in tmux - they're a GC concern, not a display concern if agent.Rig != "" && registeredRigs[agent.Rig] { if rigStatuses[agent.Rig] == nil { rigStatuses[agent.Rig] = &rigStatus{} @@ -225,8 +223,6 @@ func runMayorStatusLine(t *tmux.Tmux) error { rigStatuses[agent.Rig].hasWitness = true case AgentRefinery: rigStatuses[agent.Rig].hasRefinery = true - case AgentPolecat: - rigStatuses[agent.Rig].polecatCount++ } } @@ -254,9 +250,10 @@ func runMayorStatusLine(t *tmux.Tmux) error { var parts []string // Add per-agent-type health in consistent order - // Format: "1/10 😺" = 1 working out of 10 total + // Format: "1/3 👁️" = 1 working out of 3 total // Only show agent types that have sessions - agentOrder := []AgentType{AgentPolecat, AgentWitness, AgentRefinery, AgentDeacon} + // Note: Polecats and Deacon excluded - idle state display is misleading noise + agentOrder := []AgentType{AgentWitness, AgentRefinery} var agentParts []string for _, agentType := range agentOrder { health := healthByType[agentType] @@ -287,7 +284,7 @@ func runMayorStatusLine(t *tmux.Tmux) error { rigs = append(rigs, rigInfo{name: rigName, status: status}) } - // Sort by: 1) running state, 2) polecat count (desc), 3) operational state, 4) alphabetical + // Sort by: 1) running state, 2) operational state, 3) alphabetical sort.Slice(rigs, func(i, j int) bool { isRunningI := rigs[i].status.hasWitness || rigs[i].status.hasRefinery isRunningJ := rigs[j].status.hasWitness || rigs[j].status.hasRefinery @@ -297,12 +294,7 @@ func runMayorStatusLine(t *tmux.Tmux) error { return isRunningI } - // Secondary sort: polecat count (descending) - if rigs[i].status.polecatCount != rigs[j].status.polecatCount { - return rigs[i].status.polecatCount > rigs[j].status.polecatCount - } - - // Tertiary sort: operational state (for non-running rigs: OPERATIONAL < PARKED < DOCKED) + // Secondary sort: operational state (for non-running rigs: OPERATIONAL < PARKED < DOCKED) stateOrder := map[string]int{"OPERATIONAL": 0, "PARKED": 1, "DOCKED": 2} stateI := stateOrder[rigs[i].status.opState] stateJ := stateOrder[rigs[j].status.opState] @@ -310,7 +302,7 @@ func runMayorStatusLine(t *tmux.Tmux) error { return stateI < stateJ } - // Quaternary sort: alphabetical + // Tertiary sort: alphabetical return rigs[i].name < rigs[j].name }) @@ -352,17 +344,12 @@ func runMayorStatusLine(t *tmux.Tmux) error { } } - // Show polecat count if > 0 // All icons get 1 space, Park gets 2 space := " " if led == "🅿️" { space = " " } - display := led + space + rig.name - if status.polecatCount > 0 { - display += fmt.Sprintf("(%d)", status.polecatCount) - } - rigParts = append(rigParts, display) + rigParts = append(rigParts, led+space+rig.name) } if len(rigParts) > 0 { @@ -421,7 +408,6 @@ func runDeaconStatusLine(t *tmux.Tmux) error { } rigs := make(map[string]bool) - polecatCount := 0 for _, s := range sessions { agent := categorizeSession(s) if agent == nil { @@ -431,16 +417,13 @@ func runDeaconStatusLine(t *tmux.Tmux) error { if agent.Rig != "" && registeredRigs[agent.Rig] { rigs[agent.Rig] = true } - if agent.Type == AgentPolecat && registeredRigs[agent.Rig] { - polecatCount++ - } } rigCount := len(rigs) // Build status + // Note: Polecats excluded - they're ephemeral and idle detection is a GC concern var parts []string parts = append(parts, fmt.Sprintf("%d rigs", rigCount)) - parts = append(parts, fmt.Sprintf("%d 😺", polecatCount)) // Priority 1: Check for hooked work (town beads for deacon) hookedWork := "" @@ -466,7 +449,8 @@ func runDeaconStatusLine(t *tmux.Tmux) error { } // runWitnessStatusLine outputs status for a witness session. -// Shows: polecat count, crew count, hook or mail preview +// Shows: crew count, hook or mail preview +// Note: Polecats excluded - they're ephemeral and idle detection is a GC concern func runWitnessStatusLine(t *tmux.Tmux, rigName string) error { if rigName == "" { // Try to extract from session name: gt--witness @@ -483,25 +467,20 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error { townRoot, _ = workspace.Find(paneDir) } - // Count polecats and crew in this rig + // Count crew in this rig (crew are persistent, worth tracking) sessions, err := t.ListSessions() if err != nil { return nil // Silent fail } - polecatCount := 0 crewCount := 0 for _, s := range sessions { agent := categorizeSession(s) if agent == nil { continue } - if agent.Rig == rigName { - if agent.Type == AgentPolecat { - polecatCount++ - } else if agent.Type == AgentCrew { - crewCount++ - } + if agent.Rig == rigName && agent.Type == AgentCrew { + crewCount++ } } @@ -509,7 +488,6 @@ func runWitnessStatusLine(t *tmux.Tmux, rigName string) error { // Build status var parts []string - parts = append(parts, fmt.Sprintf("%d 😺", polecatCount)) if crewCount > 0 { parts = append(parts, fmt.Sprintf("%d crew", crewCount)) } diff --git a/internal/cmd/version.go b/internal/cmd/version.go index abc7345b..37c82db4 100644 --- a/internal/cmd/version.go +++ b/internal/cmd/version.go @@ -12,7 +12,7 @@ import ( // Version information - set at build time via ldflags var ( - Version = "0.3.1" + Version = "0.4.0" // Build can be set via ldflags at compile time Build = "dev" // Commit and Branch - the git revision the binary was built from (optional ldflag) diff --git a/internal/formula/formulas/mol-backoff-test.formula.toml b/internal/formula/formulas/mol-backoff-test.formula.toml deleted file mode 100644 index a8245adf..00000000 --- a/internal/formula/formulas/mol-backoff-test.formula.toml +++ /dev/null @@ -1,95 +0,0 @@ -description = """ -Test molecule for verifying exponential backoff behavior. - -Uses SHORT intervals (2s base, 10s max) so you can observe multiple -backoff cycles in under a minute. - -Expected backoff progression: -- Cycle 0: 2s -- Cycle 1: 4s -- Cycle 2: 8s -- Cycle 3+: 10s (capped) - -Full backoff cycle observable in ~24 seconds. - -## Running This Test - -1. Create a test agent bead (if not exists): - ```bash - bd create --type=agent --title="Backoff Test Agent" --id=hq-backoff-test - ``` - -2. Run the deacon with this formula, or execute steps manually: - ```bash - gt mol step await-signal --agent-bead hq-backoff-test \ - --backoff-base 2s --backoff-mult 2 --backoff-max 10s - ``` - -3. Watch the idle counter increment on each timeout: - ```bash - bd show hq-backoff-test --json | jq '.[] | .labels' - ``` - -4. Trigger activity to test signal wake: - ```bash - bd create --title="Wake signal" --silent # Any bd command triggers activity - ``` - -5. Reset idle counter after signal (caller responsibility): - ```bash - bd update hq-backoff-test --set-labels=idle:0 - ``` -""" -formula = "mol-backoff-test" -version = 1 - -[[steps]] -id = "heartbeat" -title = "Touch heartbeat" -description = """ -Touch heartbeat to show we're alive. - -```bash -gt deacon heartbeat "backoff test cycle" -``` - -This updates the deacon's heartbeat file to signal liveness. -""" - -[[steps]] -id = "await-signal" -title = "Wait with exponential backoff (SHORT intervals)" -needs = ["heartbeat"] -description = """ -Wait for activity with exponential backoff using TEST intervals. - -**IMPORTANT**: This uses SHORT intervals for testing: -- Base: 2s (production: 60s) -- Multiplier: 2 -- Max: 10s (production: 10m) - -```bash -gt mol step await-signal --agent-bead hq-backoff-test \ - --backoff-base 2s --backoff-mult 2 --backoff-max 10s -``` - -**Expected behavior:** - -| Idle Cycles | Timeout | -|-------------|---------| -| 0 | 2s | -| 1 | 4s | -| 2 | 8s | -| 3+ | 10s | - -**On timeout**: The command auto-increments the `idle:N` label on the agent bead. -Continue to the next patrol cycle (loop back to heartbeat). - -**On signal**: Activity was detected. Reset the idle counter: -```bash -bd update hq-backoff-test --set-labels=idle:0 -``` -Then loop back to heartbeat for the next cycle. - -**To exit**: When context is high or testing is complete, exit cleanly. -""" diff --git a/npm-package/package.json b/npm-package/package.json index 18e475bc..3bf7a2a7 100644 --- a/npm-package/package.json +++ b/npm-package/package.json @@ -1,6 +1,6 @@ { "name": "@gastown/gt", - "version": "0.3.0", + "version": "0.4.0", "description": "Gas Town CLI - multi-agent workspace manager with native binary support", "main": "bin/gt.js", "bin": {