6 files changed,
+562,
-73
M
Makefile
+9,
-8
1@@ -1,15 +1,16 @@
2
3 build: clean
4- @if command -v nix >/dev/null 2>&1; then \
5- echo "Building with nix..."; \
6- nix build; \
7- else \
8- echo "Building with go..."; \
9- mkdir -p result/bin; \
10- go build -o result/bin/bug *.go; \
11- fi
12+ @echo "Building with go..."
13+ @mkdir -p result/bin
14+ @go build -o result/bin/bug *.go
15 .PHONY: build
16
17+install: build
18+ @cp SKILL.md $(HOME)/.agents/skills/superpowers/working-with-bug/SKILL.md
19+ @echo "Installed skill to superpowers/working-with-bug"
20+ @echo "Symlinking to /usr/local/bin/bug..."
21+ @ln -s result/bin/bug /usr/local/bin/bug 2>/dev/null || true
22+
23 clean:
24 @# Remove result directory/symlink
25 @rm -rf result
M
SKILL.md
+190,
-18
1@@ -104,66 +104,198 @@ bug agent read abc1234
2 - Retrieve documented plans from comments
3 - Check current status before acting
4
5-### bug agent new --title "..." --message "..."
6+### bug agent new --title "..." [--message "..." | --stdin | --from-file <path>]
7
8 Create a new bug/issue as the agent.
9
10 ```bash
11+# Using --message flag (simple strings)
12 bug agent new --title "CI Failure" --message "Build failed on commit abc123"
13+
14+# Using --stdin (pipe content)
15+echo "Build failed on commit abc123" | bug agent new --title "CI Failure" --stdin
16+
17+# Using --from-file (read from file)
18+bug agent new --title "CI Failure" --from-file /tmp/description.md
19 ```
20
21 **Required flags:**
22 - `--title`: Issue title
23-- `--message`: Issue description
24+- One of the following for message content:
25+ - `--message "..."`: Provide message as a command-line string
26+ - `--stdin`: Read message from standard input
27+ - `--from-file <path>`: Read message from a file
28+
29+**Important:** `--message`, `--stdin`, and `--from-file` are **mutually exclusive**. You must choose exactly one method to provide the message content.
30+
31+**Best practice for multi-line markdown content:** Use `--stdin` or `--from-file` instead of `--message`. Long multi-line strings with markdown formatting often fail when passed as shell command-line arguments due to escaping issues.
32
33-**Both flags are required.** This command is strictly non-interactive.
34+**Examples with HEREDOC and temp files:**
35+
36+```bash
37+# Using HEREDOC with --stdin
38+bug agent new --title "Complex Bug" --stdin << 'EOF'
39+## Description
40+
41+This bug involves multiple steps:
42+1. First step
43+2. Second step
44+
45+**Expected:** It should work
46+**Actual:** It fails with error
47+EOF
48+
49+# Using a temp file for large content
50+cat > /tmp/bug_desc.md << 'EOF'
51+## Problem
52+Detailed markdown content here...
53+
54+- List item 1
55+- List item 2
56+EOF
57+bug agent new --title "Complex Bug" --from-file /tmp/bug_desc.md
58+```
59
60-### bug agent comment [bugID] --message "..."
61+### bug agent comment [bugID] [--message "..." | --stdin | --from-file <path>]
62
63 Add a comment to an existing bug.
64
65 ```bash
66+# Using --message flag (simple strings)
67 bug agent comment abc1234 --message "Automated analysis complete"
68+
69+# Using --stdin (pipe content)
70+echo "Analysis complete" | bug agent comment abc1234 --stdin
71+
72+# Using --from-file (read from file)
73+bug agent comment abc1234 --from-file /tmp/comment.md
74 ```
75
76-**Required flag:**
77-- `--message`: Comment text (supports markdown)
78+**Required:** Exactly one of the following for message content:
79+- `--message "..."`: Provide message as a command-line string
80+- `--stdin`: Read message from standard input
81+- `--from-file <path>`: Read message from a file
82+
83+**Important:** `--message`, `--stdin`, and `--from-file` are **mutually exclusive**. You must choose exactly one method to provide the message content.
84+
85+**Best practice for multi-line markdown content:** Use `--stdin` or `--from-file` instead of `--message`. Long multi-line strings with markdown formatting often fail when passed as shell command-line arguments due to escaping issues.
86+
87+**Examples with HEREDOC and temp files:**
88+
89+```bash
90+# Using HEREDOC with --stdin
91+bug agent comment abc1234 --stdin << 'EOF'
92+## Implementation Plan
93+
94+### Task 1: [Component Name]
95+**Files:**
96+- Create: path/to/file.ts
97+
98+**Step 1: Write the failing test**
99+[code example]
100+EOF
101+
102+# Using a temp file for large content
103+cat > /tmp/progress.md << 'EOF'
104+## Progress Update
105+
106+Completed the following:
107+- Item 1
108+- Item 2
109+EOF
110+bug agent comment abc1234 --from-file /tmp/progress.md
111+```
112
113 **Use this to:**
114 - Document implementation plans
115 - Add progress updates
116 - Record findings or analysis results
117
118-**Tip:** For multi-line plans, write the markdown content directly after `--message`. The shell handles line breaks appropriately.
119-
120-### bug agent edit [bugID] --title "..." --message "..."
121+### bug agent edit [bugID] --title "..." [--message "..." | --stdin | --from-file <path>]
122
123 Edit an issue's title and/or description.
124
125 ```bash
126+# Edit title only
127 bug agent edit abc1234 --title "New Title"
128+
129+# Edit message using --message flag (simple strings)
130 bug agent edit abc1234 --message "New description"
131-bug agent edit abc1234 --title "Title" --message "Description"
132+
133+# Edit message using --stdin
134+echo "New description" | bug agent edit abc1234 --stdin
135+
136+# Edit message using --from-file
137+bug agent edit abc1234 --from-file /tmp/new_desc.md
138+
139+# Edit both title and message
140+bug agent edit abc1234 --title "New Title" --message "New description"
141 ```
142
143-**Flags (at least one required):**
144-- `--title`: New issue title
145-- `--message`: New issue description
146+**Flags:**
147+- `--title`: New issue title (optional, but at least one flag must be provided)
148+- One of the following for message content (optional):
149+ - `--message "..."`: Provide message as a command-line string
150+ - `--stdin`: Read message from standard input
151+ - `--from-file <path>`: Read message from a file
152
153-**Note:** When editing bugs, at least one of `--title` or `--message` must be provided.
154+**Important:** When editing the message, `--message`, `--stdin`, and `--from-file` are **mutually exclusive**. You must choose exactly one method to provide the message content.
155
156-### bug agent edit [commentID] --message "..."
157+**Best practice for multi-line markdown content:** Use `--stdin` or `--from-file` instead of `--message`. Long multi-line strings with markdown formatting often fail when passed as shell command-line arguments due to escaping issues.
158+
159+**Examples with HEREDOC:**
160+
161+```bash
162+# Update description with HEREDOC
163+bug agent edit abc1234 --stdin << 'EOF'
164+## Updated Description
165+
166+New details about this bug:
167+- Point 1
168+- Point 2
169+EOF
170+```
171+
172+**Note:** When editing bugs, at least one of `--title` or a message option must be provided.
173+
174+### bug agent edit [commentID] [--message "..." | --stdin | --from-file <path>]
175
176 Edit an existing comment.
177
178 ```bash
179+# Using --message flag (simple strings)
180 bug agent edit def5678 --message "Updated comment text"
181+
182+# Using --stdin (pipe content)
183+echo "Updated comment text" | bug agent edit def5678 --stdin
184+
185+# Using --from-file (read from file)
186+bug agent edit def5678 --from-file /tmp/updated_comment.md
187 ```
188
189-**Required flag:**
190-- `--message`: New comment text
191+**Required:** Exactly one of the following for message content:
192+- `--message "..."`: Provide message as a command-line string
193+- `--stdin`: Read message from standard input
194+- `--from-file <path>`: Read message from a file
195+
196+**Important:**
197+- Comments have no title field. Only message options are accepted when editing a comment ID.
198+- `--message`, `--stdin`, and `--from-file` are **mutually exclusive**. You must choose exactly one method to provide the message content.
199
200-**Important:** Comments have no title field. Only `--message` is accepted when editing a comment ID.
201+**Best practice for multi-line markdown content:** Use `--stdin` or `--from-file` instead of `--message`. Long multi-line strings with markdown formatting often fail when passed as shell command-line arguments due to escaping issues.
202+
203+**Examples with HEREDOC:**
204+
205+```bash
206+# Update comment with HEREDOC
207+bug agent edit def5678 --stdin << 'EOF'
208+## Updated Analysis
209+
210+New findings:
211+- Finding 1
212+- Finding 2
213+EOF
214+```
215
216 ### bug agent open [bugID]
217
218@@ -234,6 +366,46 @@ The `bug agent edit` command automatically detects the type and behaves accordin
219 - For bugs: accepts `--title` and/or `--message`
220 - For comments: accepts only `--message`
221
222+### Message Content Options
223+
224+Agent commands that require message content (`bug agent new`, `bug agent comment`, `bug agent edit`) support three mutually exclusive methods for providing the message:
225+
226+| Option | When to Use | Example |
227+|--------|-------------|---------|
228+| `--message "..."` | Short, simple text without special characters | `bug agent new --title "Bug" --message "It broke"` |
229+| `--stdin` | Multi-line content, markdown, or piped input | `cat description.md \| bug agent new --title "Bug" --stdin` |
230+| `--from-file <path>` | Large content already in a file | `bug agent new --title "Bug" --from-file /tmp/desc.md` |
231+
232+**Important rules:**
233+1. **Mutually exclusive:** You must use exactly one of `--message`, `--stdin`, or `--from-file` per command
234+2. **Best practice for markdown:** Use `--stdin` or `--from-file` for multi-line markdown content to avoid shell escaping issues
235+3. **Error handling:** If you specify multiple options, the command will fail with a clear error message explaining the conflict
236+
237+**HEREDOC patterns (recommended for agents):**
238+
239+```bash
240+# Pattern 1: Inline HEREDOC to stdin
241+bug agent comment abc1234 --stdin << 'EOF'
242+## Implementation Plan
243+
244+### Task 1
245+- Step 1
246+- Step 2
247+EOF
248+
249+# Pattern 2: Write to temp file first
250+plan_file=$(mktemp)
251+cat > "$plan_file" << 'EOF'
252+## Implementation Plan
253+
254+### Task 1
255+- Step 1
256+- Step 2
257+EOF
258+bug agent comment abc1234 --from-file "$plan_file"
259+rm "$plan_file"
260+```
261+
262 ## Complete Workflow Example
263
264 **Scenario:** User asks "Can you fix the bug with ID abc1234?"
+127,
-7
1@@ -3,6 +3,7 @@
2 package main
3
4 import (
5+ "os"
6 "os/exec"
7 "strings"
8 "testing"
9@@ -64,7 +65,7 @@ func TestAgentCommentCommand_WithFlag(t *testing.T) {
10
11 // Add a comment as agent using the flag
12 commentText := "This is an automated agent comment"
13- if err := runAgentComment(tmpDir, bugID, commentText); err != nil {
14+ if err := runAgentComment(tmpDir, bugID, commentText, "", false); err != nil {
15 t.Fatalf("runAgentComment failed: %v", err)
16 }
17
18@@ -155,7 +156,7 @@ func TestAgentCommentCommand_WithShortID(t *testing.T) {
19
20 // Add a comment as agent using short ID
21 commentText := "Agent comment via short ID"
22- if err := runAgentComment(tmpDir, shortID, commentText); err != nil {
23+ if err := runAgentComment(tmpDir, shortID, commentText, "", false); err != nil {
24 t.Fatalf("runAgentComment with short ID failed: %v", err)
25 }
26
27@@ -228,7 +229,7 @@ func TestAgentCommentCommand_MissingAgentIdentity(t *testing.T) {
28 repo.Close()
29
30 // Try to add a comment as agent (should fail)
31- err = runAgentComment(tmpDir, bugID, "Test comment")
32+ err = runAgentComment(tmpDir, bugID, "Test comment", "", false)
33 if err == nil {
34 t.Error("expected error when agent identity not found, got nil")
35 }
36@@ -284,7 +285,7 @@ func TestAgentCommentCommand_EmptyMessageError(t *testing.T) {
37 repo.Close()
38
39 // Try with empty message
40- err = runAgentComment(tmpDir, bugID, " ")
41+ err = runAgentComment(tmpDir, bugID, " ", "", false)
42 if err == nil {
43 t.Error("expected error for empty message, got nil")
44 }
45@@ -314,7 +315,7 @@ func TestAgentCommentCommand_InvalidBugID(t *testing.T) {
46 }
47
48 // Try to add a comment to non-existent bug
49- err := runAgentComment(tmpDir, "nonexistent", "Test comment")
50+ err := runAgentComment(tmpDir, "nonexistent", "Test comment", "", false)
51 if err == nil {
52 t.Error("expected error for invalid bug ID, got nil")
53 }
54@@ -377,7 +378,7 @@ func TestAgentCommentCommand_MultipleComments(t *testing.T) {
55 }
56
57 for _, comment := range comments {
58- if err := runAgentComment(tmpDir, bugID, comment); err != nil {
59+ if err := runAgentComment(tmpDir, bugID, comment, "", false); err != nil {
60 t.Fatalf("runAgentComment failed: %v", err)
61 }
62 }
63@@ -466,7 +467,7 @@ func TestAgentCommentCommand_WithMarkdown(t *testing.T) {
64 ## Details
65 All tests passed successfully.`
66
67- if err := runAgentComment(tmpDir, bugID, markdownComment); err != nil {
68+ if err := runAgentComment(tmpDir, bugID, markdownComment, "", false); err != nil {
69 t.Fatalf("runAgentComment failed: %v", err)
70 }
71
72@@ -491,3 +492,122 @@ All tests passed successfully.`
73 t.Errorf("comment message does not match expected markdown:\ngot:\n%s\nwant:\n%s", snap.Comments[1].Message, markdownComment)
74 }
75 }
76+
77+// TestAgentCommentCommand_WithStdin tests adding a comment using --stdin flag
78+func TestAgentCommentCommand_WithStdin(t *testing.T) {
79+ tmpDir := t.TempDir()
80+
81+ // Initialize a git repo
82+ initCmd := exec.Command("git", "init")
83+ initCmd.Dir = tmpDir
84+ if err := initCmd.Run(); err != nil {
85+ t.Fatalf("failed to init git repo: %v", err)
86+ }
87+
88+ // Initialize identities
89+ if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
90+ t.Fatalf("failed to create user identity: %v", err)
91+ }
92+ if err := createIdentity(tmpDir, "agent", "", false); err != nil {
93+ t.Fatalf("failed to create agent identity: %v", err)
94+ }
95+
96+ // Create a bug
97+ title := "Test Bug for Stdin Comment"
98+ if err := runNew(tmpDir, title, ""); err != nil {
99+ t.Fatalf("runNew failed: %v", err)
100+ }
101+
102+ // Get the bug ID
103+ repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
104+ if err != nil {
105+ t.Fatalf("failed to open repo: %v", err)
106+ }
107+
108+ var bugID string
109+ for streamedBug := range bug.ReadAll(repo) {
110+ if streamedBug.Err != nil {
111+ t.Fatalf("failed to read bug: %v", streamedBug.Err)
112+ }
113+ b := streamedBug.Entity
114+ snap := b.Compile()
115+ if snap.Title == title {
116+ bugID = b.Id().String()
117+ break
118+ }
119+ }
120+ repo.Close()
121+
122+ // Multi-line content with markdown to test via stdin
123+ stdinContent := `# Implementation Plan
124+
125+## Task 1: Setup
126+**Files:**
127+- Create: path/to/file.go
128+- Test: path/to/file_test.go
129+
130+**Step 1: Initialize**
131+Initialize the configuration:
132+
133+` + "```go" + `
134+func Initialize() error {
135+ return nil
136+}
137+` + "```" + `
138+
139+## Next Steps
140+1. Run tests
141+2. Verify results
142+3. Document changes`
143+
144+ // Create a pipe to simulate stdin
145+ r, w, err := os.Pipe()
146+ if err != nil {
147+ t.Fatalf("failed to create pipe: %v", err)
148+ }
149+
150+ // Write content to the pipe in a goroutine
151+ go func() {
152+ defer w.Close()
153+ w.WriteString(stdinContent)
154+ }()
155+
156+ // Save original stdin and replace with our pipe
157+ oldStdin := os.Stdin
158+ os.Stdin = r
159+ defer func() { os.Stdin = oldStdin }()
160+
161+ // Add a comment as agent using stdin flag
162+ // We pass empty message and true for useStdin
163+ if err := runAgentComment(tmpDir, bugID, "", "", true); err != nil {
164+ t.Fatalf("runAgentComment with stdin failed: %v", err)
165+ }
166+
167+ // Verify comment was added with stdin content
168+ repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
169+ if err != nil {
170+ t.Fatalf("failed to open repo: %v", err)
171+ }
172+ defer repo.Close()
173+
174+ b, err := bug.Read(repo, entity.Id(bugID))
175+ if err != nil {
176+ t.Fatalf("failed to read bug: %v", err)
177+ }
178+
179+ snap := b.Compile()
180+ if len(snap.Comments) != 2 {
181+ t.Errorf("expected 2 comments, got %d", len(snap.Comments))
182+ }
183+
184+ // Content should be trimmed but preserve markdown
185+ expectedContent := strings.TrimSpace(stdinContent)
186+ if snap.Comments[1].Message != expectedContent {
187+ t.Errorf("comment message does not match expected stdin content:\ngot:\n%s\nwant:\n%s", snap.Comments[1].Message, expectedContent)
188+ }
189+
190+ // Verify agent is the author
191+ if snap.Comments[1].Author.Name() != "agent" {
192+ t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "agent")
193+ }
194+}
+12,
-12
1@@ -35,7 +35,7 @@ func TestAgentEditCommand_EditIssue(t *testing.T) {
2 }
3
4 // Create a bug as agent
5- if err := runAgentNew(tmpDir, "Original Title", "Original description"); err != nil {
6+ if err := runAgentNew(tmpDir, "Original Title", "Original description", "", false); err != nil {
7 t.Fatalf("runAgentNew failed: %v", err)
8 }
9
10@@ -66,7 +66,7 @@ func TestAgentEditCommand_EditIssue(t *testing.T) {
11 // Edit as agent
12 newTitle := "Agent Updated Title"
13 newMessage := "Agent updated description"
14- if err := runAgentEdit(tmpDir, bugID, newTitle, newMessage); err != nil {
15+ if err := runAgentEdit(tmpDir, bugID, newTitle, newMessage, "", false); err != nil {
16 t.Fatalf("runAgentEdit failed: %v", err)
17 }
18
19@@ -114,7 +114,7 @@ func TestAgentEditCommand_EditComment(t *testing.T) {
20 }
21
22 // Create a bug as agent
23- if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
24+ if err := runAgentNew(tmpDir, "Test Bug", "Description", "", false); err != nil {
25 t.Fatalf("runAgentNew failed: %v", err)
26 }
27
28@@ -140,7 +140,7 @@ func TestAgentEditCommand_EditComment(t *testing.T) {
29
30 // Add a comment as agent
31 originalComment := "Original agent comment"
32- if err := runAgentComment(tmpDir, bugID, originalComment); err != nil {
33+ if err := runAgentComment(tmpDir, bugID, originalComment, "", false); err != nil {
34 t.Fatalf("runAgentComment failed: %v", err)
35 }
36
37@@ -163,7 +163,7 @@ func TestAgentEditCommand_EditComment(t *testing.T) {
38
39 // Edit the comment as agent
40 newComment := "Updated agent comment"
41- if err := runAgentEdit(tmpDir, commentID, "", newComment); err != nil {
42+ if err := runAgentEdit(tmpDir, commentID, "", newComment, "", false); err != nil {
43 t.Fatalf("runAgentEdit failed: %v", err)
44 }
45
46@@ -211,7 +211,7 @@ func TestAgentEditCommand_RequiresMessage(t *testing.T) {
47 }
48
49 // Create a bug as agent
50- if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
51+ if err := runAgentNew(tmpDir, "Test Bug", "Description", "", false); err != nil {
52 t.Fatalf("runAgentNew failed: %v", err)
53 }
54
55@@ -236,7 +236,7 @@ func TestAgentEditCommand_RequiresMessage(t *testing.T) {
56 repo.Close()
57
58 // Add a comment as agent
59- if err := runAgentComment(tmpDir, bugID, "Original comment"); err != nil {
60+ if err := runAgentComment(tmpDir, bugID, "Original comment", "", false); err != nil {
61 t.Fatalf("runAgentComment failed: %v", err)
62 }
63
64@@ -255,7 +255,7 @@ func TestAgentEditCommand_RequiresMessage(t *testing.T) {
65 repo.Close()
66
67 // Try to edit comment without message
68- err = runAgentEdit(tmpDir, commentID, "", "")
69+ err = runAgentEdit(tmpDir, commentID, "", "", "", false)
70 if err == nil {
71 t.Error("expected error when editing comment without message, got nil")
72 }
73@@ -288,7 +288,7 @@ func TestAgentEditCommand_RequiresAtLeastOneField(t *testing.T) {
74 }
75
76 // Create a bug as agent
77- if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
78+ if err := runAgentNew(tmpDir, "Test Bug", "Description", "", false); err != nil {
79 t.Fatalf("runAgentNew failed: %v", err)
80 }
81
82@@ -313,7 +313,7 @@ func TestAgentEditCommand_RequiresAtLeastOneField(t *testing.T) {
83 repo.Close()
84
85 // Try to edit without any flags
86- err = runAgentEdit(tmpDir, bugID, "", "")
87+ err = runAgentEdit(tmpDir, bugID, "", "", "", false)
88 if err == nil {
89 t.Error("expected error when editing without any fields, got nil")
90 }
91@@ -348,7 +348,7 @@ func TestAgentEditCommand_OnlyTitle(t *testing.T) {
92 // Create a bug as agent
93 originalTitle := "Original Title"
94 originalDesc := "Original description"
95- if err := runAgentNew(tmpDir, originalTitle, originalDesc); err != nil {
96+ if err := runAgentNew(tmpDir, originalTitle, originalDesc, "", false); err != nil {
97 t.Fatalf("runAgentNew failed: %v", err)
98 }
99
100@@ -374,7 +374,7 @@ func TestAgentEditCommand_OnlyTitle(t *testing.T) {
101
102 // Edit only title as agent
103 newTitle := "New Title"
104- if err := runAgentEdit(tmpDir, bugID, newTitle, ""); err != nil {
105+ if err := runAgentEdit(tmpDir, bugID, newTitle, "", "", false); err != nil {
106 t.Fatalf("runAgentEdit failed: %v", err)
107 }
108
+110,
-4
1@@ -3,6 +3,7 @@
2 package main
3
4 import (
5+ "os"
6 "os/exec"
7 "strings"
8 "testing"
9@@ -33,7 +34,7 @@ func TestAgentNewCommand_WithFlags(t *testing.T) {
10 // Create a bug as agent using flags
11 title := "Agent Created Bug"
12 message := "This bug was created by an automated agent"
13- if err := runAgentNew(tmpDir, title, message); err != nil {
14+ if err := runAgentNew(tmpDir, title, message, "", false); err != nil {
15 t.Fatalf("runAgentNew failed: %v", err)
16 }
17
18@@ -89,7 +90,7 @@ func TestAgentNewCommand_MissingAgentIdentity(t *testing.T) {
19 }
20
21 // Try to create a bug as agent (should fail)
22- err := runAgentNew(tmpDir, "Title", "Message")
23+ err := runAgentNew(tmpDir, "Title", "Message", "", false)
24 if err == nil {
25 t.Error("expected error when agent identity not found, got nil")
26 }
27@@ -119,7 +120,7 @@ func TestAgentNewCommand_EmptyTitleError(t *testing.T) {
28 }
29
30 // Try with empty title
31- err := runAgentNew(tmpDir, " ", "Message")
32+ err := runAgentNew(tmpDir, " ", "Message", "", false)
33 if err == nil {
34 t.Error("expected error for empty title, got nil")
35 }
36@@ -149,7 +150,7 @@ func TestAgentNewCommand_EmptyMessageError(t *testing.T) {
37 }
38
39 // Try with empty message
40- err := runAgentNew(tmpDir, "Title", " ")
41+ err := runAgentNew(tmpDir, "Title", " ", "", false)
42 if err == nil {
43 t.Error("expected error for empty message, got nil")
44 }
45@@ -231,3 +232,108 @@ func TestGetAgentIdentity_NotFound(t *testing.T) {
46 t.Errorf("expected 'agent identity not found' error, got: %v", err)
47 }
48 }
49+
50+// TestReadMessageSource_FromFile tests reading message from file
51+func TestReadMessageSource_FromFile(t *testing.T) {
52+ tmpDir := t.TempDir()
53+
54+ // Create a test file with message content
55+ messageContent := "This is a test message from file\nWith multiple lines\nAnd **markdown** formatting!"
56+ testFile := tmpDir + "/test_message.txt"
57+ if err := os.WriteFile(testFile, []byte(messageContent), 0644); err != nil {
58+ t.Fatalf("failed to create test file: %v", err)
59+ }
60+
61+ // Test reading from file
62+ result, err := readMessageSource(testFile, false, "", true)
63+ if err != nil {
64+ t.Fatalf("readMessageSource failed: %v", err)
65+ }
66+
67+ // Content should be trimmed of whitespace
68+ expected := strings.TrimSpace(messageContent)
69+ if result != expected {
70+ t.Errorf("result = %q, want %q", result, expected)
71+ }
72+}
73+
74+// TestReadMessageSource_FromFile_NotFound tests error when file doesn't exist
75+func TestReadMessageSource_FromFile_NotFound(t *testing.T) {
76+ result, err := readMessageSource("/nonexistent/path/file.txt", false, "", true)
77+ if err == nil {
78+ t.Error("expected error for non-existent file, got nil")
79+ }
80+
81+ if result != "" {
82+ t.Errorf("expected empty result, got %q", result)
83+ }
84+
85+ if !strings.Contains(err.Error(), "failed to read message from file") {
86+ t.Errorf("expected 'failed to read message from file' error, got: %v", err)
87+ }
88+}
89+
90+// TestReadMessageSource_MessageFlag tests using message flag
91+func TestReadMessageSource_MessageFlag(t *testing.T) {
92+ message := "Test message from flag"
93+ result, err := readMessageSource("", false, message, true)
94+ if err != nil {
95+ t.Fatalf("readMessageSource failed: %v", err)
96+ }
97+
98+ if result != message {
99+ t.Errorf("result = %q, want %q", result, message)
100+ }
101+}
102+
103+// TestReadMessageSource_MultipleSources tests error when multiple sources specified
104+func TestReadMessageSource_MultipleSources(t *testing.T) {
105+ // Test --message and --stdin
106+ _, err := readMessageSource("", true, "message", true)
107+ if err == nil {
108+ t.Error("expected error for multiple sources, got nil")
109+ }
110+ if !strings.Contains(err.Error(), "multiple message sources specified") {
111+ t.Errorf("expected 'multiple message sources specified' error, got: %v", err)
112+ }
113+
114+ // Test --message and --from-file
115+ _, err = readMessageSource("/path/to/file", false, "message", true)
116+ if err == nil {
117+ t.Error("expected error for multiple sources, got nil")
118+ }
119+ if !strings.Contains(err.Error(), "multiple message sources specified") {
120+ t.Errorf("expected 'multiple message sources specified' error, got: %v", err)
121+ }
122+
123+ // Test --stdin and --from-file
124+ _, err = readMessageSource("/path/to/file", true, "", true)
125+ if err == nil {
126+ t.Error("expected error for multiple sources, got nil")
127+ }
128+ if !strings.Contains(err.Error(), "multiple message sources specified") {
129+ t.Errorf("expected 'multiple message sources specified' error, got: %v", err)
130+ }
131+}
132+
133+// TestReadMessageSource_NoSource tests error when no source specified but required
134+func TestReadMessageSource_NoSource(t *testing.T) {
135+ _, err := readMessageSource("", false, "", true)
136+ if err == nil {
137+ t.Error("expected error for no source, got nil")
138+ }
139+ if !strings.Contains(err.Error(), "message content required") {
140+ t.Errorf("expected 'message content required' error, got: %v", err)
141+ }
142+}
143+
144+// TestReadMessageSource_NoSourceOptional tests no error when no source and not required
145+func TestReadMessageSource_NoSourceOptional(t *testing.T) {
146+ result, err := readMessageSource("", false, "", false)
147+ if err != nil {
148+ t.Fatalf("readMessageSource failed: %v", err)
149+ }
150+ if result != "" {
151+ t.Errorf("expected empty result, got %q", result)
152+ }
153+}
M
main.go
+114,
-24
1@@ -1336,6 +1336,14 @@ var (
2 agentEditTitle string
3 agentEditMsg string
4 statusFlag string
5+
6+ // Agent command stdin/from-file flags
7+ agentNewStdinFlag bool
8+ agentNewFromFile string
9+ agentCommentStdinFlag bool
10+ agentCommentFromFile string
11+ agentEditStdinFlag bool
12+ agentEditFromFile string
13 )
14
15 var rootCmd = &cobra.Command{
16@@ -1490,7 +1498,7 @@ Example:
17 bug agent new --title "CI Failure" --message "Build failed on commit abc123"
18 bug agent new -t "Bug found" -m "Automated scan detected issue"`,
19 RunE: func(cmd *cobra.Command, args []string) error {
20- return runAgentNew(repoPath, agentTitle, agentMessage)
21+ return runAgentNew(repoPath, agentTitle, agentMessage, agentNewFromFile, agentNewStdinFlag)
22 },
23 }
24
25@@ -1655,7 +1663,7 @@ Example:
26 Args: cobra.ExactArgs(1),
27 RunE: func(cmd *cobra.Command, args []string) error {
28 commentBugID = args[0]
29- return runAgentComment(repoPath, commentBugID, agentCommentMsg)
30+ return runAgentComment(repoPath, commentBugID, agentCommentMsg, agentCommentFromFile, agentCommentStdinFlag)
31 },
32 }
33
34@@ -1676,7 +1684,7 @@ For comments:
35 bug agent edit def5678 --message "Updated comment text"`,
36 Args: cobra.ExactArgs(1),
37 RunE: func(cmd *cobra.Command, args []string) error {
38- return runAgentEdit(repoPath, args[0], agentEditTitle, agentEditMsg)
39+ return runAgentEdit(repoPath, args[0], agentEditTitle, agentEditMsg, agentEditFromFile, agentEditStdinFlag)
40 },
41 }
42
43@@ -1740,16 +1748,18 @@ func init() {
44
45 // Agent new command flags - both required, no shorthand to force explicit usage
46 agentNewCmd.Flags().StringVar(&agentTitle, "title", "", "issue title (required)")
47- agentNewCmd.Flags().StringVar(&agentMessage, "message", "", "issue description (required)")
48+ agentNewCmd.Flags().StringVar(&agentMessage, "message", "", "issue description")
49+ agentNewCmd.Flags().BoolVar(&agentNewStdinFlag, "stdin", false, "read message from stdin")
50+ agentNewCmd.Flags().StringVar(&agentNewFromFile, "from-file", "", "read message from file")
51 agentNewCmd.MarkFlagRequired("title")
52- agentNewCmd.MarkFlagRequired("message")
53
54 // Comment command flags - message is optional (opens editor if not provided)
55 commentCmd.Flags().StringVarP(&commentMessage, "message", "m", "", "comment message (opens editor if not provided)")
56
57 // Agent comment command flags - message is required (non-interactive)
58- agentCommentCmd.Flags().StringVarP(&agentCommentMsg, "message", "m", "", "comment message (required)")
59- agentCommentCmd.MarkFlagRequired("message")
60+ agentCommentCmd.Flags().StringVarP(&agentCommentMsg, "message", "m", "", "comment message")
61+ agentCommentCmd.Flags().BoolVar(&agentCommentStdinFlag, "stdin", false, "read message from stdin")
62+ agentCommentCmd.Flags().StringVar(&agentCommentFromFile, "from-file", "", "read message from file")
63
64 // Edit command flags - both optional (opens editor if neither provided)
65 editCmd.Flags().StringVarP(&editTitle, "title", "t", "", "new issue title")
66@@ -1758,6 +1768,8 @@ func init() {
67 // Agent edit command flags - at least one required
68 agentEditCmd.Flags().StringVar(&agentEditTitle, "title", "", "new issue title")
69 agentEditCmd.Flags().StringVarP(&agentEditMsg, "message", "m", "", "new issue description or comment text")
70+ agentEditCmd.Flags().BoolVar(&agentEditStdinFlag, "stdin", false, "read message from stdin")
71+ agentEditCmd.Flags().StringVar(&agentEditFromFile, "from-file", "", "read message from file")
72
73 rootCmd.AddCommand(listCmd)
74 rootCmd.AddCommand(initCmd)
75@@ -1781,15 +1793,82 @@ func init() {
76 rootCmd.AddCommand(agentCmd)
77 }
78
79+// readMessageSource reads message content from various sources based on flags.
80+// Returns error if multiple sources are specified or if required but none provided.
81+func readMessageSource(fromFile string, useStdin bool, messageFlag string, requireMessage bool) (string, error) {
82+ // Count how many message sources are specified
83+ sourceCount := 0
84+ if fromFile != "" {
85+ sourceCount++
86+ }
87+ if useStdin {
88+ sourceCount++
89+ }
90+ if messageFlag != "" {
91+ sourceCount++
92+ }
93+
94+ // Validate that only one source is specified
95+ if sourceCount > 1 {
96+ return "", fmt.Errorf(`multiple message sources specified
97+
98+Only one of the following can be used to provide message content:
99+ --message "<string>" Provide message as a command-line string
100+ --stdin Read message from standard input
101+ --from-file <path> Read message from a file
102+
103+Examples:
104+ bug agent new --title "Bug" --message "Description here"
105+ echo "Description" | bug agent new --title "Bug" --stdin
106+ bug agent new --title "Bug" --from-file /tmp/description.md`)
107+ }
108+
109+ // If no source specified and message is required, return error
110+ if sourceCount == 0 && requireMessage {
111+ return "", fmt.Errorf(`message content required
112+
113+Provide message content using one of:
114+ --message "<string>" Provide message as a command-line string
115+ --stdin Read message from standard input
116+ --from-file <path> Read message from a file
117+
118+Run 'bug agent <command> --help' for more information.`)
119+ }
120+
121+ // Read from file if specified
122+ if fromFile != "" {
123+ content, err := os.ReadFile(fromFile)
124+ if err != nil {
125+ return "", fmt.Errorf("failed to read message from file %q: %w\n\nCheck that the file exists and is readable, then try again.", fromFile, err)
126+ }
127+ return strings.TrimSpace(string(content)), nil
128+ }
129+
130+ // Read from stdin if specified
131+ if useStdin {
132+ content, err := io.ReadAll(os.Stdin)
133+ if err != nil {
134+ return "", fmt.Errorf("failed to read message from stdin: %w\n\nEnsure stdin is properly connected and try again.", err)
135+ }
136+ return strings.TrimSpace(string(content)), nil
137+ }
138+
139+ // Fall back to message flag
140+ return messageFlag, nil
141+}
142+
143 // runAgentNew creates a new bug/issue using the agent identity
144 // Both title and message are required - this is non-interactive
145-func runAgentNew(repoPath, title, message string) error {
146- // Validate inputs (both required for agent commands)
147+func runAgentNew(repoPath, title, message string, fromFile string, useStdin bool) error {
148+ // Validate title (still required)
149 if strings.TrimSpace(title) == "" {
150 return fmt.Errorf("title cannot be empty")
151 }
152- if strings.TrimSpace(message) == "" {
153- return fmt.Errorf("message cannot be empty")
154+
155+ // Read message from appropriate source
156+ messageContent, err := readMessageSource(fromFile, useStdin, message, true)
157+ if err != nil {
158+ return err
159 }
160
161 // Open repository
162@@ -1807,7 +1886,7 @@ func runAgentNew(repoPath, title, message string) error {
163
164 // Create the bug
165 unixTime := time.Now().Unix()
166- newBug, _, err := bug.Create(author, unixTime, title, message, nil, nil)
167+ newBug, _, err := bug.Create(author, unixTime, title, messageContent, nil, nil)
168 if err != nil {
169 return fmt.Errorf("failed to create bug: %w", err)
170 }
171@@ -1903,10 +1982,11 @@ func runComment(repoPath, bugIDStr, message string) error {
172
173 // runAgentComment adds a comment to an existing bug as the agent
174 // Message is required - this is non-interactive
175-func runAgentComment(repoPath, bugIDStr, message string) error {
176- // Validate message (required for agent commands)
177- if strings.TrimSpace(message) == "" {
178- return fmt.Errorf("message cannot be empty")
179+func runAgentComment(repoPath, bugIDStr, message string, fromFile string, useStdin bool) error {
180+ // Read message from appropriate source
181+ messageContent, err := readMessageSource(fromFile, useStdin, message, true)
182+ if err != nil {
183+ return err
184 }
185
186 // Resolve bug ID
187@@ -1930,7 +2010,7 @@ func runAgentComment(repoPath, bugIDStr, message string) error {
188
189 // Add the comment
190 unixTime := time.Now().Unix()
191- commentID, _, err := bug.AddComment(b, author, unixTime, message, nil, nil)
192+ commentID, _, err := bug.AddComment(b, author, unixTime, messageContent, nil, nil)
193 if err != nil {
194 return fmt.Errorf("failed to add comment: %w", err)
195 }
196@@ -2149,10 +2229,16 @@ func editComment(repo repository.ClockedRepo, b *bug.Bug, comment *bug.Comment,
197
198 // runAgentEdit edits an issue or comment as the agent identity
199 // This is non-interactive and requires at least one of --title or --message
200-func runAgentEdit(repoPath, idStr, newTitle, newMessage string) error {
201- // Validate that at least one field is being updated
202- if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
203- return fmt.Errorf("either --title or --message must be provided")
204+func runAgentEdit(repoPath, idStr, newTitle, newMessage string, fromFile string, useStdin bool) error {
205+ // Read message from appropriate source if provided
206+ var messageContent string
207+ hasMessageSource := fromFile != "" || useStdin || newMessage != ""
208+ if hasMessageSource {
209+ var err error
210+ messageContent, err = readMessageSource(fromFile, useStdin, newMessage, false)
211+ if err != nil {
212+ return err
213+ }
214 }
215
216 // Resolve the ID (bug or comment)
217@@ -2179,13 +2265,17 @@ func runAgentEdit(repoPath, idStr, newTitle, newMessage string) error {
218 var editErr error
219 switch resolved.Type {
220 case IDTypeBug:
221- editErr = editBugAgent(repo, resolved.Bug, author, unixTime, newTitle, newMessage)
222+ // For bugs, require at least title or message
223+ if strings.TrimSpace(newTitle) == "" && messageContent == "" {
224+ return fmt.Errorf("either --title or --message must be provided")
225+ }
226+ editErr = editBugAgent(repo, resolved.Bug, author, unixTime, newTitle, messageContent)
227 case IDTypeComment:
228 // For comments, only message is applicable
229- if strings.TrimSpace(newMessage) == "" {
230+ if messageContent == "" {
231 return fmt.Errorf("--message is required when editing a comment")
232 }
233- editErr = editCommentAgent(repo, resolved.Bug, resolved.Comment, author, unixTime, newMessage)
234+ editErr = editCommentAgent(repo, resolved.Bug, resolved.Comment, author, unixTime, messageContent)
235 default:
236 return fmt.Errorf("unknown ID type")
237 }