feat: add session_id field to issue close/update mutations (bd-tksk)
Adds closed_by_session tracking for entity CV building per Gas Town decision 009-session-events-architecture.md. Changes: - Add ClosedBySession field to Issue struct - Add closed_by_session column to issues table (migration 034) - Add --session flag to bd close command - Support CLAUDE_SESSION_ID env var as fallback - Add --session flag to bd update for status=closed - Display closed_by_session in bd show output - Update Storage interface to include session parameter in CloseIssue 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
This commit is contained in:
committed by
Steve Yegge
parent
7c9b975436
commit
b362b36824
@@ -46,6 +46,12 @@ create, update, show, or close operation).`,
|
||||
noAuto, _ := cmd.Flags().GetBool("no-auto")
|
||||
suggestNext, _ := cmd.Flags().GetBool("suggest-next")
|
||||
|
||||
// Get session ID from flag or environment variable
|
||||
session, _ := cmd.Flags().GetString("session")
|
||||
if session == "" {
|
||||
session = os.Getenv("CLAUDE_SESSION_ID")
|
||||
}
|
||||
|
||||
ctx := rootCtx
|
||||
|
||||
// --continue only works with a single issue
|
||||
@@ -101,6 +107,7 @@ create, update, show, or close operation).`,
|
||||
closeArgs := &rpc.CloseArgs{
|
||||
ID: id,
|
||||
Reason: reason,
|
||||
Session: session,
|
||||
SuggestNext: suggestNext,
|
||||
}
|
||||
resp, err := daemonClient.CloseIssue(closeArgs)
|
||||
@@ -175,7 +182,7 @@ create, update, show, or close operation).`,
|
||||
continue
|
||||
}
|
||||
|
||||
if err := store.CloseIssue(ctx, id, reason, actor); err != nil {
|
||||
if err := store.CloseIssue(ctx, id, reason, actor, session); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", id, err)
|
||||
continue
|
||||
}
|
||||
@@ -253,5 +260,6 @@ func init() {
|
||||
closeCmd.Flags().Bool("continue", false, "Auto-advance to next step in molecule")
|
||||
closeCmd.Flags().Bool("no-auto", false, "With --continue, show next step but don't claim it")
|
||||
closeCmd.Flags().Bool("suggest-next", false, "Show newly unblocked issues after closing")
|
||||
closeCmd.Flags().String("session", "", "Claude Code session ID (or set CLAUDE_SESSION_ID env var)")
|
||||
rootCmd.AddCommand(closeCmd)
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestDaemonAutoImportAfterGitPull(t *testing.T) {
|
||||
// NOW THE CRITICAL TEST: Agent A closes the issue and pushes
|
||||
t.Run("DaemonAutoImportsAfterGitPull", func(t *testing.T) {
|
||||
// Agent A closes the issue
|
||||
if err := clone1Store.CloseIssue(ctx, issueID, "Completed", "agent-a"); err != nil {
|
||||
if err := clone1Store.CloseIssue(ctx, issueID, "Completed", "agent-a", ""); err != nil {
|
||||
t.Fatalf("Failed to close issue: %v", err)
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ func TestDaemonAutoImportDataCorruption(t *testing.T) {
|
||||
|
||||
// THE CORRUPTION SCENARIO:
|
||||
// 1. Agent A closes the issue and pushes
|
||||
clone1Store.CloseIssue(ctx, issueID, "Done", "agent-a")
|
||||
clone1Store.CloseIssue(ctx, issueID, "Done", "agent-a", "")
|
||||
exportIssuesToJSONL(ctx, clone1Store, clone1JSONLPath)
|
||||
runGitCmd(t, clone1Dir, "add", ".beads/issues.jsonl")
|
||||
runGitCmd(t, clone1Dir, "commit", "-m", "Close issue")
|
||||
|
||||
@@ -734,7 +734,7 @@ func TestSyncBranchIntegration_EndToEnd(t *testing.T) {
|
||||
}
|
||||
|
||||
// Agent B closes the issue
|
||||
store2.CloseIssue(ctx, issueID, "Done by Agent B", "agent-b")
|
||||
store2.CloseIssue(ctx, issueID, "Done by Agent B", "agent-b", "")
|
||||
exportToJSONLWithStore(ctx, store2, clone2JSONLPath)
|
||||
|
||||
// Agent B commits to sync branch
|
||||
|
||||
@@ -283,7 +283,7 @@ func performMerge(targetID string, sourceIDs []string) map[string]interface{} {
|
||||
for _, sourceID := range sourceIDs {
|
||||
// Close the duplicate issue
|
||||
reason := fmt.Sprintf("Duplicate of %s", targetID)
|
||||
if err := store.CloseIssue(ctx, sourceID, reason, actor); err != nil {
|
||||
if err := store.CloseIssue(ctx, sourceID, reason, actor, ""); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("failed to close %s: %v", sourceID, err))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ var closeEligibleEpicsCmd = &cobra.Command{
|
||||
}
|
||||
} else {
|
||||
ctx := rootCtx
|
||||
err := store.CloseIssue(ctx, epicStatus.Epic.ID, "All children completed", "system")
|
||||
err := store.CloseIssue(ctx, epicStatus.Epic.ID, "All children completed", "system", "")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing %s: %v\n", epicStatus.Epic.ID, err)
|
||||
continue
|
||||
|
||||
@@ -385,7 +385,7 @@ func TestCloseReasonRoundTrip(t *testing.T) {
|
||||
|
||||
// Close the issue with a reason
|
||||
closeReason := "Completed: all tests passing"
|
||||
if err := store.CloseIssue(ctx, issue.ID, closeReason, "test-actor"); err != nil {
|
||||
if err := store.CloseIssue(ctx, issue.ID, closeReason, "test-actor", ""); err != nil {
|
||||
t.Fatalf("Failed to close issue: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -425,7 +425,7 @@ var gateCloseCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := store.CloseIssue(ctx, gateID, reason, actor); err != nil {
|
||||
if err := store.CloseIssue(ctx, gateID, reason, actor, ""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing gate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -543,7 +543,7 @@ Example:
|
||||
reason = fmt.Sprintf("Human approval granted: %s (%s)", gate.AwaitID, comment)
|
||||
}
|
||||
|
||||
if err := store.CloseIssue(ctx, gateID, reason, actor); err != nil {
|
||||
if err := store.CloseIssue(ctx, gateID, reason, actor, ""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error closing gate: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -807,7 +807,7 @@ This command is idempotent and safe to run repeatedly.`,
|
||||
continue
|
||||
}
|
||||
} else if store != nil {
|
||||
if err := store.CloseIssue(ctx, gate.ID, reason, actor); err != nil {
|
||||
if err := store.CloseIssue(ctx, gate.ID, reason, actor, ""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Warning: failed to close gate %s: %v\n", gate.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func TestGitPullSyncIntegration(t *testing.T) {
|
||||
issueID := issue.ID
|
||||
|
||||
// Close the issue
|
||||
if err := clone1Store.CloseIssue(ctx, issueID, "Test completed", "test-user"); err != nil {
|
||||
if err := clone1Store.CloseIssue(ctx, issueID, "Test completed", "test-user", ""); err != nil {
|
||||
t.Fatalf("Failed to close issue: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ func TestListQueryCapabilitiesSuite(t *testing.T) {
|
||||
}
|
||||
|
||||
// Close issue3 to set closed_at timestamp
|
||||
if err := s.CloseIssue(ctx, issue3.ID, "test-user", "Testing"); err != nil {
|
||||
if err := s.CloseIssue(ctx, issue3.ID, "test-user", "Testing", ""); err != nil {
|
||||
t.Fatalf("Failed to close issue3: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ Examples:
|
||||
// Step 7: Close the source issue (unless --keep-open)
|
||||
if !keepOpen {
|
||||
closeReason := fmt.Sprintf("Refiled to %s", newIssue.ID)
|
||||
if err := result.Store.CloseIssue(ctx, resolvedSourceID, closeReason, actor); err != nil {
|
||||
if err := result.Store.CloseIssue(ctx, resolvedSourceID, closeReason, actor, ""); err != nil {
|
||||
WarnError("failed to close source issue: %v", err)
|
||||
}
|
||||
// Schedule auto-flush if source was local store
|
||||
|
||||
@@ -30,7 +30,7 @@ func (h *reopenTestHelper) createIssue(title string, issueType types.IssueType,
|
||||
}
|
||||
|
||||
func (h *reopenTestHelper) closeIssue(issueID, reason string) {
|
||||
if err := h.s.CloseIssue(h.ctx, issueID, "test-user", reason); err != nil {
|
||||
if err := h.s.CloseIssue(h.ctx, issueID, "test-user", reason, ""); err != nil {
|
||||
h.t.Fatalf("Failed to close issue: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ func TestSearchWithDateAndPriorityFilters(t *testing.T) {
|
||||
}
|
||||
|
||||
// Close issue3 to set closed_at timestamp
|
||||
if err := s.CloseIssue(ctx, issue3.ID, "test-user", "Testing"); err != nil {
|
||||
if err := s.CloseIssue(ctx, issue3.ID, "test-user", "Testing", ""); err != nil {
|
||||
t.Fatalf("Failed to close issue3: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -209,6 +209,9 @@ var showCmd = &cobra.Command{
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
}
|
||||
if issue.ClosedBySession != "" {
|
||||
fmt.Printf("Closed by session: %s\n", issue.ClosedBySession)
|
||||
}
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
@@ -426,6 +429,9 @@ var showCmd = &cobra.Command{
|
||||
if issue.CloseReason != "" {
|
||||
fmt.Printf("Close reason: %s\n", issue.CloseReason)
|
||||
}
|
||||
if issue.ClosedBySession != "" {
|
||||
fmt.Printf("Closed by session: %s\n", issue.ClosedBySession)
|
||||
}
|
||||
fmt.Printf("Priority: P%d\n", issue.Priority)
|
||||
fmt.Printf("Type: %s\n", issue.IssueType)
|
||||
if issue.Assignee != "" {
|
||||
|
||||
@@ -40,6 +40,17 @@ create, update, show, or close operation).`,
|
||||
if cmd.Flags().Changed("status") {
|
||||
status, _ := cmd.Flags().GetString("status")
|
||||
updates["status"] = status
|
||||
|
||||
// If status is being set to closed, include session if provided
|
||||
if status == "closed" {
|
||||
session, _ := cmd.Flags().GetString("session")
|
||||
if session == "" {
|
||||
session = os.Getenv("CLAUDE_SESSION_ID")
|
||||
}
|
||||
if session != "" {
|
||||
updates["closed_by_session"] = session
|
||||
}
|
||||
}
|
||||
}
|
||||
if cmd.Flags().Changed("priority") {
|
||||
priorityStr, _ := cmd.Flags().GetString("priority")
|
||||
@@ -412,5 +423,6 @@ func init() {
|
||||
updateCmd.Flags().StringSlice("set-labels", nil, "Set labels, replacing all existing (repeatable)")
|
||||
updateCmd.Flags().String("parent", "", "New parent issue ID (reparents the issue, use empty string to remove parent)")
|
||||
updateCmd.Flags().Bool("claim", false, "Atomically claim the issue (sets assignee to you, status to in_progress; fails if already claimed)")
|
||||
updateCmd.Flags().String("session", "", "Claude Code session ID for status=closed (or set CLAUDE_SESSION_ID env var)")
|
||||
rootCmd.AddCommand(updateCmd)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user