diff --git a/internal/cmd/done.go b/internal/cmd/done.go index 8d0c7694..e029a88e 100644 --- a/internal/cmd/done.go +++ b/internal/cmd/done.go @@ -608,11 +608,21 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus // has attached_molecule pointing to the wisp. Without this fix, gt done // only closed the hooked bead, leaving the wisp orphaned. // 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) 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 - 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) } } @@ -645,7 +655,7 @@ func updateAgentStateOnDone(cwd, townRoot, exitType, _ string) { // issueID unus 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) } - // 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 diff --git a/internal/cmd/molecule_step.go b/internal/cmd/molecule_step.go index f7408bd3..13b38cef 100644 --- a/internal/cmd/molecule_step.go +++ b/internal/cmd/molecule_step.go @@ -53,13 +53,13 @@ func init() { // StepDoneResult is the result of a step done operation. type StepDoneResult struct { - StepID string `json:"step_id"` - MoleculeID string `json:"molecule_id"` - StepClosed bool `json:"step_closed"` - NextStepID string `json:"next_step_id,omitempty"` + StepID string `json:"step_id"` + MoleculeID string `json:"molecule_id"` + StepClosed bool `json:"step_closed"` + NextStepID string `json:"next_step_id,omitempty"` NextStepTitle string `json:"next_step_title,omitempty"` - Complete bool `json:"complete"` - Action string `json:"action"` // "continue", "done", "no_more_ready" + Complete bool `json:"complete"` + Action string `json:"action"` // "continue", "done", "no_more_ready" } 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. // Step IDs have format: mol-id.N where N is the step number. // Examples: -// gt-abc.1 -> gt-abc -// gt-xyz.3 -> gt-xyz -// bd-mol-abc.2 -> bd-mol-abc +// +// gt-abc.1 -> gt-abc +// gt-xyz.3 -> gt-xyz +// bd-mol-abc.2 -> bd-mol-abc func extractMoleculeIDFromStep(stepID string) string { // Find the last dot lastDot := strings.LastIndex(stepID, ".") @@ -363,14 +364,26 @@ func handleMoleculeComplete(cwd, townRoot, moleculeID string, dryRun bool) error } 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 send POLECAT_DONE to witness\n") 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() 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) pinnedBeads, err := b.List(beads.ListOptions{ Status: beads.StatusPinned,