Link MR bead to agent bead for traceability (gt-84ery)
Add bidirectional cross-references between MR beads and agent beads: 1. MRFields.AgentBead - tracks which agent created the MR 2. AgentFields.ActiveMR - tracks agent's current MR In gt done: - Include agent_bead in MR description when creating - Update agent bead with active_mr pointing to the new MR In refinery merge handling: - Clear agent bead's active_mr after successful merge Benefits: - Given MR, find which polecat created it - Given polecat, find their active MR - Orphan detection: MR without agent = stale 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
b241a353f3
commit
4a22e621a9
@@ -706,6 +706,7 @@ type AgentFields struct {
|
||||
HookBead string // Currently pinned work bead ID
|
||||
RoleBead string // Role definition bead ID (canonical location; may not exist yet)
|
||||
CleanupStatus string // ZFC: polecat self-reports git state (clean, has_uncommitted, has_stash, has_unpushed)
|
||||
ActiveMR string // Currently active merge request bead ID (for traceability)
|
||||
}
|
||||
|
||||
// FormatAgentDescription creates a description string from agent fields.
|
||||
@@ -745,6 +746,12 @@ func FormatAgentDescription(title string, fields *AgentFields) string {
|
||||
lines = append(lines, "cleanup_status: null")
|
||||
}
|
||||
|
||||
if fields.ActiveMR != "" {
|
||||
lines = append(lines, fmt.Sprintf("active_mr: %s", fields.ActiveMR))
|
||||
} else {
|
||||
lines = append(lines, "active_mr: null")
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
@@ -782,6 +789,8 @@ func ParseAgentFields(description string) *AgentFields {
|
||||
fields.RoleBead = value
|
||||
case "cleanup_status":
|
||||
fields.CleanupStatus = value
|
||||
case "active_mr":
|
||||
fields.ActiveMR = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -896,6 +905,26 @@ func (b *Beads) UpdateAgentCleanupStatus(id string, cleanupStatus string) error
|
||||
return b.Update(id, UpdateOptions{Description: &description})
|
||||
}
|
||||
|
||||
// UpdateAgentActiveMR updates the active_mr field in an agent bead.
|
||||
// This links the agent to their current merge request for traceability.
|
||||
// Pass empty string to clear the field (e.g., after merge completes).
|
||||
func (b *Beads) UpdateAgentActiveMR(id string, activeMR string) error {
|
||||
// First get current issue to preserve other fields
|
||||
issue, err := b.Show(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse existing fields
|
||||
fields := ParseAgentFields(issue.Description)
|
||||
fields.ActiveMR = activeMR
|
||||
|
||||
// Format new description
|
||||
description := FormatAgentDescription(issue.Title, fields)
|
||||
|
||||
return b.Update(id, UpdateOptions{Description: &description})
|
||||
}
|
||||
|
||||
// DeleteAgentBead permanently deletes an agent bead.
|
||||
// Uses --hard --force for immediate permanent deletion (no tombstone).
|
||||
func (b *Beads) DeleteAgentBead(id string) error {
|
||||
|
||||
@@ -164,6 +164,7 @@ type MRFields struct {
|
||||
Rig string // Which rig
|
||||
MergeCommit string // SHA of merge commit (set on close)
|
||||
CloseReason string // Reason for closing: merged, rejected, conflict, superseded
|
||||
AgentBead string // Agent bead ID that created this MR (for traceability)
|
||||
}
|
||||
|
||||
// ParseMRFields extracts structured merge-request fields from an issue's description.
|
||||
@@ -218,6 +219,9 @@ func ParseMRFields(issue *Issue) *MRFields {
|
||||
case "close_reason", "close-reason", "closereason":
|
||||
fields.CloseReason = value
|
||||
hasFields = true
|
||||
case "agent_bead", "agent-bead", "agentbead":
|
||||
fields.AgentBead = value
|
||||
hasFields = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +261,9 @@ func FormatMRFields(fields *MRFields) string {
|
||||
if fields.CloseReason != "" {
|
||||
lines = append(lines, "close_reason: "+fields.CloseReason)
|
||||
}
|
||||
if fields.AgentBead != "" {
|
||||
lines = append(lines, "agent_bead: "+fields.AgentBead)
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
@@ -284,6 +291,9 @@ func SetMRFields(issue *Issue, fields *MRFields) string {
|
||||
"close_reason": true,
|
||||
"close-reason": true,
|
||||
"closereason": true,
|
||||
"agent_bead": true,
|
||||
"agent-bead": true,
|
||||
"agentbead": true,
|
||||
}
|
||||
|
||||
// Collect non-MR lines from existing description
|
||||
|
||||
@@ -108,6 +108,19 @@ func runDone(cmd *cobra.Command, args []string) error {
|
||||
polecatName = parts[len(parts)-1]
|
||||
}
|
||||
|
||||
// Get agent bead ID for cross-referencing
|
||||
var agentBeadID string
|
||||
if roleInfo, err := GetRoleWithContext(cwd, townRoot); err == nil {
|
||||
ctx := RoleContext{
|
||||
Role: roleInfo.Role,
|
||||
Rig: roleInfo.Rig,
|
||||
Polecat: roleInfo.Polecat,
|
||||
TownRoot: townRoot,
|
||||
WorkDir: cwd,
|
||||
}
|
||||
agentBeadID = getAgentBeadID(ctx)
|
||||
}
|
||||
|
||||
// For COMPLETED, we need an issue ID and branch must not be main
|
||||
var mrID string
|
||||
if exitType == ExitCompleted {
|
||||
@@ -173,6 +186,9 @@ func runDone(cmd *cobra.Command, args []string) error {
|
||||
if worker != "" {
|
||||
description += fmt.Sprintf("\nworker: %s", worker)
|
||||
}
|
||||
if agentBeadID != "" {
|
||||
description += fmt.Sprintf("\nagent_bead: %s", agentBeadID)
|
||||
}
|
||||
|
||||
// Create MR bead (ephemeral wisp - will be cleaned up after merge)
|
||||
mrIssue, err := bd.Create(beads.CreateOptions{
|
||||
@@ -186,6 +202,13 @@ func runDone(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
mrID = mrIssue.ID
|
||||
|
||||
// Update agent bead with active_mr reference (for traceability)
|
||||
if agentBeadID != "" {
|
||||
if err := bd.UpdateAgentActiveMR(agentBeadID, mrID); err != nil {
|
||||
style.PrintWarning("could not update agent bead with active_mr: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Success output
|
||||
fmt.Printf("%s Work submitted to merge queue\n", style.Bold.Render("✓"))
|
||||
fmt.Printf(" MR ID: %s\n", style.Bold.Render(mrID))
|
||||
|
||||
@@ -26,6 +26,7 @@ type MR struct {
|
||||
Title string `json:"title"` // MR title
|
||||
Priority int `json:"priority"` // Priority (lower = higher priority)
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
AgentBead string `json:"agent_bead,omitempty"` // Agent bead ID that created this MR (for traceability)
|
||||
}
|
||||
|
||||
// Queue manages the MR storage.
|
||||
|
||||
@@ -262,6 +262,13 @@ func (e *Engineer) handleSuccess(mr *beads.Issue, result ProcessResult) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3.5. Clear agent bead's active_mr reference (traceability cleanup)
|
||||
if mrFields.AgentBead != "" {
|
||||
if err := e.beads.UpdateAgentActiveMR(mrFields.AgentBead, ""); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to clear agent bead %s active_mr: %v\n", mrFields.AgentBead, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Delete source branch if configured (local only - branches never go to origin)
|
||||
if e.config.DeleteMergedBranches && mrFields.Branch != "" {
|
||||
if err := e.git.DeleteBranch(mrFields.Branch, true); err != nil {
|
||||
@@ -328,6 +335,13 @@ func (e *Engineer) handleSuccessFromQueue(mr *mrqueue.MR, result ProcessResult)
|
||||
}
|
||||
}
|
||||
|
||||
// 1.5. Clear agent bead's active_mr reference (traceability cleanup)
|
||||
if mr.AgentBead != "" {
|
||||
if err := e.beads.UpdateAgentActiveMR(mr.AgentBead, ""); err != nil {
|
||||
fmt.Fprintf(e.output, "[Engineer] Warning: failed to clear agent bead %s active_mr: %v\n", mr.AgentBead, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Delete source branch if configured (local only)
|
||||
if e.config.DeleteMergedBranches && mr.Branch != "" {
|
||||
if err := e.git.DeleteBranch(mr.Branch, true); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user