fix(handoff): use env var fallback when town root detection fails

When the repo is in a broken state (wrong branch, detached HEAD, deleted
worktree), gt handoff would fail with "cannot detect town root" error.
This is exactly when handoff is most needed - to recover and hand off
to a fresh session.

Changes:
- detectTownRootFromCwd() now falls back to GT_TOWN_ROOT and GT_ROOT
  environment variables when cwd-based detection fails
- buildRestartCommand() now propagates GT_ROOT to ensure subsequent
  handoffs can also use the fallback
- Added tests for the fallback behavior

Fixes gt-x2q81.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
morsov
2026-01-21 17:35:44 -08:00
committed by beads/crew/emma
parent f6fab3afad
commit 39b1c11bb6
2 changed files with 150 additions and 3 deletions

View File

@@ -411,6 +411,10 @@ func buildRestartCommand(sessionName string) (string, error) {
}
}
// Propagate GT_ROOT so subsequent handoffs can use it as fallback
// when cwd-based detection fails (broken state recovery)
exports = append(exports, "GT_ROOT="+townRoot)
// Preserve GT_AGENT across handoff so agent override persists
if currentAgent != "" {
exports = append(exports, "GT_AGENT="+currentAgent)
@@ -498,14 +502,33 @@ func sessionToGTRole(sessionName string) string {
}
// detectTownRootFromCwd walks up from the current directory to find the town root.
// Falls back to GT_TOWN_ROOT or GT_ROOT env vars if cwd detection fails (broken state recovery).
func detectTownRootFromCwd() string {
// Use workspace.FindFromCwd which handles both primary (mayor/town.json)
// and secondary (mayor/ directory) markers
townRoot, err := workspace.FindFromCwd()
if err != nil {
return ""
if err == nil && townRoot != "" {
return townRoot
}
return townRoot
// Fallback: try environment variables for town root
// GT_TOWN_ROOT is set by shell integration, GT_ROOT is set by session manager
// This enables handoff to work even when cwd detection fails due to
// detached HEAD, wrong branch, deleted worktree, etc.
for _, envName := range []string{"GT_TOWN_ROOT", "GT_ROOT"} {
if envRoot := os.Getenv(envName); envRoot != "" {
// Verify it's actually a workspace
if _, statErr := os.Stat(filepath.Join(envRoot, workspace.PrimaryMarker)); statErr == nil {
return envRoot
}
// Try secondary marker too
if info, statErr := os.Stat(filepath.Join(envRoot, workspace.SecondaryMarker)); statErr == nil && info.IsDir() {
return envRoot
}
}
}
return ""
}
// handoffRemoteSession respawns a different session and optionally switches to it.