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:
gastown/polecats/dementus
2025-12-30 22:28:33 -08:00
committed by Steve Yegge
parent b241a353f3
commit 4a22e621a9
5 changed files with 77 additions and 0 deletions

View File

@@ -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 {

View File

@@ -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

View File

@@ -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))

View File

@@ -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.

View File

@@ -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 {