Add --parent flag support in daemon mode (bd-2e94)

- Added Parent field to CreateArgs RPC protocol
- Updated CLI to pass parent ID to daemon instead of erroring
- Added parent ID handling in RPC server to call GetNextChildID
- Added validation to prevent both --id and --parent flags
- Added comprehensive tests for hierarchical child creation
- Resolves error: '--parent flag not yet supported in daemon mode'

Amp-Thread-ID: https://ampcode.com/threads/T-3e0f76df-4ba6-4b16-bf75-bb7ea6b19541
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-11-05 13:56:25 -08:00
parent 4ccd8fe38e
commit fc89f15ca0
4 changed files with 141 additions and 17 deletions

View File

@@ -148,23 +148,15 @@ var createCmd = &cobra.Command{
}
// If parent is specified, generate child ID
if parentID != "" {
// In daemon mode, the parent will be sent to the RPC handler
// In direct mode, we generate the child ID here
if parentID != "" && daemonClient == nil {
ctx := context.Background()
var childID string
var err error
if daemonClient != nil {
// TODO: Add RPC support for GetNextChildID (bd-171)
fmt.Fprintf(os.Stderr, "Error: --parent flag not yet supported in daemon mode\n")
os.Exit(1)
} else {
// Direct mode - use storage
childID, err = store.GetNextChildID(ctx, parentID)
childID, err := store.GetNextChildID(ctx, parentID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
explicitID = childID // Set as explicit ID for the rest of the flow
}
@@ -213,6 +205,7 @@ var createCmd = &cobra.Command{
if daemonClient != nil {
createArgs := &rpc.CreateArgs{
ID: explicitID,
Parent: parentID,
Title: title,
Description: description,
IssueType: issueType,

View File

@@ -57,6 +57,7 @@ type Response struct {
// CreateArgs represents arguments for the create operation
type CreateArgs struct {
ID string `json:"id,omitempty"`
Parent string `json:"parent,omitempty"` // Parent ID for hierarchical issues
Title string `json:"title"`
Description string `json:"description,omitempty"`
IssueType string `json:"issue_type"`

View File

@@ -538,6 +538,116 @@ func TestDatabaseHandshake(t *testing.T) {
}
}
func TestCreate_WithParent(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create parent issue
parentArgs := &CreateArgs{
Title: "Parent Epic",
IssueType: "epic",
Priority: 1,
}
parentResp, err := client.Create(parentArgs)
if err != nil {
t.Fatalf("Create parent failed: %v", err)
}
var parent types.Issue
if err := json.Unmarshal(parentResp.Data, &parent); err != nil {
t.Fatalf("Failed to unmarshal parent: %v", err)
}
// Create child issue using --parent flag
childArgs := &CreateArgs{
Parent: parent.ID,
Title: "Child Task",
IssueType: "task",
Priority: 1,
}
childResp, err := client.Create(childArgs)
if err != nil {
t.Fatalf("Create child failed: %v", err)
}
var child types.Issue
if err := json.Unmarshal(childResp.Data, &child); err != nil {
t.Fatalf("Failed to unmarshal child: %v", err)
}
// Verify hierarchical ID format (should be parent.1)
expectedID := parent.ID + ".1"
if child.ID != expectedID {
t.Errorf("Expected child ID %s, got %s", expectedID, child.ID)
}
// Create second child
child2Args := &CreateArgs{
Parent: parent.ID,
Title: "Second Child Task",
IssueType: "task",
Priority: 1,
}
child2Resp, err := client.Create(child2Args)
if err != nil {
t.Fatalf("Create second child failed: %v", err)
}
var child2 types.Issue
if err := json.Unmarshal(child2Resp.Data, &child2); err != nil {
t.Fatalf("Failed to unmarshal second child: %v", err)
}
// Verify second child has incremented ID (parent.2)
expectedID2 := parent.ID + ".2"
if child2.ID != expectedID2 {
t.Errorf("Expected second child ID %s, got %s", expectedID2, child2.ID)
}
}
func TestCreate_WithParentAndIDConflict(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()
// Create parent issue
parentArgs := &CreateArgs{
Title: "Parent Epic",
IssueType: "epic",
Priority: 1,
}
parentResp, err := client.Create(parentArgs)
if err != nil {
t.Fatalf("Create parent failed: %v", err)
}
var parent types.Issue
if err := json.Unmarshal(parentResp.Data, &parent); err != nil {
t.Fatalf("Failed to unmarshal parent: %v", err)
}
// Try to create with both ID and Parent (should fail)
conflictArgs := &CreateArgs{
ID: "bd-custom",
Parent: parent.ID,
Title: "Should Fail",
IssueType: "task",
Priority: 1,
}
resp, err := client.Create(conflictArgs)
if err == nil && resp.Success {
t.Fatal("Expected error when both ID and Parent are specified")
}
if !strings.Contains(resp.Error, "cannot specify both ID and Parent") {
t.Errorf("Expected conflict error message, got: %s", resp.Error)
}
}
func TestCreate_DiscoveredFromInheritsSourceRepo(t *testing.T) {
_, client, cleanup := setupTestServer(t)
defer cleanup()

View File

@@ -95,7 +95,29 @@ func (s *Server) handleCreate(req *Request) Response {
}
}
// Check for conflicting flags
if createArgs.ID != "" && createArgs.Parent != "" {
return Response{
Success: false,
Error: "cannot specify both ID and Parent",
}
}
store := s.storage
ctx := s.reqCtx(req)
// If parent is specified, generate child ID
issueID := createArgs.ID
if createArgs.Parent != "" {
childID, err := store.GetNextChildID(ctx, createArgs.Parent)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to generate child ID: %v", err),
}
}
issueID = childID
}
var design, acceptance, assignee *string
if createArgs.Design != "" {
@@ -109,7 +131,7 @@ func (s *Server) handleCreate(req *Request) Response {
}
issue := &types.Issue{
ID: createArgs.ID,
ID: issueID,
Title: createArgs.Title,
Description: createArgs.Description,
IssueType: types.IssueType(createArgs.IssueType),
@@ -120,8 +142,6 @@ func (s *Server) handleCreate(req *Request) Response {
Status: types.StatusOpen,
}
ctx := s.reqCtx(req)
// Check if any dependencies are discovered-from type
// If so, inherit source_repo from the parent issue
var discoveredFromParentID string