From 2465af3e91c19224333da259248e0bbed814556c Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sun, 21 Dec 2025 21:55:48 -0800 Subject: [PATCH] feat(witness): implement epic child filtering for auto-spawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add isChildOfEpic() function that checks if an issue is a child of the configured epic by verifying the issue has the epic in its dependents with dependency_type="blocks". When EpicID is configured in witness config, only issues that block that epic will be considered for auto-spawning. Issues that cannot be verified are safely skipped. Closes gt-zhm5. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- internal/witness/manager.go | 54 +++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/internal/witness/manager.go b/internal/witness/manager.go index cc62e3bc..1d86ceb0 100644 --- a/internal/witness/manager.go +++ b/internal/witness/manager.go @@ -685,8 +685,14 @@ func (m *Manager) autoSpawnForReadyWork(w *Witness) error { // Filter by epic if configured if w.Config.EpicID != "" { - // TODO: Check if issue is a child of the configured epic - // For now, we skip this filter + isChild, err := m.isChildOfEpic(issue.ID, w.Config.EpicID) + if err != nil { + // Skip issues we can't verify - safer than including unknown work + continue + } + if !isChild { + continue + } } // Filter by prefix if configured @@ -783,6 +789,50 @@ func (m *Manager) getReadyIssues() ([]ReadyIssue, error) { return issues, nil } +// issueDependency represents a dependency from bd show --json output. +type issueDependency struct { + ID string `json:"id"` + DependencyType string `json:"dependency_type"` +} + +// issueWithDeps represents an issue with its dependencies from bd show --json. +type issueWithDeps struct { + ID string `json:"id"` + Dependents []issueDependency `json:"dependents"` +} + +// isChildOfEpic checks if an issue blocks (is a child of) the given epic. +func (m *Manager) isChildOfEpic(issueID, epicID string) (bool, error) { + cmd := exec.Command("bd", "show", issueID, "--json") + cmd.Dir = m.workDir + + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return false, fmt.Errorf("%s", stderr.String()) + } + + var issues []issueWithDeps + if err := json.Unmarshal(stdout.Bytes(), &issues); err != nil { + return false, fmt.Errorf("parsing issue: %w", err) + } + + if len(issues) == 0 { + return false, nil + } + + // Check if the epic is in the dependents with type "blocks" + for _, dep := range issues[0].Dependents { + if dep.ID == epicID && dep.DependencyType == "blocks" { + return true, nil + } + } + + return false, nil +} + // isAlreadySpawned checks if an issue has already been spawned. func (m *Manager) isAlreadySpawned(w *Witness, issueID string) bool { for _, id := range w.SpawnedIssues {