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:
beads/crew/emma
2026-01-01 10:53:59 -08:00
committed by Steve Yegge
parent aa2ea48bf2
commit 829c8d1caf
9 changed files with 249 additions and 13 deletions

4
.beads/.gitignore vendored
View File

@@ -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
View File

@@ -120,3 +120,8 @@ state.json
# Claude Code runtime
.runtime/
# Augment AI configuration (user-specific)
.augment/
.agents/
output

View File

@@ -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
View 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)
}
})
}

View File

@@ -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')")
}

View File

@@ -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)")

View File

@@ -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

View File

@@ -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,

View File

@@ -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"