The pool state file was saving CustomNames even though Load() ignored
them (CustomNames come from settings/config.json). This caused the
state file to have stale/incorrect custom names data.
Changes:
- Create namePoolState struct for persisting only OverflowNext/MaxSize
- Save() now only writes runtime state, not configuration
- Load() uses the same struct for consistency
- Removed redundant runtime pool update from runNamepoolAdd since
the settings file is the source of truth for custom names
Fixes: gt-ofqzwv
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When gt down --all killed all Gas Town sessions, if those were the only
tmux sessions, the server would exit due to tmux's default exit-empty
setting. Users perceived this as gt down --all killed my tmux server.
Fix: Set exit-empty off before killing sessions, ensuring the server
stays running for subsequent gt up commands. The --nuke flag still
explicitly kills the server when requested.
Fixes: gt-kh8w47
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, `gt up` and `gt rig start` would start witnesses and
refineries for parked/docked rigs, bypassing the operational status
protection. Only the daemon respected the wisp config status.
Now both commands check wisp config status before starting agents:
- `gt up` shows "skipped (rig parked)" for parked/docked rigs
- `gt rig start` warns and skips parked/docked rigs
This prevents accidentally bringing parked/docked rigs back online
when running routine commands.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The CloneDivergenceCheck was calling git fetch for each clone without
a timeout, causing gt doctor to hang indefinitely when network or
authentication issues occurred. Removed the fetch - divergence detection
now uses existing local refs (may be stale but won't block).
Fixes: gt-aoklf8
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The bd dep add command was failing with only "exit status 1" shown
because stderr wasn't being captured. Now shows actual error message.
Fixes: gt-g8eqq5
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. Worktree directory cleanup used os.Remove() which only removes empty
directories. Changed to os.RemoveAll() to clean up untracked files
left behind by git worktree remove (overlay files, .beads/, etc.)
2. Branch deletion hardcoded mayor/rig but worktrees are created from
.repo.git when using bare repo architecture. Now checks for bare
repo first to match where the branch was created.
Fixes: gt-6ab3cm
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The gt namepool add command was replacing custom_names instead of
appending because it saved to the runtime state file, but Load()
intentionally ignores CustomNames from that file (expecting config
to come from settings/config.json).
Changes:
- runNamepoolAdd now loads existing settings, appends the new name,
and saves to settings/config.json (the source of truth)
- runNamepoolSet now preserves existing custom names when changing
themes (was passing nil which cleared them)
- Added duplicate check to avoid adding same name twice
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix 'add-issue' command to 'add' with correct syntax including convoy-id
- Add explanation that bead IDs and issue IDs are interchangeable terms
- Standardize convoy command parameters to match actual CLI help
Closes: gt-u7qb6p
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace placeholder issue-123 style IDs with realistic bead ID format
(prefix + 5-char alphanumeric, e.g., gt-abc12). Add explanation of bead
ID format in Beads Integration section. Update command references and
mermaid diagrams to use consistent "bead" terminology.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When running from a crew workspace, BEADS_DIR is set to the rig's beads
directory. This caused auto-convoy creation to fail because bd would use
the rig's database (prefix=bd) instead of discovering the HQ database
(prefix=hq) from the working directory.
The fix clears BEADS_DIR from the environment when running bd commands
for convoy creation, allowing bd to discover the correct database from
the townBeads directory.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Fixes#525: gt up reports deacon success but session doesn't actually start
Previously, WaitForCommand failures were marked as "non-fatal" in the
manager Start() methods used by gt up. This caused gt up to report
success even when Claude failed to start, because the error was silently
ignored.
Now when WaitForCommand or WaitForRuntimeReady times out:
1. The zombie tmux session is killed
2. An error is returned to the caller
3. gt up properly reports the failure
This aligns the manager Start() behavior with the cmd start functions
(e.g., gt deacon start) which already had fatal WaitForCommand behavior.
Changed files:
- internal/deacon/manager.go
- internal/mayor/manager.go
- internal/witness/manager.go
- internal/refinery/manager.go
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convoy beads use hq-cv-* IDs for visual distinction from other town beads.
The routes.jsonl entry was being added but allowed_prefixes config was not,
causing bd create --id=hq-cv-xxx to fail prefix validation.
This adds the allowed_prefixes config (hq,hq-cv) during initTownBeads so
convoy creation works out of the box after gt install.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Start() function was returning success even if the pane died during
initialization (e.g., if Claude failed to start). This caused the caller
to get a confusing "getting pane" error when trying to use the session.
Now Start() verifies the session is still running at the end, returning
a clear error message if the session died during startup.
Fixes: gt-0cif0s
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a polecat runs gt done, the worktree is removed but the parent
polecat directory could be left behind containing only .beads/. This
caused gt polecat list to show ghost entries since exists() checks
if the polecatDir exists.
The fix adds explicit cleanup of .beads/ directories:
1. After git worktree remove succeeds, clean up any leftover .beads/
in the clonePath that was not fully removed
2. For new structure polecats, also clean up any .beads/ at the
polecatDir level before trying to remove the parent directory
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds show subcommand to gt bead that delegates to gt show (which
delegates to bd show). This completes gt-zdwy58.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add prominent warnings about the mandatory gt done requirement:
- New 'THE IDLE POLECAT HERESY' section at top of both templates
- Emphasize that sitting idle after completing work is a critical failure
- Add MANDATORY labels to completion protocol sections
- Add final reminder section before metadata block
This addresses the bug where polecats complete work but don't run gt done,
sitting idle and wasting resources instead of properly shutting down.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The gt orphans kill command now performs a unified cleanup that removes
orphaned commits via git gc AND kills orphaned Claude processes in one
operation, with a single confirmation prompt.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pin bd (beads CLI) to v0.47.1 in CI workflows and fix test agent IDs
that trigger bd's isLikelyHash() prefix extraction logic.
Changes:
- Pin bd to v0.47.1 in ci.yml and integration.yml (v0.47.2 has routing
defaults that cause prefix mismatch errors)
- Fix TestCloseAndClearAgentBead_FieldClearing: change agent IDs from
`test-testrig-polecat-0` to `test-testrig-polecat-all_fields_populated`
- Fix TestCloseAndClearAgentBead_ReasonVariations: change agent IDs from
`test-testrig-polecat-reason0` to `test-testrig-polecat-empty_reason`
Root cause: bd v0.47.1's isLikelyHash() treats suffixes of 3-8 chars
(with digits for 4+ chars) as potential git hashes. Patterns like `-0`
(single digit) and `-reason0` (7 chars with digit) caused bd to extract
the wrong prefix from agent IDs.
Using test names as suffixes (e.g., `all_fields_populated`) avoids this
because they're all >8 characters and won't trigger hash detection.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Formula scaffold beads (created when formulas are installed) were
appearing as actionable work items in `gt ready`. These are template
beads, not actual work.
Add filtering to exclude issues whose ID:
- Matches a formula name exactly (e.g., "mol-deacon-patrol")
- Starts with "<formula-name>." (step scaffolds like "mol-deacon-patrol.inbox-check")
The fix reads the formulas directory to get installed formula names
and filters issues accordingly for both town and rig beads.
Fixes: gt-579
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: Add automatic orphaned claude process cleanup
Claude Code's Task tool spawns subagent processes that sometimes don't clean up
properly after completion. These accumulate and consume significant memory
(observed: 17 processes using ~6GB RAM).
This change adds automatic cleanup in two places:
1. **Deacon patrol** (primary): New patrol step "orphan-process-cleanup" runs
`gt deacon cleanup-orphans` early in each cycle. More responsive (~30s).
2. **Daemon heartbeat** (fallback): Runs cleanup every 3 minutes as safety net
when deacon is down.
Detection uses TTY column - processes with TTY "?" have no controlling terminal.
This is safe because:
- Processes in terminals (user sessions) have a TTY like "pts/0" - untouched
- Only kills processes with no controlling terminal
- Orphaned subagents are children of tmux server with no TTY
New files:
- internal/util/orphan.go: FindOrphanedClaudeProcesses, CleanupOrphanedClaudeProcesses
- internal/util/orphan_test.go: Tests for orphan detection
New command:
- `gt deacon cleanup-orphans`: Manual/patrol-triggered cleanup
Fixes#587
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(orphan): add Windows build tag and minimum age check
Addresses review feedback on PR #588:
1. Add //go:build !windows to orphan.go and orphan_test.go
- The code uses Unix-specific syscalls (SIGTERM, ESRCH) and
ps command options that don't exist on Windows
2. Add minimum age check (60 seconds) to prevent false positives
- Prevents race conditions with newly spawned subagents
- Addresses reviewer concern about cron/systemd processes
- Uses portable etime format instead of Linux-only etimes
3. Add parseEtime helper with comprehensive tests
- Parses [[DD-]HH:]MM:SS format (works on both Linux and macOS)
- etimes (seconds) is Linux-specific, etime is portable
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(orphan): add proper SIGTERM→SIGKILL escalation with state tracking
Previous approach used process age which doesn't work: a Task subagent
runs without TTY from birth, so a long-running legitimate subagent that
later fails to exit would be immediately SIGKILLed without trying SIGTERM.
New approach uses a state file to track signal history:
1. First encounter → SIGTERM, record PID + timestamp in state file
2. Next cycle (after 60s grace period) → if still alive, SIGKILL
3. Next cycle → if survived SIGKILL, log as unkillable and remove
State file: $XDG_RUNTIME_DIR/gastown-orphan-state (or /tmp/)
Format: "<pid> <signal> <unix_timestamp>" per line
The state file is automatically cleaned up:
- Dead processes removed on load
- Unkillable processes removed after logging
Also updates callers to use new CleanupResult type which includes
the signal sent (SIGTERM, SIGKILL, or UNKILLABLE).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes#566
The daemon spawned 812 refinery sessions over 4 days because:
1. Zombie detection was too strict - used IsAgentRunning(session, "node")
but Claude reports pane command as version number (e.g., "2.1.7"),
causing healthy sessions to be killed and recreated every heartbeat.
2. daemon.json patrol config was completely ignored - the daemon never
loaded or checked the enabled flags.
Changes:
- refinery/manager.go: Use IsClaudeRunning() instead of IsAgentRunning()
for robust Claude detection (handles "node", "claude", version patterns)
- daemon/types.go: Add PatrolConfig types and LoadPatrolConfig() to read
mayor/daemon.json
- daemon/daemon.go: Load patrol config at startup, check enabled flags
before calling ensureRefineriesRunning/ensureWitnessesRunning, add
diagnostic logging for "already running" cases
Tested: Verified over multiple heartbeats that refinery shows "already
running, skipping spawn" instead of spawning new sessions.
Co-authored-by: mayor <your-github-email@example.com>
Each rig now gets a deterministic theme based on its name instead of
always defaulting to mad-max. Uses a prime multiplier hash (×31) for
good distribution across themes. Same rig name always gets the same
theme. Users can still override with `gt namepool set`.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: stabilize beads and config tests
* fix: remove t.Parallel() incompatible with t.Setenv()
The test now uses t.Setenv() which cannot be used with t.Parallel() in Go.
This completes the conflict resolution from the rebase.
* style: fix gofmt issue in beads_test.go
Remove extra blank line in comment block.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- gt hook --clear: alias for 'gt unhook' (gt-eod2iv)
- gt close: wrapper for 'bd close' (gt-msak6o)
- gt bead move: move beads between repos (gt-dzdbr7)
These commands were natural guesses that agents tried but didn't exist.
Following the desire-paths approach to improve agent ergonomics.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When `gt rig add` fails due to GitHub password auth being disabled,
provide a helpful error message that:
- Explains that GitHub no longer supports password authentication
- Suggests the equivalent SSH URL for GitHub/GitLab repos
- Falls back to generic SSH suggestion for other hosts
Also adds tests for the URL conversion function.
Fixes#548
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When attaching to a session from within tmux, use 'tmux switch-client'
instead of 'tmux attach-session' to avoid the nested session error.
Fixes#603
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(daemon): prevent runaway refinery session spawning
Fixes#566
The daemon spawned 812 refinery sessions over 4 days because:
1. Zombie detection was too strict - used IsAgentRunning(session, "node")
but Claude reports pane command as version number (e.g., "2.1.7"),
causing healthy sessions to be killed and recreated every heartbeat.
2. daemon.json patrol config was completely ignored - the daemon never
loaded or checked the enabled flags.
Changes:
- refinery/manager.go: Use IsClaudeRunning() instead of IsAgentRunning()
for robust Claude detection (handles "node", "claude", version patterns)
- daemon/types.go: Add PatrolConfig types and LoadPatrolConfig() to read
mayor/daemon.json
- daemon/daemon.go: Load patrol config at startup, check enabled flags
before calling ensureRefineriesRunning/ensureWitnessesRunning, add
diagnostic logging for "already running" cases
Tested: Verified over multiple heartbeats that refinery shows "already
running, skipping spawn" instead of spawning new sessions.
* fix: Add grace period to prevent Deacon restart loop
The daemon had a race condition where:
1. ensureDeaconRunning() starts a new Deacon session
2. checkDeaconHeartbeat() runs in same heartbeat cycle
3. Heartbeat file is stale (from before crash)
4. Session is immediately killed
5. Infinite restart loop every 3 minutes
Fix:
- Track when Deacon was last started (deaconLastStarted field)
- Skip heartbeat check during 5-minute grace period
- Add config support for Deacon (consistency with refinery/witness)
After grace period, normal heartbeat checking resumes. Genuinely
stuck sessions (no heartbeat update after 5+ min) are still detected.
Fixes#589
---------
Co-authored-by: mayor <your-github-email@example.com>
When JSON parsing of inbox output fails, the code falls back to plain
text mode. However, the error from the fallback `gt mail inbox` command
was being silently ignored with `_`, masking failures and making
debugging difficult.
This change properly captures and returns the error if the fallback
command fails.
Co-authored-by: Gastown Bot <bot@gastown.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add CleanExcludingBeads() method that returns true if the only uncommitted
changes are .beads/ database files. These files are synced across worktrees
and shouldn't block polecat cleanup.
Fixes#516
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
KillSessionWithProcesses was only killing descendant processes,
assuming the session kill would terminate the pane process itself.
However, if the pane process (claude) calls setsid(), it detaches
from the controlling terminal and survives the session kill.
This fix explicitly kills the pane PID after killing descendants,
before killing the tmux session. This catches processes that have
escaped the process tree via setsid().
Fixes#513
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When multiple agents start simultaneously (e.g., `gt up`), each runs
`gt nudge deacon session-started` in their SessionStart hook. These
nudges arrive concurrently and can interleave in the tmux input buffer,
causing:
1. Text from one nudge mixing with another
2. Enter keys not properly submitting messages
3. Garbled input requiring manual intervention
This fix adds per-session mutex serialization to NudgeSession() and
NudgePane(). Concurrent nudges to the same session now queue and
execute one at a time.
## Root Cause
The NudgeSession pattern sends text, waits 500ms, sends Escape, waits
100ms, then sends Enter. When multiple nudges arrive within this ~800ms
window, their send-keys commands interleave, corrupting the input.
## Alternatives Considered
1. **Delay deacon nudges** - Add sleep before nudge in SessionStart
- Simplest (one-line change)
- But: doesn't prevent concurrent nudges from multiple agents
2. **Debounce session-started** - Deacon ignores rapid-fire nudges
- Medium complexity
- But: only helps session-started, not other nudge types
3. **File-based signaling** - Replace tmux nudges with file watches
- Avoids tmux input issues entirely
- But: significant architectural change
4. **File upstream bug** - Report to Claude Code team
- SessionStart hooks fire async and can interleave
- But: fix timeline unknown, need robustness now
## Tradeoffs
- Concurrent nudges to same session now queue (adds latency)
- Memory: one mutex per unique session name (bounded, acceptable)
- Does not fix Claude Code's underlying async hook behavior
## Testing
- Build passes
- All tmux package tests pass
- Manual testing: started deacon + multiple witnesses concurrently,
nudges processed correctly without garbled input
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add tests to verify that rig.Manager.AddRig correctly creates witness
and refinery agent beads via initAgentBeads. Also improve mock bd:
- Fix mock bd to handle --no-daemon --allow-stale global flags
- Return valid JSON for create commands with bead ID
- Log create commands for test verification
- Add TestRigAddCreatesAgentBeads integration test
- Add TestAgentBeadIDs unit test for bead ID generation
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(mq): skip closed MRs in list, next, and ready views (gt-qtb3w)
The gt mq list command with --status=open filter was incorrectly displaying
CLOSED merge requests as 'ready'. This occurred because bd list --status=open
was returning closed issues.
Added manual status filtering in three locations:
- mq_list.go: Filter closed MRs in all list views
- mq_next.go: Skip closed MRs when finding next ready MR
- engineer.go: Skip closed MRs in refinery's ready queue
Also fixed build error in mail_queue.go where QueueConfig struct (non-pointer)
was being compared to nil.
Workaround for upstream bd list status filter bug.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* style: fix gofmt issue in engineer.go comment block
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The help text claimed 'gt mail read' marks messages as read, but this
was intentionally removed in 71d313ed to preserve handoff messages.
Update the help text to accurately reflect the current behavior and
point users to 'gt mail mark-read' for explicit read marking.
Add validateIssue() to check that an issue exists and is not tombstoned
before creating the tmux session. This prevents CPU spin loops from
agents retrying work on invalid issues.
Fixes#569
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When gt doctor runs, it now detects and kills zombie sessions - tmux
sessions that are valid Gas Town sessions (gt-*, hq-*) but have no
Claude/node process running inside. These occur when Claude exits or
crashes but the tmux session remains.
Previously, OrphanSessionCheck only validated session names but did not
check if Claude was actually running. This left empty sessions
accumulating over time.
Fixes#472
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
WriteRoutes() would fail if the beads directory didn't exist yet.
Add os.MkdirAll before creating the routes file.
Fixes#552
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Formula scaffolds (beads with IDs starting with "mol-") are templates
created when formulas are installed, not actual work items. They were
incorrectly appearing in gt ready output as actionable work.
Fixes#579
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ListUnread() was returning all messages in beads mode instead of
filtering by the Read field. Apply the same filtering logic used
in legacy mode to both code paths.
Fixes#595
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The test was duplicating the icon selection logic in a switch statement
instead of calling the actual function being tested. Extract the icon
logic into getMigrationStatusIcon() and have the test call it directly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(beads): cache version check and add timeout to prevent cli lag
* fix(migrate_agents_test): fix icon expectations to match actual output
The printMigrationResult function uses icons with two leading spaces
(" ✓", " ⊘", " ✗") but the test expected icons without spaces.
This fixes the test expectations to match the actual output format.
When using `gt sling <formula> --on <bead>`, the wisp was bonded to the
target bead but the attached_molecule field wasn't being set in the
bead's description. This caused `gt hook` to report "No molecule
attached" even though the formula was correctly bonded.
Now both sling.go (--on mode) and sling_formula.go (standalone formula)
call storeAttachedMoleculeInBead() to record the molecule attachment
after wisp creation. This ensures gt hook can properly display molecule
progress.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Branch names like "polecat/furiosa-mkb0vq9f" don't contain the actual
issue ID, causing gt done to incorrectly parse "furiosa-mkb0vq9f" as the
issue. This broke integration branch auto-detection since the wrong issue
was used for parent epic lookup.
Changes:
- After parsing branch name, check the agent's hook_bead field which
contains the actual issue ID (e.g., "gt-845.1")
- Fix parseBranchName to not extract fake issue IDs from modern polecat branches
- Fix detectIntegrationBranch to traverse full parent chain (molecule → bug → epic)
- Include issue ID in polecat branch names when HookBead is set
Added tests covering:
- Agent hook returns correct issue ID
- Modern polecat branch format parsing
- Integration branch detection through parent chain
Fixes#411
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
IsSilentExit used type assertion which fails on wrapped errors.
Changed to errors.As to properly unwrap and detect SilentExitError.
Added test to verify wrapped error detection works.