feat(beads): Capture session_id in issue close for CV attribution
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user