fix(molecules): cascade-close child wisps on molecule completion (gt-zbnr)
When deacon patrol molecules completed, their child step wisps were not being closed automatically. This caused orphan wisp accumulation - 143+ orphaned wisps were found in one cleanup session. The fix ensures that when a molecule completes (via gt done or gt mol step done), all descendant step issues are recursively closed before the molecule itself. Changes: - done.go: Added closeDescendants() call in updateAgentStateOnDone before closing the attached molecule - molecule_step.go: Added closeDescendants() call in handleMoleculeComplete for all roles (not just polecats) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -640,11 +640,21 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus
|
|||||||
// has attached_molecule pointing to the wisp. Without this fix, gt done
|
// has attached_molecule pointing to the wisp. Without this fix, gt done
|
||||||
// only closed the hooked bead, leaving the wisp orphaned.
|
// only closed the hooked bead, leaving the wisp orphaned.
|
||||||
// Order matters: wisp closes -> unblocks base bead -> base bead closes.
|
// Order matters: wisp closes -> unblocks base bead -> base bead closes.
|
||||||
|
//
|
||||||
|
// BUG FIX (gt-zbnr): Close child wisps BEFORE closing the molecule itself.
|
||||||
|
// Deacon patrol molecules have child step wisps that were being orphaned
|
||||||
|
// when the patrol completed. Now we cascade-close all descendants first.
|
||||||
attachment := beads.ParseAttachmentFields(hookedBead)
|
attachment := beads.ParseAttachmentFields(hookedBead)
|
||||||
if attachment != nil && attachment.AttachedMolecule != "" {
|
if attachment != nil && attachment.AttachedMolecule != "" {
|
||||||
if err := bd.Close(attachment.AttachedMolecule); err != nil {
|
moleculeID := attachment.AttachedMolecule
|
||||||
|
// Cascade-close all child wisps before closing the molecule
|
||||||
|
childrenClosed := closeDescendants(bd, moleculeID)
|
||||||
|
if childrenClosed > 0 {
|
||||||
|
fmt.Printf(" Closed %d child step issues\n", childrenClosed)
|
||||||
|
}
|
||||||
|
if err := bd.Close(moleculeID); err != nil {
|
||||||
// Non-fatal: warn but continue
|
// Non-fatal: warn but continue
|
||||||
fmt.Fprintf(os.Stderr, "Warning: couldn't close attached molecule %s: %v\n", attachment.AttachedMolecule, err)
|
fmt.Fprintf(os.Stderr, "Warning: couldn't close attached molecule %s: %v\n", moleculeID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,7 +687,7 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus
|
|||||||
if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil {
|
if _, err := bd.Run("agent", "state", agentBeadID, "awaiting-gate"); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err)
|
fmt.Fprintf(os.Stderr, "Warning: couldn't set agent %s to awaiting-gate: %v\n", agentBeadID, err)
|
||||||
}
|
}
|
||||||
// ExitCompleted and ExitDeferred don't set state - observable from tmux
|
// ExitCompleted and ExitDeferred don't set state - observable from tmux
|
||||||
}
|
}
|
||||||
|
|
||||||
// ZFC #10: Self-report cleanup status
|
// ZFC #10: Self-report cleanup status
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ func init() {
|
|||||||
|
|
||||||
// StepDoneResult is the result of a step done operation.
|
// StepDoneResult is the result of a step done operation.
|
||||||
type StepDoneResult struct {
|
type StepDoneResult struct {
|
||||||
StepID string `json:"step_id"`
|
StepID string `json:"step_id"`
|
||||||
MoleculeID string `json:"molecule_id"`
|
MoleculeID string `json:"molecule_id"`
|
||||||
StepClosed bool `json:"step_closed"`
|
StepClosed bool `json:"step_closed"`
|
||||||
NextStepID string `json:"next_step_id,omitempty"`
|
NextStepID string `json:"next_step_id,omitempty"`
|
||||||
NextStepTitle string `json:"next_step_title,omitempty"`
|
NextStepTitle string `json:"next_step_title,omitempty"`
|
||||||
Complete bool `json:"complete"`
|
Complete bool `json:"complete"`
|
||||||
Action string `json:"action"` // "continue", "done", "no_more_ready"
|
Action string `json:"action"` // "continue", "done", "no_more_ready"
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMoleculeStepDone(cmd *cobra.Command, args []string) error {
|
func runMoleculeStepDone(cmd *cobra.Command, args []string) error {
|
||||||
@@ -162,9 +162,10 @@ func runMoleculeStepDone(cmd *cobra.Command, args []string) error {
|
|||||||
// extractMoleculeIDFromStep extracts the molecule ID from a step ID.
|
// extractMoleculeIDFromStep extracts the molecule ID from a step ID.
|
||||||
// Step IDs have format: mol-id.N where N is the step number.
|
// Step IDs have format: mol-id.N where N is the step number.
|
||||||
// Examples:
|
// Examples:
|
||||||
// gt-abc.1 -> gt-abc
|
//
|
||||||
// gt-xyz.3 -> gt-xyz
|
// gt-abc.1 -> gt-abc
|
||||||
// bd-mol-abc.2 -> bd-mol-abc
|
// gt-xyz.3 -> gt-xyz
|
||||||
|
// bd-mol-abc.2 -> bd-mol-abc
|
||||||
func extractMoleculeIDFromStep(stepID string) string {
|
func extractMoleculeIDFromStep(stepID string) string {
|
||||||
// Find the last dot
|
// Find the last dot
|
||||||
lastDot := strings.LastIndex(stepID, ".")
|
lastDot := strings.LastIndex(stepID, ".")
|
||||||
@@ -388,14 +389,26 @@ func handleMoleculeComplete(cwd, townRoot, moleculeID string, dryRun bool) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
|
fmt.Printf("[dry-run] Would close child steps of %s\n", moleculeID)
|
||||||
fmt.Printf("[dry-run] Would unpin work for %s\n", agentID)
|
fmt.Printf("[dry-run] Would unpin work for %s\n", agentID)
|
||||||
fmt.Printf("[dry-run] Would send POLECAT_DONE to witness\n")
|
fmt.Printf("[dry-run] Would send POLECAT_DONE to witness\n")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unpin the molecule bead (set status to open, will be closed by gt done or manually)
|
// BUG FIX (gt-zbnr): Close child steps before unpinning/completing.
|
||||||
|
// Deacon patrol molecules have child step wisps that were being orphaned
|
||||||
|
// when the patrol completed. Now we cascade-close all descendants first.
|
||||||
workDir, err := findLocalBeadsDir()
|
workDir, err := findLocalBeadsDir()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
b := beads.New(workDir)
|
||||||
|
childrenClosed := closeDescendants(b, moleculeID)
|
||||||
|
if childrenClosed > 0 {
|
||||||
|
fmt.Printf("%s Closed %d child step issues\n", style.Bold.Render("✓"), childrenClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpin the molecule bead (set status to open, will be closed by gt done or manually)
|
||||||
|
if workDir, err := findLocalBeadsDir(); err == nil {
|
||||||
b := beads.New(workDir)
|
b := beads.New(workDir)
|
||||||
pinnedBeads, err := b.List(beads.ListOptions{
|
pinnedBeads, err := b.List(beads.ListOptions{
|
||||||
Status: beads.StatusPinned,
|
Status: beads.StatusPinned,
|
||||||
|
|||||||
Reference in New Issue
Block a user