feat: add 'convoy' issue type with reactive completion (bd-hj0s)
- Add TypeConvoy to issue types for cross-project tracking - Implement reactive completion: when all tracked issues close, convoy auto-closes with reason "All tracked issues completed" - Uses 'tracks' dependency type (non-blocking, cross-prefix capable) - Update help text for --type flag in list/create commands - Add test for convoy reactive completion behavior 🤖 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:
@@ -1100,6 +1100,83 @@ func (s *SQLiteStorage) CloseIssue(ctx context.Context, id string, reason string
|
||||
return fmt.Errorf("failed to invalidate blocked cache: %w", err)
|
||||
}
|
||||
|
||||
// Reactive convoy completion: check if any convoys tracking this issue should auto-close
|
||||
// Find convoys that track this issue (convoy.issue_id tracks closed_issue.depends_on_id)
|
||||
convoyRows, err := tx.QueryContext(ctx, `
|
||||
SELECT DISTINCT d.issue_id
|
||||
FROM dependencies d
|
||||
JOIN issues i ON d.issue_id = i.id
|
||||
WHERE d.depends_on_id = ?
|
||||
AND d.type = ?
|
||||
AND i.issue_type = ?
|
||||
AND i.status != ?
|
||||
`, id, types.DepTracks, types.TypeConvoy, types.StatusClosed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find tracking convoys: %w", err)
|
||||
}
|
||||
defer func() { _ = convoyRows.Close() }()
|
||||
|
||||
var convoyIDs []string
|
||||
for convoyRows.Next() {
|
||||
var convoyID string
|
||||
if err := convoyRows.Scan(&convoyID); err != nil {
|
||||
return fmt.Errorf("failed to scan convoy ID: %w", err)
|
||||
}
|
||||
convoyIDs = append(convoyIDs, convoyID)
|
||||
}
|
||||
if err := convoyRows.Err(); err != nil {
|
||||
return fmt.Errorf("convoy rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
// For each convoy, check if all tracked issues are now closed
|
||||
for _, convoyID := range convoyIDs {
|
||||
// Count non-closed tracked issues for this convoy
|
||||
var openCount int
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM dependencies d
|
||||
JOIN issues i ON d.depends_on_id = i.id
|
||||
WHERE d.issue_id = ?
|
||||
AND d.type = ?
|
||||
AND i.status != ?
|
||||
AND i.status != ?
|
||||
`, convoyID, types.DepTracks, types.StatusClosed, types.StatusTombstone).Scan(&openCount)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to count open tracked issues for convoy %s: %w", convoyID, err)
|
||||
}
|
||||
|
||||
// If all tracked issues are closed, auto-close the convoy
|
||||
if openCount == 0 {
|
||||
closeReason := "All tracked issues completed"
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
UPDATE issues SET status = ?, closed_at = ?, updated_at = ?, close_reason = ?
|
||||
WHERE id = ?
|
||||
`, types.StatusClosed, now, now, closeReason, convoyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to auto-close convoy %s: %w", convoyID, err)
|
||||
}
|
||||
|
||||
// Record the close event
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO events (issue_id, event_type, actor, comment)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, convoyID, types.EventClosed, "system:convoy-completion", closeReason)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to record convoy close event: %w", err)
|
||||
}
|
||||
|
||||
// Mark convoy as dirty
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO dirty_issues (issue_id, marked_at)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT (issue_id) DO UPDATE SET marked_at = excluded.marked_at
|
||||
`, convoyID, now)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to mark convoy dirty: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user