feat: Implement Step.Gate evaluation (Phase 1: Human Gates)
This implements Phase 1 of the Step.Gate feature (bd-7zka.2): - bd cook now creates gate issues for steps with gate fields - Gate issues have type=gate and block the gated step via dependency - bd list filters out gate issues by default (use --include-gates to show) - New bd gate command with list and resolve subcommands Gate types supported in Phase 1: - human: Manual closure via bd close or bd gate resolve Implementation details: - createGateIssue() in cook.go creates gate issues with proper metadata - collectSteps() creates gate dependencies when processing gated steps - IssueFilter.ExcludeTypes added to storage layer for type-based filtering - Gate command provides dedicated UX for gate management 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -476,6 +476,47 @@ func cookFormulaToSubgraph(f *formula.Formula, protoID string) (*TemplateSubgrap
|
||||
}, nil
|
||||
}
|
||||
|
||||
// createGateIssue creates a gate issue for a step with a Gate field.
|
||||
// Gate issues have type=gate and block the step they guard.
|
||||
// Returns the gate issue and its ID.
|
||||
func createGateIssue(step *formula.Step, parentID string) *types.Issue {
|
||||
if step.Gate == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate gate issue ID: {parentID}.gate-{step.ID}
|
||||
gateID := fmt.Sprintf("%s.gate-%s", parentID, step.ID)
|
||||
|
||||
// Build title from gate type and ID
|
||||
title := fmt.Sprintf("Gate: %s", step.Gate.Type)
|
||||
if step.Gate.ID != "" {
|
||||
title = fmt.Sprintf("Gate: %s %s", step.Gate.Type, step.Gate.ID)
|
||||
}
|
||||
|
||||
// Parse timeout if specified
|
||||
var timeout time.Duration
|
||||
if step.Gate.Timeout != "" {
|
||||
if parsed, err := time.ParseDuration(step.Gate.Timeout); err == nil {
|
||||
timeout = parsed
|
||||
}
|
||||
}
|
||||
|
||||
return &types.Issue{
|
||||
ID: gateID,
|
||||
Title: title,
|
||||
Description: fmt.Sprintf("Async gate for step %s", step.ID),
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeGate,
|
||||
AwaitType: step.Gate.Type,
|
||||
AwaitID: step.Gate.ID,
|
||||
Timeout: timeout,
|
||||
IsTemplate: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// processStepToIssue converts a formula.Step to a types.Issue.
|
||||
// The issue includes all fields including Labels populated from step.Labels and waits_for.
|
||||
// This is the shared core logic used by both DB-persisted and in-memory cooking.
|
||||
@@ -562,6 +603,41 @@ func collectSteps(steps []*formula.Step, parentID string,
|
||||
Type: types.DepParentChild,
|
||||
})
|
||||
|
||||
// Create gate issue if step has a Gate (bd-7zka.2)
|
||||
if step.Gate != nil {
|
||||
gateIssue := createGateIssue(step, parentID)
|
||||
*issues = append(*issues, gateIssue)
|
||||
|
||||
// Add gate to mapping (use gate-{step.ID} as key)
|
||||
gateKey := fmt.Sprintf("gate-%s", step.ID)
|
||||
idMapping[gateKey] = gateIssue.ID
|
||||
if issueMap != nil {
|
||||
issueMap[gateIssue.ID] = gateIssue
|
||||
}
|
||||
|
||||
// Handle gate labels if needed
|
||||
if labelHandler != nil && len(gateIssue.Labels) > 0 {
|
||||
for _, label := range gateIssue.Labels {
|
||||
labelHandler(gateIssue.ID, label)
|
||||
}
|
||||
gateIssue.Labels = nil
|
||||
}
|
||||
|
||||
// Gate is a child of the parent (same level as the step)
|
||||
*deps = append(*deps, &types.Dependency{
|
||||
IssueID: gateIssue.ID,
|
||||
DependsOnID: parentID,
|
||||
Type: types.DepParentChild,
|
||||
})
|
||||
|
||||
// Step depends on gate (gate blocks the step)
|
||||
*deps = append(*deps, &types.Dependency{
|
||||
IssueID: issue.ID,
|
||||
DependsOnID: gateIssue.ID,
|
||||
Type: types.DepBlocks,
|
||||
})
|
||||
}
|
||||
|
||||
// Recursively collect children
|
||||
if len(step.Children) > 0 {
|
||||
collectSteps(step.Children, issue.ID, idMapping, issueMap, issues, deps, labelHandler)
|
||||
|
||||
1129
cmd/bd/gate.go
1129
cmd/bd/gate.go
File diff suppressed because it is too large
Load Diff
@@ -415,6 +415,9 @@ var listCmd = &cobra.Command{
|
||||
// Template filtering
|
||||
includeTemplates, _ := cmd.Flags().GetBool("include-templates")
|
||||
|
||||
// Gate filtering (bd-7zka.2)
|
||||
includeGates, _ := cmd.Flags().GetBool("include-gates")
|
||||
|
||||
// Parent filtering
|
||||
parentID, _ := cmd.Flags().GetString("parent")
|
||||
|
||||
@@ -620,6 +623,12 @@ var listCmd = &cobra.Command{
|
||||
filter.IsTemplate = &isTemplate
|
||||
}
|
||||
|
||||
// Gate filtering: exclude gate issues by default (bd-7zka.2)
|
||||
// Use --include-gates or --type gate to show gate issues
|
||||
if !includeGates && issueType != "gate" {
|
||||
filter.ExcludeTypes = append(filter.ExcludeTypes, types.TypeGate)
|
||||
}
|
||||
|
||||
// Parent filtering: filter children by parent issue
|
||||
if parentID != "" {
|
||||
filter.ParentID = &parentID
|
||||
@@ -721,6 +730,13 @@ var listCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
|
||||
// Type exclusion (bd-7zka.2)
|
||||
if len(filter.ExcludeTypes) > 0 {
|
||||
for _, t := range filter.ExcludeTypes {
|
||||
listArgs.ExcludeTypes = append(listArgs.ExcludeTypes, string(t))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := daemonClient.List(listArgs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
@@ -961,6 +977,9 @@ func init() {
|
||||
// Template filtering: exclude templates by default
|
||||
listCmd.Flags().Bool("include-templates", false, "Include template molecules in output")
|
||||
|
||||
// Gate filtering: exclude gate issues by default (bd-7zka.2)
|
||||
listCmd.Flags().Bool("include-gates", false, "Include gate issues in output (normally hidden)")
|
||||
|
||||
// Parent filtering: filter children by parent issue
|
||||
listCmd.Flags().String("parent", "", "Filter by parent issue ID (shows children of specified issue)")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user