feat: add --notes flag to bd create command (GH#830)
Add --notes flag to bd create command, enabling agents to set notes during issue creation instead of requiring a separate update command. Motivation: AI agents repeatedly tried to use --notes with bd create. Context is fresh at creation time - forcing a two-step process means context loss or workflow interruption. Changes: - cmd/bd/flags.go: Added --notes flag to common issue flags - cmd/bd/create.go: Read and pass notes in both RPC and direct modes - cmd/bd/update.go: Removed duplicate --notes flag definition - internal/rpc/protocol.go: Added Notes field to CreateArgs - internal/rpc/server_issues_epics.go: Process Notes in handleCreate - cmd/bd/create_notes_test.go: Comprehensive test coverage - website/docs/cli-reference/issues.md: Documentation Also adds gitignore entries for Augment AI and .beads/redirect. Co-authored-by: Leon Letto <lettol@vmware.com>
This commit is contained in:
committed by
Steve Yegge
parent
aa2ea48bf2
commit
829c8d1caf
4
.beads/.gitignore
vendored
4
.beads/.gitignore
vendored
@@ -19,6 +19,10 @@ sync-state.json
|
||||
db.sqlite
|
||||
bd.db
|
||||
|
||||
# Worktree redirect file (contains relative path to main repo's .beads/)
|
||||
# Must not be committed as paths would be wrong in other clones
|
||||
redirect
|
||||
|
||||
# Merge artifacts (temporary files from 3-way merge)
|
||||
beads.base.jsonl
|
||||
beads.base.meta.json
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -120,3 +120,8 @@ state.json
|
||||
|
||||
# Claude Code runtime
|
||||
.runtime/
|
||||
|
||||
# Augment AI configuration (user-specific)
|
||||
.augment/
|
||||
.agents/
|
||||
output
|
||||
|
||||
@@ -84,6 +84,7 @@ var createCmd = &cobra.Command{
|
||||
|
||||
design, _ := cmd.Flags().GetString("design")
|
||||
acceptance, _ := cmd.Flags().GetString("acceptance")
|
||||
notes, _ := cmd.Flags().GetString("notes")
|
||||
|
||||
// Parse priority (supports both "1" and "P1" formats)
|
||||
priorityStr, _ := cmd.Flags().GetString("priority")
|
||||
@@ -151,7 +152,7 @@ var createCmd = &cobra.Command{
|
||||
targetRig = prefixOverride
|
||||
}
|
||||
if targetRig != "" {
|
||||
createInRig(cmd, targetRig, title, description, issueType, priority, design, acceptance, assignee, labels, externalRef, wisp)
|
||||
createInRig(cmd, targetRig, title, description, issueType, priority, design, acceptance, notes, assignee, labels, externalRef, wisp)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,7 +178,7 @@ var createCmd = &cobra.Command{
|
||||
if err != nil {
|
||||
debug.Logf("Warning: failed to detect user role: %v\n", err)
|
||||
}
|
||||
|
||||
|
||||
routingConfig := &routing.RoutingConfig{
|
||||
Mode: config.GetString("routing.mode"),
|
||||
DefaultRepo: config.GetString("routing.default"),
|
||||
@@ -185,10 +186,10 @@ var createCmd = &cobra.Command{
|
||||
ContributorRepo: config.GetString("routing.contributor"),
|
||||
ExplicitOverride: repoOverride,
|
||||
}
|
||||
|
||||
|
||||
repoPath = routing.DetermineTargetRepo(routingConfig, userRole, ".")
|
||||
}
|
||||
|
||||
|
||||
// TODO(bd-6x6g): Switch to target repo for multi-repo support
|
||||
// For now, we just log the target repo in debug mode
|
||||
if repoPath != "." {
|
||||
@@ -272,6 +273,7 @@ var createCmd = &cobra.Command{
|
||||
Priority: priority,
|
||||
Design: design,
|
||||
AcceptanceCriteria: acceptance,
|
||||
Notes: notes,
|
||||
Assignee: assignee,
|
||||
ExternalRef: externalRef,
|
||||
EstimatedMinutes: estimatedMinutes,
|
||||
@@ -329,6 +331,7 @@ var createCmd = &cobra.Command{
|
||||
Description: description,
|
||||
Design: design,
|
||||
AcceptanceCriteria: acceptance,
|
||||
Notes: notes,
|
||||
Status: types.StatusOpen,
|
||||
Priority: priority,
|
||||
IssueType: types.IssueType(issueType),
|
||||
@@ -347,7 +350,7 @@ var createCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
ctx := rootCtx
|
||||
|
||||
|
||||
// Check if any dependencies are discovered-from type
|
||||
// If so, inherit source_repo from the parent issue
|
||||
var discoveredFromParentID string
|
||||
@@ -356,16 +359,16 @@ var createCmd = &cobra.Command{
|
||||
if depSpec == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
var depType types.DependencyType
|
||||
var dependsOnID string
|
||||
|
||||
|
||||
if strings.Contains(depSpec, ":") {
|
||||
parts := strings.SplitN(depSpec, ":", 2)
|
||||
if len(parts) == 2 {
|
||||
depType = types.DependencyType(strings.TrimSpace(parts[0]))
|
||||
dependsOnID = strings.TrimSpace(parts[1])
|
||||
|
||||
|
||||
if depType == types.DepDiscoveredFrom && dependsOnID != "" {
|
||||
discoveredFromParentID = dependsOnID
|
||||
break
|
||||
@@ -373,7 +376,7 @@ var createCmd = &cobra.Command{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we found a discovered-from dependency, inherit source_repo from parent
|
||||
if discoveredFromParentID != "" {
|
||||
parentIssue, err := store.GetIssue(ctx, discoveredFromParentID)
|
||||
@@ -382,7 +385,7 @@ var createCmd = &cobra.Command{
|
||||
}
|
||||
// If error getting parent or parent has no source_repo, continue with default
|
||||
}
|
||||
|
||||
|
||||
if err := store.CreateIssue(ctx, issue, actor); err != nil {
|
||||
FatalError("%v", err)
|
||||
}
|
||||
@@ -559,7 +562,7 @@ func init() {
|
||||
|
||||
// createInRig creates an issue in a different rig using --rig flag.
|
||||
// This bypasses the normal daemon/direct flow and directly creates in the target rig.
|
||||
func createInRig(cmd *cobra.Command, rigName, title, description, issueType string, priority int, design, acceptance, assignee string, labels []string, externalRef string, wisp bool) {
|
||||
func createInRig(cmd *cobra.Command, rigName, title, description, issueType string, priority int, design, acceptance, notes, assignee string, labels []string, externalRef string, wisp bool) {
|
||||
ctx := rootCtx
|
||||
|
||||
// Find the town-level beads directory (where routes.jsonl lives)
|
||||
@@ -597,6 +600,7 @@ func createInRig(cmd *cobra.Command, rigName, title, description, issueType stri
|
||||
Description: description,
|
||||
Design: design,
|
||||
AcceptanceCriteria: acceptance,
|
||||
Notes: notes,
|
||||
Status: types.StatusOpen,
|
||||
Priority: priority,
|
||||
IssueType: types.IssueType(issueType),
|
||||
|
||||
208
cmd/bd/create_notes_test.go
Normal file
208
cmd/bd/create_notes_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/rpc"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
// TestCreateWithNotes verifies that the --notes flag works correctly
|
||||
// during issue creation in both direct mode and RPC mode.
|
||||
func TestCreateWithNotes(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
|
||||
s := newTestStore(t, testDB)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("DirectMode_WithNotes", func(t *testing.T) {
|
||||
issue := &types.Issue{
|
||||
Title: "Issue with notes",
|
||||
Notes: "These are my test notes",
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
Status: types.StatusOpen,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify
|
||||
retrieved, err := s.GetIssue(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve issue: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Notes != "These are my test notes" {
|
||||
t.Errorf("expected notes 'These are my test notes', got %q", retrieved.Notes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DirectMode_WithoutNotes", func(t *testing.T) {
|
||||
issue := &types.Issue{
|
||||
Title: "Issue without notes",
|
||||
Priority: 2,
|
||||
IssueType: types.TypeBug,
|
||||
Status: types.StatusOpen,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify
|
||||
retrieved, err := s.GetIssue(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve issue: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Notes != "" {
|
||||
t.Errorf("expected empty notes, got %q", retrieved.Notes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DirectMode_WithNotesAndOtherFields", func(t *testing.T) {
|
||||
issue := &types.Issue{
|
||||
Title: "Full issue with notes",
|
||||
Description: "Detailed description",
|
||||
Design: "Design notes here",
|
||||
AcceptanceCriteria: "All tests pass",
|
||||
Notes: "Additional implementation notes",
|
||||
Priority: 1,
|
||||
IssueType: types.TypeFeature,
|
||||
Status: types.StatusOpen,
|
||||
Assignee: "testuser",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify all fields
|
||||
retrieved, err := s.GetIssue(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve issue: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Title != "Full issue with notes" {
|
||||
t.Errorf("expected title 'Full issue with notes', got %q", retrieved.Title)
|
||||
}
|
||||
if retrieved.Description != "Detailed description" {
|
||||
t.Errorf("expected description, got %q", retrieved.Description)
|
||||
}
|
||||
if retrieved.Design != "Design notes here" {
|
||||
t.Errorf("expected design, got %q", retrieved.Design)
|
||||
}
|
||||
if retrieved.AcceptanceCriteria != "All tests pass" {
|
||||
t.Errorf("expected acceptance criteria, got %q", retrieved.AcceptanceCriteria)
|
||||
}
|
||||
if retrieved.Notes != "Additional implementation notes" {
|
||||
t.Errorf("expected notes 'Additional implementation notes', got %q", retrieved.Notes)
|
||||
}
|
||||
if retrieved.Assignee != "testuser" {
|
||||
t.Errorf("expected assignee 'testuser', got %q", retrieved.Assignee)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DirectMode_NotesWithSpecialCharacters", func(t *testing.T) {
|
||||
specialNotes := "Notes with special chars: \n- Bullet point\n- Another one\n\nAnd \"quotes\" and 'apostrophes'"
|
||||
issue := &types.Issue{
|
||||
Title: "Issue with special char notes",
|
||||
Notes: specialNotes,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
Status: types.StatusOpen,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("failed to create issue: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify
|
||||
retrieved, err := s.GetIssue(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve issue: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Notes != specialNotes {
|
||||
t.Errorf("notes mismatch.\nExpected: %q\nGot: %q", specialNotes, retrieved.Notes)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCreateWithNotesRPC verifies notes field works via RPC protocol
|
||||
func TestCreateWithNotesRPC(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testDB := filepath.Join(tmpDir, ".beads", "beads.db")
|
||||
s := newTestStore(t, testDB)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("RPC_CreateArgs_WithNotes", func(t *testing.T) {
|
||||
// Test that CreateArgs properly includes Notes field
|
||||
args := &rpc.CreateArgs{
|
||||
Title: "RPC test issue",
|
||||
Description: "Testing RPC mode",
|
||||
Notes: "RPC notes field",
|
||||
Priority: 1,
|
||||
IssueType: "task",
|
||||
}
|
||||
|
||||
// Verify the struct has the Notes field populated
|
||||
if args.Notes != "RPC notes field" {
|
||||
t.Errorf("expected Notes field 'RPC notes field', got %q", args.Notes)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("RPC_CreateIssue_WithNotes", func(t *testing.T) {
|
||||
// Simulate what the RPC handler does
|
||||
createArgs := &rpc.CreateArgs{
|
||||
Title: "RPC created issue",
|
||||
Description: "Created via RPC",
|
||||
Design: "RPC design",
|
||||
AcceptanceCriteria: "RPC acceptance",
|
||||
Notes: "RPC implementation notes",
|
||||
Priority: 2,
|
||||
IssueType: "feature",
|
||||
Assignee: "rpcuser",
|
||||
}
|
||||
|
||||
// Create issue as RPC handler would
|
||||
issue := &types.Issue{
|
||||
Title: createArgs.Title,
|
||||
Description: createArgs.Description,
|
||||
Design: createArgs.Design,
|
||||
AcceptanceCriteria: createArgs.AcceptanceCriteria,
|
||||
Notes: createArgs.Notes,
|
||||
Priority: createArgs.Priority,
|
||||
IssueType: types.IssueType(createArgs.IssueType),
|
||||
Assignee: createArgs.Assignee,
|
||||
Status: types.StatusOpen,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.CreateIssue(ctx, issue, "test"); err != nil {
|
||||
t.Fatalf("failed to create issue via RPC simulation: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve and verify
|
||||
retrieved, err := s.GetIssue(ctx, issue.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve issue: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Notes != "RPC implementation notes" {
|
||||
t.Errorf("expected notes 'RPC implementation notes', got %q", retrieved.Notes)
|
||||
}
|
||||
if retrieved.Description != "Created via RPC" {
|
||||
t.Errorf("expected description 'Created via RPC', got %q", retrieved.Description)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -19,6 +19,7 @@ func registerCommonIssueFlags(cmd *cobra.Command) {
|
||||
_ = cmd.Flags().MarkHidden("description-file") // Hidden alias
|
||||
cmd.Flags().String("design", "", "Design notes")
|
||||
cmd.Flags().String("acceptance", "", "Acceptance criteria")
|
||||
cmd.Flags().String("notes", "", "Additional notes")
|
||||
cmd.Flags().String("external-ref", "", "External reference (e.g., 'gh-9', 'jira-ABC')")
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +414,6 @@ func init() {
|
||||
updateCmd.Flags().String("title", "", "New title")
|
||||
updateCmd.Flags().StringP("type", "t", "", "New type (bug|feature|task|epic|chore|merge-request|molecule|gate)")
|
||||
registerCommonIssueFlags(updateCmd)
|
||||
updateCmd.Flags().String("notes", "", "Additional notes")
|
||||
updateCmd.Flags().String("acceptance-criteria", "", "DEPRECATED: use --acceptance")
|
||||
_ = updateCmd.Flags().MarkHidden("acceptance-criteria") // Only fails if flag missing (caught in tests)
|
||||
updateCmd.Flags().IntP("estimate", "e", 0, "Time estimate in minutes (e.g., 60 for 1 hour)")
|
||||
|
||||
@@ -82,6 +82,7 @@ type CreateArgs struct {
|
||||
Priority int `json:"priority"`
|
||||
Design string `json:"design,omitempty"`
|
||||
AcceptanceCriteria string `json:"acceptance_criteria,omitempty"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Assignee string `json:"assignee,omitempty"`
|
||||
ExternalRef string `json:"external_ref,omitempty"` // Link to external issue trackers
|
||||
EstimatedMinutes *int `json:"estimated_minutes,omitempty"` // Time estimate in minutes
|
||||
|
||||
@@ -183,13 +183,16 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
issueID = childID
|
||||
}
|
||||
|
||||
var design, acceptance, assignee, externalRef *string
|
||||
var design, acceptance, notes, assignee, externalRef *string
|
||||
if createArgs.Design != "" {
|
||||
design = &createArgs.Design
|
||||
}
|
||||
if createArgs.AcceptanceCriteria != "" {
|
||||
acceptance = &createArgs.AcceptanceCriteria
|
||||
}
|
||||
if createArgs.Notes != "" {
|
||||
notes = &createArgs.Notes
|
||||
}
|
||||
if createArgs.Assignee != "" {
|
||||
assignee = &createArgs.Assignee
|
||||
}
|
||||
@@ -205,6 +208,7 @@ func (s *Server) handleCreate(req *Request) Response {
|
||||
Priority: createArgs.Priority,
|
||||
Design: strValue(design),
|
||||
AcceptanceCriteria: strValue(acceptance),
|
||||
Notes: strValue(notes),
|
||||
Assignee: strValue(assignee),
|
||||
ExternalRef: externalRef,
|
||||
EstimatedMinutes: createArgs.EstimatedMinutes,
|
||||
|
||||
@@ -21,6 +21,9 @@ bd create <title> [flags]
|
||||
--type, -t Issue type (bug|feature|task|epic|chore)
|
||||
--priority, -p Priority 0-4
|
||||
--description, -d Detailed description
|
||||
--design Design notes
|
||||
--acceptance Acceptance criteria
|
||||
--notes Additional notes
|
||||
--labels, -l Comma-separated labels
|
||||
--parent Parent issue ID
|
||||
--deps Dependencies (type:id format)
|
||||
@@ -37,6 +40,13 @@ bd create "Login fails with special chars" -t bug -p 1
|
||||
bd create "Add export to PDF" -t feature -p 2 \
|
||||
--description="Users want to export reports as PDF files"
|
||||
|
||||
# Feature with design, acceptance, and notes
|
||||
bd create "Implement user authentication" -t feature -p 1 \
|
||||
--description="Add JWT-based authentication" \
|
||||
--design="Use bcrypt for password hashing, JWT for sessions" \
|
||||
--acceptance="All tests pass, security audit complete" \
|
||||
--notes="Consider rate limiting for login attempts"
|
||||
|
||||
# Task with labels
|
||||
bd create "Update CI config" -t task -l "ci,infrastructure"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user