Add --from-file and --stdin options for agents to provide messages
feat: add --stdin and --from-file flags to agent commands feat: implement readMessageSource helper function feat: integrate message source into agent new command feat: integrate message source into agent comment command feat: integrate message source into agent edit command feat: add validation to prevent conflicting message flags test: add integration tests for --stdin option test: add integration tests for --from-file option docs: update SKILL.md with --stdin and --from-file options
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?"
M agent_comment_integration_test.go
+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+}
M agent_edit_integration_test.go
+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 
M agent_integration_test.go
+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 	}