Skip to content

Commit e9652d6

Browse files
committed
Merge polecat/nux: fix patrol wisp database routing (gt-ctir)
2 parents a19bb32 + 919616e commit e9652d6

4 files changed

Lines changed: 91 additions & 0 deletions

File tree

internal/beads/beads_agent.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ type AgentFields struct {
8484
// Agent state constants for the agent_state field in agent beads.
8585
// These represent the beads-level agent lifecycle state, which is distinct from
8686
// the polecat.State lifecycle type (though values overlap).
87+
// Note: AgentStateIdle and AgentStateStuck are declared above in the primary block.
8788
const (
8889
AgentStateAwaitingGate = "awaiting-gate" // Waiting for a gate condition, intentional pause
8990
)

internal/cmd/bd_helpers.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type bdCmd struct {
1717
stderr io.Writer
1818
autoCommit bool
1919
gtRoot string
20+
beadsDir string
2021
}
2122

2223
// BdCmd creates a new bd command builder with the given arguments.
@@ -50,6 +51,15 @@ func (b *bdCmd) WithGTRoot(root string) *bdCmd {
5051
return b
5152
}
5253

54+
// WithBeadsDir sets BEADS_DIR explicitly in the environment.
55+
// This prevents inherited BEADS_DIR from the parent process from causing
56+
// bd to write to the wrong database. The dir should be the resolved
57+
// .beads directory path (e.g., from beads.ResolveBeadsDir).
58+
func (b *bdCmd) WithBeadsDir(dir string) *bdCmd {
59+
b.beadsDir = dir
60+
return b
61+
}
62+
5363
// Dir sets the working directory for the command.
5464
func (b *bdCmd) Dir(dir string) *bdCmd {
5565
b.dir = dir
@@ -96,6 +106,14 @@ func (b *bdCmd) buildEnv() []string {
96106
env = append(env, "GT_ROOT="+b.gtRoot)
97107
}
98108

109+
// Add BEADS_DIR if specified.
110+
// This prevents inherited BEADS_DIR from causing bd to target the wrong
111+
// database (e.g., HQ instead of rig). See gt-ctir.
112+
if b.beadsDir != "" {
113+
env = filterEnvKey(env, "BEADS_DIR")
114+
env = append(env, "BEADS_DIR="+b.beadsDir)
115+
}
116+
99117
return env
100118
}
101119

internal/cmd/bd_helpers_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,71 @@ func TestBdCmd_EnvImmutability(t *testing.T) {
400400
}
401401
}
402402

403+
func TestBdCmd_WithBeadsDir_SetsEnv(t *testing.T) {
404+
// WithBeadsDir should set BEADS_DIR in the environment
405+
bdc := BdCmd("show", "id").
406+
WithBeadsDir("/town/rig/mayor/rig/.beads")
407+
cmd := bdc.Build()
408+
envMap := parseEnv(cmd.Env)
409+
410+
if envMap["BEADS_DIR"] != "/town/rig/mayor/rig/.beads" {
411+
t.Errorf("BEADS_DIR = %q, want %q", envMap["BEADS_DIR"], "/town/rig/mayor/rig/.beads")
412+
}
413+
}
414+
415+
func TestBdCmd_WithBeadsDir_OverridesInherited(t *testing.T) {
416+
// WithBeadsDir should override an inherited BEADS_DIR from the parent
417+
// process. This is the core fix for gt-ctir: without overriding,
418+
// bd could write to the wrong database (HQ instead of rig).
419+
baseEnv := []string{"PATH=/usr/bin", "BEADS_DIR=/town/.beads", "HOME=/home/user"}
420+
421+
bdc := &bdCmd{
422+
args: []string{"mol", "wisp", "create", "proto-id"},
423+
env: baseEnv,
424+
stderr: os.Stderr,
425+
}
426+
bdc.WithBeadsDir("/town/rig/mayor/rig/.beads")
427+
cmd := bdc.Build()
428+
envMap := parseEnv(cmd.Env)
429+
430+
if envMap["BEADS_DIR"] != "/town/rig/mayor/rig/.beads" {
431+
t.Errorf("BEADS_DIR = %q, want %q (should override inherited)", envMap["BEADS_DIR"], "/town/rig/mayor/rig/.beads")
432+
}
433+
434+
// Verify exactly one BEADS_DIR entry (deduplication)
435+
count := 0
436+
for _, e := range cmd.Env {
437+
if strings.HasPrefix(e, "BEADS_DIR=") {
438+
count++
439+
}
440+
}
441+
if count != 1 {
442+
t.Errorf("found %d BEADS_DIR entries, want 1 (dedup must remove old)", count)
443+
}
444+
}
445+
446+
func TestBdCmd_EmptyBeadsDir_Skipped(t *testing.T) {
447+
// Empty WithBeadsDir should not add BEADS_DIR to env
448+
bdc := BdCmd("show", "id").
449+
WithBeadsDir("")
450+
bdc.env = filterEnv(bdc.env, "BEADS_DIR")
451+
cmd := bdc.Build()
452+
453+
for _, e := range cmd.Env {
454+
if strings.HasPrefix(e, "BEADS_DIR=") {
455+
t.Errorf("BEADS_DIR should not be added when empty, found: %s", e)
456+
}
457+
}
458+
}
459+
460+
func TestBdCmd_WithBeadsDir_Chaining(t *testing.T) {
461+
// WithBeadsDir should return receiver for chaining
462+
bdc := BdCmd("test")
463+
if bdc.WithBeadsDir("/test") != bdc {
464+
t.Error("WithBeadsDir() should return receiver for chaining")
465+
}
466+
}
467+
403468
// filterEnv returns env with all entries matching the given key prefix removed.
404469
func filterEnv(env []string, key string) []string {
405470
prefix := key + "="

internal/cmd/patrol_helpers.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ func formatBeadLine(issue *beads.Issue) string {
144144
// autoSpawnPatrol creates and pins a new patrol wisp.
145145
// Returns the patrol ID or an error.
146146
func autoSpawnPatrol(cfg PatrolConfig) (string, error) {
147+
// Resolve the beads directory following redirects.
148+
// This ensures bd targets the correct database (e.g., rig database
149+
// instead of HQ) regardless of inherited BEADS_DIR. See gt-ctir.
150+
resolvedBeadsDir := beads.ResolveBeadsDir(cfg.BeadsDir)
151+
147152
// Find the proto ID for the patrol molecule
148153
cmdCatalog := exec.Command("gt", "formula", "list")
149154
cmdCatalog.Dir = cfg.BeadsDir
@@ -184,6 +189,7 @@ func autoSpawnPatrol(cfg PatrolConfig) (string, error) {
184189
}
185190
cmdSpawn := BdCmd(spawnArgs...).
186191
WithAutoCommit().
192+
WithBeadsDir(resolvedBeadsDir).
187193
Dir(cfg.BeadsDir).
188194
Build()
189195
var stdoutSpawn, stderrSpawn bytes.Buffer
@@ -227,6 +233,7 @@ func autoSpawnPatrol(cfg PatrolConfig) (string, error) {
227233
// Hook the wisp to the agent so gt mol status sees it
228234
if err := BdCmd("update", patrolID, "--status=hooked", "--assignee="+cfg.Assignee).
229235
WithAutoCommit().
236+
WithBeadsDir(resolvedBeadsDir).
230237
Dir(cfg.BeadsDir).
231238
Run(); err != nil {
232239
return patrolID, fmt.Errorf("created wisp %s but failed to hook", patrolID)

0 commit comments

Comments
 (0)