From 62354dfe1b9967d2720272dc8608b7648313d459 Mon Sep 17 00:00:00 2001 From: valkyrie Date: Thu, 1 Jan 2026 18:16:35 -0800 Subject: [PATCH] feat(beads): Capture session_id in issue close for CV attribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass CLAUDE_SESSION_ID environment variable to bd close for work attribution tracking, enabling queries like "what work did this session do?" for entity CV building (per decision 009-session-events-architecture.md). Changes: - beads.Close() and CloseWithReason() now pass --session to bd close - Updated all direct exec.Command("bd", "close"...) calls: - internal/mail/mailbox.go - closeInDir() - internal/cmd/swarm.go - swarm land and cancel - internal/cmd/hook.go - auto-replace completed beads - internal/cmd/synthesis.go - convoy close - internal/cmd/crew_lifecycle.go - workspace removal - internal/cmd/polecat.go - polecat nuke The bd CLI already supports --session (or CLAUDE_SESSION_ID env var) so this change ensures consistent session tracking across all gt close paths. Fixes: gt-nvz8b 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/beads/beads.go | 16 ++++++++++++++++ internal/cmd/crew_lifecycle.go | 6 +++++- internal/cmd/hook.go | 8 ++++++-- internal/cmd/polecat.go | 6 +++++- internal/cmd/swarm.go | 12 ++++++++++-- internal/cmd/synthesis.go | 6 +++++- internal/mail/mailbox.go | 7 ++++++- 7 files changed, 53 insertions(+), 8 deletions(-) diff --git a/internal/beads/beads.go b/internal/beads/beads.go index abfa161d..99da8da3 100644 --- a/internal/beads/beads.go +++ b/internal/beads/beads.go @@ -538,17 +538,27 @@ func (b *Beads) Update(id string, opts UpdateOptions) error { } // Close closes one or more issues. +// If CLAUDE_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 := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + args = append(args, "--session="+sessionID) + } + _, err := b.run(args...) return err } // CloseWithReason closes one or more issues with a reason. +// If CLAUDE_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 @@ -556,6 +566,12 @@ func (b *Beads) CloseWithReason(reason string, ids ...string) error { args := append([]string{"close"}, ids...) args = append(args, "--reason="+reason) + + // Pass session ID for work attribution if available + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + args = append(args, "--session="+sessionID) + } + _, err := b.run(args...) return err } diff --git a/internal/cmd/crew_lifecycle.go b/internal/cmd/crew_lifecycle.go index 5b5aafb8..c2398970 100644 --- a/internal/cmd/crew_lifecycle.go +++ b/internal/cmd/crew_lifecycle.go @@ -90,7 +90,11 @@ func runCrewRemove(cmd *cobra.Command, args []string) error { } prefix := beads.GetPrefixForRig(townRoot, r.Name) agentBeadID := beads.CrewBeadIDWithPrefix(prefix, r.Name, name) - closeCmd := exec.Command("bd", "close", agentBeadID, "--reason=Crew workspace removed") + closeArgs := []string{"close", agentBeadID, "--reason=Crew workspace removed"} + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + closeArgs = append(closeArgs, "--session="+sessionID) + } + closeCmd := exec.Command("bd", closeArgs...) closeCmd.Dir = r.Path // Run from rig directory for proper beads resolution if output, err := closeCmd.CombinedOutput(); err != nil { // Non-fatal: bead might not exist or already be closed diff --git a/internal/cmd/hook.go b/internal/cmd/hook.go index b660892a..0f7862d0 100644 --- a/internal/cmd/hook.go +++ b/internal/cmd/hook.go @@ -143,8 +143,12 @@ func runHook(cmd *cobra.Command, args []string) error { if !hookDryRun { if hasAttachment { // Close completed molecule bead (use bd close --force for pinned) - closeCmd := exec.Command("bd", "close", existing.ID, "--force", - "--reason=Auto-replaced by gt hook (molecule complete)") + closeArgs := []string{"close", existing.ID, "--force", + "--reason=Auto-replaced by gt hook (molecule complete)"} + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + closeArgs = append(closeArgs, "--session="+sessionID) + } + closeCmd := exec.Command("bd", closeArgs...) closeCmd.Stderr = os.Stderr if err := closeCmd.Run(); err != nil { return fmt.Errorf("closing completed bead %s: %w", existing.ID, err) diff --git a/internal/cmd/polecat.go b/internal/cmd/polecat.go index dfbf76b2..57e7def9 100644 --- a/internal/cmd/polecat.go +++ b/internal/cmd/polecat.go @@ -1573,7 +1573,11 @@ func runPolecatNuke(cmd *cobra.Command, args []string) error { // Step 5: Close agent bead (if exists) agentBeadID := beads.PolecatBeadID(p.rigName, p.polecatName) - closeCmd := exec.Command("bd", "close", agentBeadID, "--reason=nuked") + closeArgs := []string{"close", agentBeadID, "--reason=nuked"} + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + closeArgs = append(closeArgs, "--session="+sessionID) + } + closeCmd := exec.Command("bd", closeArgs...) closeCmd.Dir = filepath.Join(p.r.Path, "mayor", "rig") if err := closeCmd.Run(); err != nil { // Non-fatal - agent bead might not exist diff --git a/internal/cmd/swarm.go b/internal/cmd/swarm.go index df92fb50..b2c41cbe 100644 --- a/internal/cmd/swarm.go +++ b/internal/cmd/swarm.go @@ -816,7 +816,11 @@ func runSwarmLand(cmd *cobra.Command, args []string) error { } // Close the swarm epic in beads - closeCmd := exec.Command("bd", "close", swarmID, "--reason", "Swarm landed to main") + closeArgs := []string{"close", swarmID, "--reason", "Swarm landed to main"} + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + closeArgs = append(closeArgs, "--session="+sessionID) + } + closeCmd := exec.Command("bd", closeArgs...) closeCmd.Dir = foundRig.BeadsPath() if err := closeCmd.Run(); err != nil { style.PrintWarning("couldn't close swarm epic in beads: %v", err) @@ -871,7 +875,11 @@ func runSwarmCancel(cmd *cobra.Command, args []string) error { } // Close the swarm epic in beads with cancelled reason - closeCmd := exec.Command("bd", "close", swarmID, "--reason", "Swarm cancelled") + closeArgs := []string{"close", swarmID, "--reason", "Swarm cancelled"} + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + closeArgs = append(closeArgs, "--session="+sessionID) + } + closeCmd := exec.Command("bd", closeArgs...) closeCmd.Dir = foundRig.BeadsPath() if err := closeCmd.Run(); err != nil { return fmt.Errorf("closing swarm: %w", err) diff --git a/internal/cmd/synthesis.go b/internal/cmd/synthesis.go index 655a0624..a5d3f5e4 100644 --- a/internal/cmd/synthesis.go +++ b/internal/cmd/synthesis.go @@ -321,7 +321,11 @@ func runSynthesisClose(cmd *cobra.Command, args []string) error { } // Close the convoy - closeCmd := exec.Command("bd", "close", convoyID, "--reason=synthesis complete") + closeArgs := []string{"close", convoyID, "--reason=synthesis complete"} + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + closeArgs = append(closeArgs, "--session="+sessionID) + } + closeCmd := exec.Command("bd", closeArgs...) closeCmd.Dir = townBeads closeCmd.Stderr = os.Stderr diff --git a/internal/mail/mailbox.go b/internal/mail/mailbox.go index e68fdb11..e4fc0108 100644 --- a/internal/mail/mailbox.go +++ b/internal/mail/mailbox.go @@ -313,7 +313,12 @@ func (m *Mailbox) markReadBeads(id string) error { // closeInDir closes a message in a specific beads directory. func (m *Mailbox) closeInDir(id, beadsDir string) error { - cmd := exec.Command("bd", "close", id) + args := []string{"close", id} + // Pass session ID for work attribution if available + if sessionID := os.Getenv("CLAUDE_SESSION_ID"); sessionID != "" { + args = append(args, "--session="+sessionID) + } + cmd := exec.Command("bd", args...) cmd.Dir = m.workDir cmd.Env = append(cmd.Environ(), "BEADS_DIR="+beadsDir)