add flake.nix and Makefile target to update vendorHash
split bug to separate repo; adds issue comments feat: add ID field to CommentData struct feat: populate comment ID from CombinedId feat: display comment ID in issue detail template feat: add CSS styling for comment ID display feat: add ID field to CommentData struct feat: populate comment ID from CombinedId feat: display comment ID in issue detail template feat: add CSS styling for comment ID display split bug to separate repo; adds issue comments feat: add initial flake.nix for nix builds fix: update vendorHash in flake.nix chore: add flake.lock for reproducible builds docs: add nix build instructions to README chore: add direnv support with .envrc feat: add initial flake.nix for nix builds fix: update vendorHash in flake.nix chore: add flake.lock for reproducible builds docs: add nix build instructions to README chore: add direnv support with .envrc feat: add Makefile targets for updating flake vendorHash and lock file
27 files changed,  +221, -6421
A .envrc
+1, -0
1@@ -0,0 +1 @@
2+use flake
M .gitignore
+4, -0
 1@@ -6,6 +6,10 @@ public/
 2 testdata.site/
 3 testdata.repo/
 4 docs/plans/
 5+.direnv/
 6+update-vendor-hash
 7+update-flake-lock
 8+result
 9 # Only ignore 'bug' at root level, not in cmd/bug/
10 /bug
11 bug-test
M Makefile
+46, -12
 1@@ -1,21 +1,23 @@
 2 REV=$(shell git rev-parse --short HEAD)
 3 PROJECT="git-pgit-$(REV)"
 4 
 5-smol:
 6-	curl https://pico.sh/smol.css -o ./static/smol.css
 7-.PHONY: smol
 8 
 9-clean:
10-	rm -rf ./public
11-.PHONY: clean
12-
13-build:
14-	go build -o pgit ./cmd/pgit
15+build: clean
16+	@if command -v nix >/dev/null 2>&1; then \
17+		echo "Building with nix..."; \
18+		nix build; \
19+	else \
20+		echo "Building with go..."; \
21+		mkdir -p result/bin; \
22+		go build -o result/bin/pgit ./cmd/pgit; \
23+	fi
24 .PHONY: build
25 
26-img:
27-	docker build -t neurosnap/pgit:latest .
28-.PHONY: img
29+clean:
30+	@# Remove result symlink (not the target in /nix/store)
31+	@rm -f result
32+	@rm -rf bin/
33+.PHONY: clean
34 
35 fmt:
36 	go fmt ./...
37@@ -29,6 +31,38 @@ test:
38 	go test ./...
39 .PHONY: test
40 
41+# Update flake.nix vendorHash when go.mod/go.sum changes
42+# This target depends on go.mod and go.sum, so it will only run when they change
43+update-vendor-hash: flake.nix go.mod go.sum
44+	@echo "Checking if vendorHash needs update..."
45+	@nix build 2>&1 | tee /tmp/nix-build.log; \
46+	if grep -q "hash mismatch" /tmp/nix-build.log; then \
47+		NEW_HASH=$$(grep "got:" /tmp/nix-build.log | sed 's/.*got: *//' | tail -1); \
48+		echo "Updating vendorHash to $$NEW_HASH"; \
49+		sed -i "s|vendorHash = \"sha256-[^\"]*\";|vendorHash = \"$$NEW_HASH\";|" flake.nix; \
50+		sed -i "s|vendorHash = pkgs.lib.fakeHash;|vendorHash = \"$$NEW_HASH\";|" flake.nix; \
51+	elif grep -q "error:.*fakeHash" /tmp/nix-build.log; then \
52+		NEW_HASH=$$(grep "got:" /tmp/nix-build.log | sed 's/.*got: *//' | tail -1); \
53+		echo "Updating vendorHash to $$NEW_HASH"; \
54+		sed -i "s|vendorHash = pkgs.lib.fakeHash;|vendorHash = \"$$NEW_HASH\";|" flake.nix; \
55+	else \
56+		echo "vendorHash is up to date"; \
57+	fi
58+	@rm -f /tmp/nix-build.log
59+	@touch update-vendor-hash
60+.PHONY: update-vendor-hash
61+
62+# Update flake.lock when flake.nix inputs change
63+update-flake-lock: flake.nix
64+	nix flake lock
65+	touch update-flake-lock
66+.PHONY: update-flake-lock
67+
68+# Update both vendor hash and flake lock
69+update-flake: update-vendor-hash update-flake-lock
70+	@echo "Flake updated successfully"
71+.PHONY: update-flake
72+
73 test-site:
74 	mkdir -p testdata.site
75 	rm -rf testdata.site/*
M README.md
+37, -54
  1@@ -9,6 +9,8 @@ It will only generate a commit log and files for the provided revisions.
  2 
  3 ## usage
  4 
  5+### building with make
  6+
  7 ```bash
  8 make build
  9 ```
 10@@ -17,6 +19,36 @@ make build
 11 ./pgit --revs main --label pico --out ./public
 12 ```
 13 
 14+### building with nix flakes
 15+
 16+Build the project:
 17+
 18+```bash
 19+nix build
 20+```
 21+
 22+This creates a `result` symlink containing the `pgit` binary at `result/bin/pgit`.
 23+
 24+Run the binary:
 25+
 26+```bash
 27+./result/bin/pgit --revs main --label pico --out ./public
 28+```
 29+
 30+### development shell
 31+
 32+Enter a development environment with all dependencies:
 33+
 34+```bash
 35+nix develop
 36+```
 37+
 38+Or run a single command:
 39+
 40+```bash
 41+nix develop --command make build
 42+```
 43+
 44 To learn more about the options run:
 45 
 46 ```bash
 47@@ -121,61 +153,12 @@ Remove the hook:
 48 
 49 This will restore any previous post-commit hook if one existed.
 50 
 51-## editing issues and comments
 52-
 53-The `bug edit` command allows you to modify existing issues and comments.
 54 
 55-### Edit an issue
 56+## Special Thanks
 57 
 58-Update the title, description, or both:
 59-
 60-```bash
 61-# Open editor with current content
 62-bug edit abc1234
 63-
 64-# Update only the title
 65-bug edit abc1234 --title "New Title"
 66-bug edit abc1234 -t "New Title"
 67-
 68-# Update only the description
 69-bug edit abc1234 --message "New description"
 70-bug edit abc1234 -m "New description"
 71-
 72-# Update both title and description
 73-bug edit abc1234 -t "New Title" -m "New description"
 74-```
 75-
 76-### Edit a comment
 77-
 78-```bash
 79-# Open editor with current comment text
 80-bug edit def5678
 81-
 82-# Update comment directly
 83-bug edit def5678 -m "Updated comment text"
 84-```
 85-
 86-When using the editor:
 87-- For issues: edit the title on the first line, leave a blank line, then edit the description
 88-- For comments: edit the comment text directly
 89-- Lines starting with `;;` are ignored (instructions)
 90-
 91-### Agent edit command
 92-
 93-For automated/agent use, the `bug agent edit` command is non-interactive and
 94-requires at least one of `--title` or `--message`:
 95-
 96-```bash
 97-# Edit an issue as agent
 98-bug agent edit abc1234 --title "New Title"
 99-bug agent edit abc1234 --message "New description"
100-bug agent edit abc1234 --title "Title" --message "Description"
101-
102-# Edit a comment as agent (requires --message)
103-bug agent edit def5678 --message "Updated comment"
104-```
105+This project was heavily inspired by
106+[stagit](https://codemadness.org/stagit.html) and is a fork of [pgit](https://pgit.pico.sh).
107 
108-## inspiration
109+## License
110 
111-This project was heavily inspired by
112-[stagit](https://codemadness.org/stagit.html)
113+The pgit project was originally licensed under MIT.
D cmd/bug/agent_comment_integration_test.go
+0, -493
  1@@ -1,493 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os/exec"
  8-	"strings"
  9-	"testing"
 10-
 11-	"github.com/git-bug/git-bug/entities/bug"
 12-	"github.com/git-bug/git-bug/entity"
 13-	"github.com/git-bug/git-bug/repository"
 14-)
 15-
 16-// TestAgentCommentCommand_WithFlag adds a comment as agent using the message flag
 17-func TestAgentCommentCommand_WithFlag(t *testing.T) {
 18-	tmpDir := t.TempDir()
 19-
 20-	// Initialize a git repo
 21-	initCmd := exec.Command("git", "init")
 22-	initCmd.Dir = tmpDir
 23-	if err := initCmd.Run(); err != nil {
 24-		t.Fatalf("failed to init git repo: %v", err)
 25-	}
 26-
 27-	// Initialize git-bug with both user and agent identities
 28-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 29-		t.Fatalf("failed to create user identity: %v", err)
 30-	}
 31-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
 32-		t.Fatalf("failed to create agent identity: %v", err)
 33-	}
 34-
 35-	// Create a bug first as user
 36-	title := "Test Bug for Agent Comment"
 37-	message := "Initial bug description"
 38-	if err := runNew(tmpDir, title, message); err != nil {
 39-		t.Fatalf("runNew failed: %v", err)
 40-	}
 41-
 42-	// Get the bug ID
 43-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 44-	if err != nil {
 45-		t.Fatalf("failed to open repo: %v", err)
 46-	}
 47-
 48-	var bugID string
 49-	for streamedBug := range bug.ReadAll(repo) {
 50-		if streamedBug.Err != nil {
 51-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
 52-		}
 53-		b := streamedBug.Entity
 54-		snap := b.Compile()
 55-		if snap.Title == title {
 56-			bugID = b.Id().String()
 57-			break
 58-		}
 59-	}
 60-	repo.Close()
 61-
 62-	if bugID == "" {
 63-		t.Fatal("could not find created bug")
 64-	}
 65-
 66-	// Add a comment as agent using the flag
 67-	commentText := "This is an automated agent comment"
 68-	if err := runAgentComment(tmpDir, bugID, commentText); err != nil {
 69-		t.Fatalf("runAgentComment failed: %v", err)
 70-	}
 71-
 72-	// Verify comment was added with agent as author
 73-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
 74-	if err != nil {
 75-		t.Fatalf("failed to open repo: %v", err)
 76-	}
 77-	defer repo.Close()
 78-
 79-	b, err := bug.Read(repo, entity.Id(bugID))
 80-	if err != nil {
 81-		t.Fatalf("failed to read bug: %v", err)
 82-	}
 83-
 84-	snap := b.Compile()
 85-	if len(snap.Comments) != 2 {
 86-		t.Errorf("expected 2 comments (original + new), got %d", len(snap.Comments))
 87-	}
 88-
 89-	// Check the second comment (index 1) is our agent comment
 90-	if snap.Comments[1].Message != commentText {
 91-		t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
 92-	}
 93-
 94-	if snap.Comments[1].Author.Name() != "agent" {
 95-		t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "agent")
 96-	}
 97-
 98-	if snap.Comments[1].Author.Email() != "" {
 99-		t.Errorf("comment author email = %q, want empty", snap.Comments[1].Author.Email())
100-	}
101-}
102-
103-// TestAgentCommentCommand_WithShortID adds a comment as agent using short bug ID
104-func TestAgentCommentCommand_WithShortID(t *testing.T) {
105-	tmpDir := t.TempDir()
106-
107-	// Initialize a git repo
108-	initCmd := exec.Command("git", "init")
109-	initCmd.Dir = tmpDir
110-	if err := initCmd.Run(); err != nil {
111-		t.Fatalf("failed to init git repo: %v", err)
112-	}
113-
114-	// Initialize identities
115-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
116-		t.Fatalf("failed to create user identity: %v", err)
117-	}
118-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
119-		t.Fatalf("failed to create agent identity: %v", err)
120-	}
121-
122-	// Create a bug
123-	title := "Test Bug for Agent Short ID"
124-	if err := runNew(tmpDir, title, ""); err != nil {
125-		t.Fatalf("runNew failed: %v", err)
126-	}
127-
128-	// Get the bug ID and short ID
129-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
130-	if err != nil {
131-		t.Fatalf("failed to open repo: %v", err)
132-	}
133-
134-	var bugID, shortID string
135-	for streamedBug := range bug.ReadAll(repo) {
136-		if streamedBug.Err != nil {
137-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
138-		}
139-		b := streamedBug.Entity
140-		snap := b.Compile()
141-		if snap.Title == title {
142-			bugID = b.Id().String()
143-			if len(bugID) > 7 {
144-				shortID = bugID[:7]
145-			} else {
146-				shortID = bugID
147-			}
148-			break
149-		}
150-	}
151-	repo.Close()
152-
153-	if shortID == "" {
154-		t.Fatal("could not find created bug")
155-	}
156-
157-	// Add a comment as agent using short ID
158-	commentText := "Agent comment via short ID"
159-	if err := runAgentComment(tmpDir, shortID, commentText); err != nil {
160-		t.Fatalf("runAgentComment with short ID failed: %v", err)
161-	}
162-
163-	// Verify comment was added
164-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
165-	if err != nil {
166-		t.Fatalf("failed to open repo: %v", err)
167-	}
168-	defer repo.Close()
169-
170-	b, err := bug.Read(repo, entity.Id(bugID))
171-	if err != nil {
172-		t.Fatalf("failed to read bug: %v", err)
173-	}
174-
175-	snap := b.Compile()
176-	if len(snap.Comments) != 2 {
177-		t.Errorf("expected 2 comments, got %d", len(snap.Comments))
178-	}
179-
180-	if snap.Comments[1].Message != commentText {
181-		t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
182-	}
183-
184-	if snap.Comments[1].Author.Name() != "agent" {
185-		t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "agent")
186-	}
187-}
188-
189-// TestAgentCommentCommand_MissingAgentIdentity tests error when agent not initialized
190-func TestAgentCommentCommand_MissingAgentIdentity(t *testing.T) {
191-	tmpDir := t.TempDir()
192-
193-	// Initialize a git repo
194-	initCmd := exec.Command("git", "init")
195-	initCmd.Dir = tmpDir
196-	if err := initCmd.Run(); err != nil {
197-		t.Fatalf("failed to init git repo: %v", err)
198-	}
199-
200-	// Only create user identity, NOT agent
201-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
202-		t.Fatalf("failed to create user identity: %v", err)
203-	}
204-
205-	// Create a bug as user
206-	title := "Test Bug"
207-	if err := runNew(tmpDir, title, ""); err != nil {
208-		t.Fatalf("runNew failed: %v", err)
209-	}
210-
211-	// Get the bug ID
212-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
213-	if err != nil {
214-		t.Fatalf("failed to open repo: %v", err)
215-	}
216-
217-	var bugID string
218-	for streamedBug := range bug.ReadAll(repo) {
219-		if streamedBug.Err != nil {
220-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
221-		}
222-		b := streamedBug.Entity
223-		snap := b.Compile()
224-		if snap.Title == title {
225-			bugID = b.Id().String()
226-			break
227-		}
228-	}
229-	repo.Close()
230-
231-	// Try to add a comment as agent (should fail)
232-	err = runAgentComment(tmpDir, bugID, "Test comment")
233-	if err == nil {
234-		t.Error("expected error when agent identity not found, got nil")
235-	}
236-
237-	if !strings.Contains(err.Error(), "agent identity not found") {
238-		t.Errorf("expected 'agent identity not found' error, got: %v", err)
239-	}
240-}
241-
242-// TestAgentCommentCommand_EmptyMessageError tests error for empty message
243-func TestAgentCommentCommand_EmptyMessageError(t *testing.T) {
244-	tmpDir := t.TempDir()
245-
246-	// Initialize a git repo
247-	initCmd := exec.Command("git", "init")
248-	initCmd.Dir = tmpDir
249-	if err := initCmd.Run(); err != nil {
250-		t.Fatalf("failed to init git repo: %v", err)
251-	}
252-
253-	// Initialize identities
254-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
255-		t.Fatalf("failed to create user identity: %v", err)
256-	}
257-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
258-		t.Fatalf("failed to create agent identity: %v", err)
259-	}
260-
261-	// Create a bug
262-	title := "Test Bug"
263-	if err := runNew(tmpDir, title, ""); err != nil {
264-		t.Fatalf("runNew failed: %v", err)
265-	}
266-
267-	// Get the bug ID
268-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
269-	if err != nil {
270-		t.Fatalf("failed to open repo: %v", err)
271-	}
272-
273-	var bugID string
274-	for streamedBug := range bug.ReadAll(repo) {
275-		if streamedBug.Err != nil {
276-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
277-		}
278-		b := streamedBug.Entity
279-		snap := b.Compile()
280-		if snap.Title == title {
281-			bugID = b.Id().String()
282-			break
283-		}
284-	}
285-	repo.Close()
286-
287-	// Try with empty message
288-	err = runAgentComment(tmpDir, bugID, "   ")
289-	if err == nil {
290-		t.Error("expected error for empty message, got nil")
291-	}
292-
293-	if !strings.Contains(err.Error(), "message cannot be empty") {
294-		t.Errorf("expected 'message cannot be empty' error, got: %v", err)
295-	}
296-}
297-
298-// TestAgentCommentCommand_InvalidBugID tests error for invalid bug ID
299-func TestAgentCommentCommand_InvalidBugID(t *testing.T) {
300-	tmpDir := t.TempDir()
301-
302-	// Initialize a git repo
303-	initCmd := exec.Command("git", "init")
304-	initCmd.Dir = tmpDir
305-	if err := initCmd.Run(); err != nil {
306-		t.Fatalf("failed to init git repo: %v", err)
307-	}
308-
309-	// Initialize identities
310-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
311-		t.Fatalf("failed to create user identity: %v", err)
312-	}
313-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
314-		t.Fatalf("failed to create agent identity: %v", err)
315-	}
316-
317-	// Try to add a comment to non-existent bug
318-	err := runAgentComment(tmpDir, "nonexistent", "Test comment")
319-	if err == nil {
320-		t.Error("expected error for invalid bug ID, got nil")
321-	}
322-
323-	if !strings.Contains(err.Error(), "failed to resolve bug ID") {
324-		t.Errorf("expected 'failed to resolve bug ID' error, got: %v", err)
325-	}
326-}
327-
328-// TestAgentCommentCommand_MultipleComments tests adding multiple agent comments
329-func TestAgentCommentCommand_MultipleComments(t *testing.T) {
330-	tmpDir := t.TempDir()
331-
332-	// Initialize a git repo
333-	initCmd := exec.Command("git", "init")
334-	initCmd.Dir = tmpDir
335-	if err := initCmd.Run(); err != nil {
336-		t.Fatalf("failed to init git repo: %v", err)
337-	}
338-
339-	// Initialize identities
340-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
341-		t.Fatalf("failed to create user identity: %v", err)
342-	}
343-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
344-		t.Fatalf("failed to create agent identity: %v", err)
345-	}
346-
347-	// Create a bug as user
348-	title := "Test Bug for Multiple Agent Comments"
349-	if err := runNew(tmpDir, title, ""); err != nil {
350-		t.Fatalf("runNew failed: %v", err)
351-	}
352-
353-	// Get the bug ID
354-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
355-	if err != nil {
356-		t.Fatalf("failed to open repo: %v", err)
357-	}
358-
359-	var bugID string
360-	for streamedBug := range bug.ReadAll(repo) {
361-		if streamedBug.Err != nil {
362-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
363-		}
364-		b := streamedBug.Entity
365-		snap := b.Compile()
366-		if snap.Title == title {
367-			bugID = b.Id().String()
368-			break
369-		}
370-	}
371-	repo.Close()
372-
373-	// Add multiple comments as agent
374-	comments := []string{
375-		"First agent comment",
376-		"Second agent comment",
377-		"Third agent comment with **markdown**",
378-	}
379-
380-	for _, comment := range comments {
381-		if err := runAgentComment(tmpDir, bugID, comment); err != nil {
382-			t.Fatalf("runAgentComment failed: %v", err)
383-		}
384-	}
385-
386-	// Verify all comments were added
387-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
388-	if err != nil {
389-		t.Fatalf("failed to open repo: %v", err)
390-	}
391-	defer repo.Close()
392-
393-	b, err := bug.Read(repo, entity.Id(bugID))
394-	if err != nil {
395-		t.Fatalf("failed to read bug: %v", err)
396-	}
397-
398-	snap := b.Compile()
399-	// Original + 3 comments = 4 total
400-	if len(snap.Comments) != 4 {
401-		t.Errorf("expected 4 comments, got %d", len(snap.Comments))
402-	}
403-
404-	// Verify each comment (index 1-3 are our added comments)
405-	for i, expected := range comments {
406-		if snap.Comments[i+1].Message != expected {
407-			t.Errorf("comment %d message = %q, want %q", i+1, snap.Comments[i+1].Message, expected)
408-		}
409-		if snap.Comments[i+1].Author.Name() != "agent" {
410-			t.Errorf("comment %d author = %q, want %q", i+1, snap.Comments[i+1].Author.Name(), "agent")
411-		}
412-	}
413-}
414-
415-// TestAgentCommentCommand_WithMarkdown tests that markdown is preserved
416-func TestAgentCommentCommand_WithMarkdown(t *testing.T) {
417-	tmpDir := t.TempDir()
418-
419-	// Initialize a git repo
420-	initCmd := exec.Command("git", "init")
421-	initCmd.Dir = tmpDir
422-	if err := initCmd.Run(); err != nil {
423-		t.Fatalf("failed to init git repo: %v", err)
424-	}
425-
426-	// Initialize identities
427-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
428-		t.Fatalf("failed to create user identity: %v", err)
429-	}
430-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
431-		t.Fatalf("failed to create agent identity: %v", err)
432-	}
433-
434-	// Create a bug
435-	title := "Test Bug for Markdown"
436-	if err := runNew(tmpDir, title, ""); err != nil {
437-		t.Fatalf("runNew failed: %v", err)
438-	}
439-
440-	// Get the bug ID
441-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
442-	if err != nil {
443-		t.Fatalf("failed to open repo: %v", err)
444-	}
445-
446-	var bugID string
447-	for streamedBug := range bug.ReadAll(repo) {
448-		if streamedBug.Err != nil {
449-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
450-		}
451-		b := streamedBug.Entity
452-		snap := b.Compile()
453-		if snap.Title == title {
454-			bugID = b.Id().String()
455-			break
456-		}
457-	}
458-	repo.Close()
459-
460-	// Add a comment with markdown
461-	markdownComment := `# Analysis Report
462-
463-## Summary
464-- **Status**: PASSED
465-- **Duration**: 5m 32s
466-
467-## Details
468-All tests passed successfully.`
469-
470-	if err := runAgentComment(tmpDir, bugID, markdownComment); err != nil {
471-		t.Fatalf("runAgentComment failed: %v", err)
472-	}
473-
474-	// Verify comment was added with markdown preserved
475-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
476-	if err != nil {
477-		t.Fatalf("failed to open repo: %v", err)
478-	}
479-	defer repo.Close()
480-
481-	b, err := bug.Read(repo, entity.Id(bugID))
482-	if err != nil {
483-		t.Fatalf("failed to read bug: %v", err)
484-	}
485-
486-	snap := b.Compile()
487-	if len(snap.Comments) != 2 {
488-		t.Errorf("expected 2 comments, got %d", len(snap.Comments))
489-	}
490-
491-	if snap.Comments[1].Message != markdownComment {
492-		t.Errorf("comment message does not match expected markdown:\ngot:\n%s\nwant:\n%s", snap.Comments[1].Message, markdownComment)
493-	}
494-}
D cmd/bug/agent_edit_integration_test.go
+0, -400
  1@@ -1,400 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os/exec"
  8-	"strings"
  9-	"testing"
 10-
 11-	"github.com/git-bug/git-bug/entities/bug"
 12-	"github.com/git-bug/git-bug/entity"
 13-	"github.com/git-bug/git-bug/repository"
 14-)
 15-
 16-// TestAgentEditCommand_EditIssue edits an issue as the agent
 17-func TestAgentEditCommand_EditIssue(t *testing.T) {
 18-	tmpDir := t.TempDir()
 19-
 20-	// Initialize a git repo
 21-	initCmd := exec.Command("git", "init")
 22-	initCmd.Dir = tmpDir
 23-	if err := initCmd.Run(); err != nil {
 24-		t.Fatalf("failed to init git repo: %v", err)
 25-	}
 26-	// Add origin remote for bug init
 27-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
 28-	remoteCmd.Dir = tmpDir
 29-	if err := remoteCmd.Run(); err != nil {
 30-		t.Fatalf("failed to add origin remote: %v", err)
 31-	}
 32-
 33-	// Initialize git-bug identities (creates both user and agent)
 34-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
 35-		t.Fatalf("runInit failed: %v", err)
 36-	}
 37-
 38-	// Create a bug as agent
 39-	if err := runAgentNew(tmpDir, "Original Title", "Original description"); err != nil {
 40-		t.Fatalf("runAgentNew failed: %v", err)
 41-	}
 42-
 43-	// Get the bug ID
 44-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 45-	if err != nil {
 46-		t.Fatalf("failed to open repo: %v", err)
 47-	}
 48-
 49-	var bugID string
 50-	for streamedBug := range bug.ReadAll(repo) {
 51-		if streamedBug.Err != nil {
 52-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
 53-		}
 54-		b := streamedBug.Entity
 55-		snap := b.Compile()
 56-		if snap.Title == "Original Title" {
 57-			bugID = b.Id().String()
 58-			break
 59-		}
 60-	}
 61-	repo.Close()
 62-
 63-	if bugID == "" {
 64-		t.Fatal("could not find created bug")
 65-	}
 66-
 67-	// Edit as agent
 68-	newTitle := "Agent Updated Title"
 69-	newMessage := "Agent updated description"
 70-	if err := runAgentEdit(tmpDir, bugID, newTitle, newMessage); err != nil {
 71-		t.Fatalf("runAgentEdit failed: %v", err)
 72-	}
 73-
 74-	// Verify both were updated
 75-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
 76-	if err != nil {
 77-		t.Fatalf("failed to open repo: %v", err)
 78-	}
 79-	defer repo.Close()
 80-
 81-	b, err := bug.Read(repo, entity.Id(bugID))
 82-	if err != nil {
 83-		t.Fatalf("failed to read bug: %v", err)
 84-	}
 85-
 86-	snap := b.Compile()
 87-	if snap.Title != newTitle {
 88-		t.Errorf("title = %q, want %q", snap.Title, newTitle)
 89-	}
 90-	if len(snap.Comments) == 0 || snap.Comments[0].Message != newMessage {
 91-		t.Errorf("description = %q, want %q", snap.Comments[0].Message, newMessage)
 92-	}
 93-}
 94-
 95-// TestAgentEditCommand_EditComment edits a comment as the agent
 96-func TestAgentEditCommand_EditComment(t *testing.T) {
 97-	tmpDir := t.TempDir()
 98-
 99-	// Initialize a git repo
100-	initCmd := exec.Command("git", "init")
101-	initCmd.Dir = tmpDir
102-	if err := initCmd.Run(); err != nil {
103-		t.Fatalf("failed to init git repo: %v", err)
104-	}
105-	// Add origin remote for bug init
106-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
107-	remoteCmd.Dir = tmpDir
108-	if err := remoteCmd.Run(); err != nil {
109-		t.Fatalf("failed to add origin remote: %v", err)
110-	}
111-
112-	// Initialize git-bug identities
113-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
114-		t.Fatalf("runInit failed: %v", err)
115-	}
116-
117-	// Create a bug as agent
118-	if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
119-		t.Fatalf("runAgentNew failed: %v", err)
120-	}
121-
122-	// Get the bug ID
123-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
124-	if err != nil {
125-		t.Fatalf("failed to open repo: %v", err)
126-	}
127-
128-	var bugID string
129-	for streamedBug := range bug.ReadAll(repo) {
130-		if streamedBug.Err != nil {
131-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
132-		}
133-		b := streamedBug.Entity
134-		snap := b.Compile()
135-		if snap.Title == "Test Bug" {
136-			bugID = b.Id().String()
137-			break
138-		}
139-	}
140-	repo.Close()
141-
142-	// Add a comment as agent
143-	originalComment := "Original agent comment"
144-	if err := runAgentComment(tmpDir, bugID, originalComment); err != nil {
145-		t.Fatalf("runAgentComment failed: %v", err)
146-	}
147-
148-	// Get the comment ID
149-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
150-	if err != nil {
151-		t.Fatalf("failed to open repo: %v", err)
152-	}
153-
154-	b, err := bug.Read(repo, entity.Id(bugID))
155-	if err != nil {
156-		t.Fatalf("failed to read bug: %v", err)
157-	}
158-	snap := b.Compile()
159-	if len(snap.Comments) < 2 {
160-		t.Fatal("expected at least 2 comments")
161-	}
162-	commentID := snap.Comments[1].CombinedId().String()
163-	repo.Close()
164-
165-	// Edit the comment as agent
166-	newComment := "Updated agent comment"
167-	if err := runAgentEdit(tmpDir, commentID, "", newComment); err != nil {
168-		t.Fatalf("runAgentEdit failed: %v", err)
169-	}
170-
171-	// Verify comment was updated
172-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
173-	if err != nil {
174-		t.Fatalf("failed to open repo: %v", err)
175-	}
176-	defer repo.Close()
177-
178-	b, err = bug.Read(repo, entity.Id(bugID))
179-	if err != nil {
180-		t.Fatalf("failed to read bug: %v", err)
181-	}
182-
183-	snap = b.Compile()
184-	if len(snap.Comments) < 2 {
185-		t.Fatal("expected at least 2 comments")
186-	}
187-	if snap.Comments[1].Message != newComment {
188-		t.Errorf("comment = %q, want %q", snap.Comments[1].Message, newComment)
189-	}
190-}
191-
192-// TestAgentEditCommand_RequiresMessage tests that agent edit requires at least one field
193-func TestAgentEditCommand_RequiresMessage(t *testing.T) {
194-	tmpDir := t.TempDir()
195-
196-	// Initialize a git repo
197-	initCmd := exec.Command("git", "init")
198-	initCmd.Dir = tmpDir
199-	if err := initCmd.Run(); err != nil {
200-		t.Fatalf("failed to init git repo: %v", err)
201-	}
202-	// Add origin remote for bug init
203-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
204-	remoteCmd.Dir = tmpDir
205-	if err := remoteCmd.Run(); err != nil {
206-		t.Fatalf("failed to add origin remote: %v", err)
207-	}
208-
209-	// Initialize git-bug identities
210-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
211-		t.Fatalf("runInit failed: %v", err)
212-	}
213-
214-	// Create a bug as agent
215-	if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
216-		t.Fatalf("runAgentNew failed: %v", err)
217-	}
218-
219-	// Get the bug ID
220-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
221-	if err != nil {
222-		t.Fatalf("failed to open repo: %v", err)
223-	}
224-
225-	var bugID string
226-	for streamedBug := range bug.ReadAll(repo) {
227-		if streamedBug.Err != nil {
228-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
229-		}
230-		b := streamedBug.Entity
231-		snap := b.Compile()
232-		if snap.Title == "Test Bug" {
233-			bugID = b.Id().String()
234-			break
235-		}
236-	}
237-	repo.Close()
238-
239-	// Add a comment as agent
240-	if err := runAgentComment(tmpDir, bugID, "Original comment"); err != nil {
241-		t.Fatalf("runAgentComment failed: %v", err)
242-	}
243-
244-	// Get the comment ID
245-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
246-	if err != nil {
247-		t.Fatalf("failed to open repo: %v", err)
248-	}
249-
250-	b, err := bug.Read(repo, entity.Id(bugID))
251-	if err != nil {
252-		t.Fatalf("failed to read bug: %v", err)
253-	}
254-	snap := b.Compile()
255-	commentID := snap.Comments[1].CombinedId().String()
256-	repo.Close()
257-
258-	// Try to edit comment without message
259-	err = runAgentEdit(tmpDir, commentID, "", "")
260-	if err == nil {
261-		t.Error("expected error when editing comment without message, got nil")
262-	}
263-
264-	if !strings.Contains(err.Error(), "either --title or --message") {
265-		t.Errorf("expected 'either --title or --message' error, got: %v", err)
266-	}
267-}
268-
269-// TestAgentEditCommand_RequiresAtLeastOneField tests that agent edit requires at least one field
270-func TestAgentEditCommand_RequiresAtLeastOneField(t *testing.T) {
271-	tmpDir := t.TempDir()
272-
273-	// Initialize a git repo
274-	initCmd := exec.Command("git", "init")
275-	initCmd.Dir = tmpDir
276-	if err := initCmd.Run(); err != nil {
277-		t.Fatalf("failed to init git repo: %v", err)
278-	}
279-	// Add origin remote for bug init
280-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
281-	remoteCmd.Dir = tmpDir
282-	if err := remoteCmd.Run(); err != nil {
283-		t.Fatalf("failed to add origin remote: %v", err)
284-	}
285-
286-	// Initialize git-bug identities
287-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
288-		t.Fatalf("runInit failed: %v", err)
289-	}
290-
291-	// Create a bug as agent
292-	if err := runAgentNew(tmpDir, "Test Bug", "Description"); err != nil {
293-		t.Fatalf("runAgentNew failed: %v", err)
294-	}
295-
296-	// Get the bug ID
297-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
298-	if err != nil {
299-		t.Fatalf("failed to open repo: %v", err)
300-	}
301-
302-	var bugID string
303-	for streamedBug := range bug.ReadAll(repo) {
304-		if streamedBug.Err != nil {
305-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
306-		}
307-		b := streamedBug.Entity
308-		snap := b.Compile()
309-		if snap.Title == "Test Bug" {
310-			bugID = b.Id().String()
311-			break
312-		}
313-	}
314-	repo.Close()
315-
316-	// Try to edit without any flags
317-	err = runAgentEdit(tmpDir, bugID, "", "")
318-	if err == nil {
319-		t.Error("expected error when editing without any fields, got nil")
320-	}
321-
322-	if !strings.Contains(err.Error(), "either --title or --message") {
323-		t.Errorf("expected 'either --title or --message' error, got: %v", err)
324-	}
325-}
326-
327-// TestAgentEditCommand_OnlyTitle edits only the title as agent
328-func TestAgentEditCommand_OnlyTitle(t *testing.T) {
329-	tmpDir := t.TempDir()
330-
331-	// Initialize a git repo
332-	initCmd := exec.Command("git", "init")
333-	initCmd.Dir = tmpDir
334-	if err := initCmd.Run(); err != nil {
335-		t.Fatalf("failed to init git repo: %v", err)
336-	}
337-	// Add origin remote for bug init
338-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
339-	remoteCmd.Dir = tmpDir
340-	if err := remoteCmd.Run(); err != nil {
341-		t.Fatalf("failed to add origin remote: %v", err)
342-	}
343-
344-	// Initialize git-bug identities
345-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
346-		t.Fatalf("runInit failed: %v", err)
347-	}
348-
349-	// Create a bug as agent
350-	originalTitle := "Original Title"
351-	originalDesc := "Original description"
352-	if err := runAgentNew(tmpDir, originalTitle, originalDesc); err != nil {
353-		t.Fatalf("runAgentNew failed: %v", err)
354-	}
355-
356-	// Get the bug ID
357-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
358-	if err != nil {
359-		t.Fatalf("failed to open repo: %v", err)
360-	}
361-
362-	var bugID string
363-	for streamedBug := range bug.ReadAll(repo) {
364-		if streamedBug.Err != nil {
365-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
366-		}
367-		b := streamedBug.Entity
368-		snap := b.Compile()
369-		if snap.Title == originalTitle {
370-			bugID = b.Id().String()
371-			break
372-		}
373-	}
374-	repo.Close()
375-
376-	// Edit only title as agent
377-	newTitle := "New Title"
378-	if err := runAgentEdit(tmpDir, bugID, newTitle, ""); err != nil {
379-		t.Fatalf("runAgentEdit failed: %v", err)
380-	}
381-
382-	// Verify only title changed
383-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
384-	if err != nil {
385-		t.Fatalf("failed to open repo: %v", err)
386-	}
387-	defer repo.Close()
388-
389-	b, err := bug.Read(repo, entity.Id(bugID))
390-	if err != nil {
391-		t.Fatalf("failed to read bug: %v", err)
392-	}
393-
394-	snap := b.Compile()
395-	if snap.Title != newTitle {
396-		t.Errorf("title = %q, want %q", snap.Title, newTitle)
397-	}
398-	if len(snap.Comments) == 0 || snap.Comments[0].Message != originalDesc {
399-		t.Errorf("description was changed unexpectedly")
400-	}
401-}
D cmd/bug/agent_integration_test.go
+0, -233
  1@@ -1,233 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os/exec"
  8-	"strings"
  9-	"testing"
 10-
 11-	"github.com/git-bug/git-bug/entities/bug"
 12-	"github.com/git-bug/git-bug/repository"
 13-)
 14-
 15-// TestAgentNewCommand_WithFlags creates a bug as agent using flags
 16-func TestAgentNewCommand_WithFlags(t *testing.T) {
 17-	tmpDir := t.TempDir()
 18-
 19-	// Initialize a git repo
 20-	initCmd := exec.Command("git", "init")
 21-	initCmd.Dir = tmpDir
 22-	if err := initCmd.Run(); err != nil {
 23-		t.Fatalf("failed to init git repo: %v", err)
 24-	}
 25-
 26-	// Initialize git-bug with both user and agent identities
 27-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 28-		t.Fatalf("failed to create user identity: %v", err)
 29-	}
 30-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
 31-		t.Fatalf("failed to create agent identity: %v", err)
 32-	}
 33-
 34-	// Create a bug as agent using flags
 35-	title := "Agent Created Bug"
 36-	message := "This bug was created by an automated agent"
 37-	if err := runAgentNew(tmpDir, title, message); err != nil {
 38-		t.Fatalf("runAgentNew failed: %v", err)
 39-	}
 40-
 41-	// Verify bug was created with agent as author
 42-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 43-	if err != nil {
 44-		t.Fatalf("failed to open repo: %v", err)
 45-	}
 46-	defer repo.Close()
 47-
 48-	// Count bugs
 49-	bugCount := 0
 50-	for streamedBug := range bug.ReadAll(repo) {
 51-		if streamedBug.Err != nil {
 52-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
 53-		}
 54-		b := streamedBug.Entity
 55-		snap := b.Compile()
 56-
 57-		if snap.Title == title {
 58-			bugCount++
 59-			if snap.Comments[0].Message != message {
 60-				t.Errorf("message = %q, want %q", snap.Comments[0].Message, message)
 61-			}
 62-			if snap.Author.Name() != "agent" {
 63-				t.Errorf("author = %q, want %q", snap.Author.Name(), "agent")
 64-			}
 65-			if snap.Author.Email() != "" {
 66-				t.Errorf("author email = %q, want empty", snap.Author.Email())
 67-			}
 68-		}
 69-	}
 70-
 71-	if bugCount != 1 {
 72-		t.Errorf("expected 1 bug with title %q, found %d", title, bugCount)
 73-	}
 74-}
 75-
 76-// TestAgentNewCommand_MissingAgentIdentity tests error when agent not initialized
 77-func TestAgentNewCommand_MissingAgentIdentity(t *testing.T) {
 78-	tmpDir := t.TempDir()
 79-
 80-	// Initialize a git repo
 81-	initCmd := exec.Command("git", "init")
 82-	initCmd.Dir = tmpDir
 83-	if err := initCmd.Run(); err != nil {
 84-		t.Fatalf("failed to init git repo: %v", err)
 85-	}
 86-
 87-	// Only create user identity, NOT agent
 88-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 89-		t.Fatalf("failed to create user identity: %v", err)
 90-	}
 91-
 92-	// Try to create a bug as agent (should fail)
 93-	err := runAgentNew(tmpDir, "Title", "Message")
 94-	if err == nil {
 95-		t.Error("expected error when agent identity not found, got nil")
 96-	}
 97-
 98-	if !strings.Contains(err.Error(), "agent identity not found") {
 99-		t.Errorf("expected 'agent identity not found' error, got: %v", err)
100-	}
101-}
102-
103-// TestAgentNewCommand_EmptyTitleError tests error for empty title
104-func TestAgentNewCommand_EmptyTitleError(t *testing.T) {
105-	tmpDir := t.TempDir()
106-
107-	// Initialize a git repo
108-	initCmd := exec.Command("git", "init")
109-	initCmd.Dir = tmpDir
110-	if err := initCmd.Run(); err != nil {
111-		t.Fatalf("failed to init git repo: %v", err)
112-	}
113-
114-	// Initialize identities
115-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
116-		t.Fatalf("failed to create user identity: %v", err)
117-	}
118-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
119-		t.Fatalf("failed to create agent identity: %v", err)
120-	}
121-
122-	// Try with empty title
123-	err := runAgentNew(tmpDir, "   ", "Message")
124-	if err == nil {
125-		t.Error("expected error for empty title, got nil")
126-	}
127-
128-	if !strings.Contains(err.Error(), "title cannot be empty") {
129-		t.Errorf("expected 'title cannot be empty' error, got: %v", err)
130-	}
131-}
132-
133-// TestAgentNewCommand_EmptyMessageError tests error for empty message
134-func TestAgentNewCommand_EmptyMessageError(t *testing.T) {
135-	tmpDir := t.TempDir()
136-
137-	// Initialize a git repo
138-	initCmd := exec.Command("git", "init")
139-	initCmd.Dir = tmpDir
140-	if err := initCmd.Run(); err != nil {
141-		t.Fatalf("failed to init git repo: %v", err)
142-	}
143-
144-	// Initialize identities
145-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
146-		t.Fatalf("failed to create user identity: %v", err)
147-	}
148-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
149-		t.Fatalf("failed to create agent identity: %v", err)
150-	}
151-
152-	// Try with empty message
153-	err := runAgentNew(tmpDir, "Title", "   ")
154-	if err == nil {
155-		t.Error("expected error for empty message, got nil")
156-	}
157-
158-	if !strings.Contains(err.Error(), "message cannot be empty") {
159-		t.Errorf("expected 'message cannot be empty' error, got: %v", err)
160-	}
161-}
162-
163-// TestGetAgentIdentity_Success tests retrieving agent identity
164-func TestGetAgentIdentity_Success(t *testing.T) {
165-	tmpDir := t.TempDir()
166-
167-	// Initialize a git repo
168-	initCmd := exec.Command("git", "init")
169-	initCmd.Dir = tmpDir
170-	if err := initCmd.Run(); err != nil {
171-		t.Fatalf("failed to init git repo: %v", err)
172-	}
173-
174-	// Create user and agent identities
175-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
176-		t.Fatalf("failed to create user identity: %v", err)
177-	}
178-	if err := createIdentity(tmpDir, "agent", "", false); err != nil {
179-		t.Fatalf("failed to create agent identity: %v", err)
180-	}
181-
182-	// Open repo and get agent identity
183-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
184-	if err != nil {
185-		t.Fatalf("failed to open repo: %v", err)
186-	}
187-	defer repo.Close()
188-
189-	agent, err := getAgentIdentity(repo)
190-	if err != nil {
191-		t.Fatalf("getAgentIdentity failed: %v", err)
192-	}
193-
194-	if agent.Name() != "agent" {
195-		t.Errorf("agent name = %q, want %q", agent.Name(), "agent")
196-	}
197-
198-	if agent.Email() != "" {
199-		t.Errorf("agent email = %q, want empty", agent.Email())
200-	}
201-}
202-
203-// TestGetAgentIdentity_NotFound tests error when agent doesn't exist
204-func TestGetAgentIdentity_NotFound(t *testing.T) {
205-	tmpDir := t.TempDir()
206-
207-	// Initialize a git repo
208-	initCmd := exec.Command("git", "init")
209-	initCmd.Dir = tmpDir
210-	if err := initCmd.Run(); err != nil {
211-		t.Fatalf("failed to init git repo: %v", err)
212-	}
213-
214-	// Only create user identity
215-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
216-		t.Fatalf("failed to create user identity: %v", err)
217-	}
218-
219-	// Open repo and try to get agent identity
220-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
221-	if err != nil {
222-		t.Fatalf("failed to open repo: %v", err)
223-	}
224-	defer repo.Close()
225-
226-	_, err = getAgentIdentity(repo)
227-	if err == nil {
228-		t.Error("expected error when agent identity not found, got nil")
229-	}
230-
231-	if !strings.Contains(err.Error(), "agent identity not found") {
232-		t.Errorf("expected 'agent identity not found' error, got: %v", err)
233-	}
234-}
D cmd/bug/comment_integration_test.go
+0, -360
  1@@ -1,360 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os/exec"
  8-	"strings"
  9-	"testing"
 10-
 11-	"github.com/git-bug/git-bug/entities/bug"
 12-	"github.com/git-bug/git-bug/entity"
 13-	"github.com/git-bug/git-bug/repository"
 14-)
 15-
 16-// TestCommentCommand_WithFlag adds a comment using the message flag
 17-func TestCommentCommand_WithFlag(t *testing.T) {
 18-	tmpDir := t.TempDir()
 19-
 20-	// Initialize a git repo
 21-	initCmd := exec.Command("git", "init")
 22-	initCmd.Dir = tmpDir
 23-	if err := initCmd.Run(); err != nil {
 24-		t.Fatalf("failed to init git repo: %v", err)
 25-	}
 26-
 27-	// Initialize git-bug identities
 28-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 29-		t.Fatalf("failed to create identity: %v", err)
 30-	}
 31-
 32-	// Create a bug first
 33-	title := "Test Bug for Comment"
 34-	message := "Initial bug description"
 35-	if err := runNew(tmpDir, title, message); err != nil {
 36-		t.Fatalf("runNew failed: %v", err)
 37-	}
 38-
 39-	// Get the bug ID
 40-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 41-	if err != nil {
 42-		t.Fatalf("failed to open repo: %v", err)
 43-	}
 44-
 45-	var bugID string
 46-	for streamedBug := range bug.ReadAll(repo) {
 47-		if streamedBug.Err != nil {
 48-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
 49-		}
 50-		b := streamedBug.Entity
 51-		snap := b.Compile()
 52-		if snap.Title == title {
 53-			bugID = b.Id().String()
 54-			break
 55-		}
 56-	}
 57-	repo.Close()
 58-
 59-	if bugID == "" {
 60-		t.Fatal("could not find created bug")
 61-	}
 62-
 63-	// Add a comment using the flag
 64-	commentText := "This is a test comment"
 65-	if err := runComment(tmpDir, bugID, commentText); err != nil {
 66-		t.Fatalf("runComment failed: %v", err)
 67-	}
 68-
 69-	// Verify comment was added
 70-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
 71-	if err != nil {
 72-		t.Fatalf("failed to open repo: %v", err)
 73-	}
 74-	defer repo.Close()
 75-
 76-	b, err := bug.Read(repo, entity.Id(bugID))
 77-	if err != nil {
 78-		t.Fatalf("failed to read bug: %v", err)
 79-	}
 80-
 81-	snap := b.Compile()
 82-	if len(snap.Comments) != 2 {
 83-		t.Errorf("expected 2 comments (original + new), got %d", len(snap.Comments))
 84-	}
 85-
 86-	// Check the second comment (index 1) is our new comment
 87-	if snap.Comments[1].Message != commentText {
 88-		t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
 89-	}
 90-
 91-	if snap.Comments[1].Author.Name() != "Test User" {
 92-		t.Errorf("comment author = %q, want %q", snap.Comments[1].Author.Name(), "Test User")
 93-	}
 94-}
 95-
 96-// TestCommentCommand_WithShortID adds a comment using a short bug ID
 97-func TestCommentCommand_WithShortID(t *testing.T) {
 98-	tmpDir := t.TempDir()
 99-
100-	// Initialize a git repo
101-	initCmd := exec.Command("git", "init")
102-	initCmd.Dir = tmpDir
103-	if err := initCmd.Run(); err != nil {
104-		t.Fatalf("failed to init git repo: %v", err)
105-	}
106-
107-	// Initialize git-bug identities
108-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
109-		t.Fatalf("failed to create identity: %v", err)
110-	}
111-
112-	// Create a bug
113-	title := "Test Bug for Short ID"
114-	if err := runNew(tmpDir, title, ""); err != nil {
115-		t.Fatalf("runNew failed: %v", err)
116-	}
117-
118-	// Get the bug ID and short ID
119-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
120-	if err != nil {
121-		t.Fatalf("failed to open repo: %v", err)
122-	}
123-
124-	var bugID, shortID string
125-	for streamedBug := range bug.ReadAll(repo) {
126-		if streamedBug.Err != nil {
127-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
128-		}
129-		b := streamedBug.Entity
130-		snap := b.Compile()
131-		if snap.Title == title {
132-			bugID = b.Id().String()
133-			// Use first 7 chars as short ID
134-			if len(bugID) > 7 {
135-				shortID = bugID[:7]
136-			} else {
137-				shortID = bugID
138-			}
139-			break
140-		}
141-	}
142-	repo.Close()
143-
144-	if shortID == "" {
145-		t.Fatal("could not find created bug")
146-	}
147-
148-	// Add a comment using short ID
149-	commentText := "Comment added via short ID"
150-	if err := runComment(tmpDir, shortID, commentText); err != nil {
151-		t.Fatalf("runComment with short ID failed: %v", err)
152-	}
153-
154-	// Verify comment was added
155-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
156-	if err != nil {
157-		t.Fatalf("failed to open repo: %v", err)
158-	}
159-	defer repo.Close()
160-
161-	b, err := bug.Read(repo, entity.Id(bugID))
162-	if err != nil {
163-		t.Fatalf("failed to read bug: %v", err)
164-	}
165-
166-	snap := b.Compile()
167-	if len(snap.Comments) != 2 {
168-		t.Errorf("expected 2 comments, got %d", len(snap.Comments))
169-	}
170-
171-	if snap.Comments[1].Message != commentText {
172-		t.Errorf("comment message = %q, want %q", snap.Comments[1].Message, commentText)
173-	}
174-}
175-
176-// TestCommentCommand_NoIdentityError tests error when identity not set
177-func TestCommentCommand_NoIdentityError(t *testing.T) {
178-	tmpDir := t.TempDir()
179-
180-	// Initialize a git repo (but no git-bug identities)
181-	initCmd := exec.Command("git", "init")
182-	initCmd.Dir = tmpDir
183-	if err := initCmd.Run(); err != nil {
184-		t.Fatalf("failed to init git repo: %v", err)
185-	}
186-
187-	// Don't create any identities - try to add a comment to non-existent bug
188-	// Bug ID resolution happens before identity check, so we should get ID error first
189-	err := runComment(tmpDir, "nonexistent123", "Test comment")
190-	if err == nil {
191-		t.Error("expected error, got nil")
192-	}
193-
194-	// Since no bugs exist at all, we should get an ID resolution error
195-	if !strings.Contains(err.Error(), "failed to resolve bug ID") {
196-		t.Errorf("expected 'failed to resolve bug ID' error, got: %v", err)
197-	}
198-}
199-
200-// TestCommentCommand_EmptyMessageError tests error for empty message
201-func TestCommentCommand_EmptyMessageError(t *testing.T) {
202-	tmpDir := t.TempDir()
203-
204-	// Initialize a git repo
205-	initCmd := exec.Command("git", "init")
206-	initCmd.Dir = tmpDir
207-	if err := initCmd.Run(); err != nil {
208-		t.Fatalf("failed to init git repo: %v", err)
209-	}
210-
211-	// Initialize git-bug identities
212-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
213-		t.Fatalf("failed to create identity: %v", err)
214-	}
215-
216-	// Create a bug first
217-	title := "Test Bug"
218-	if err := runNew(tmpDir, title, ""); err != nil {
219-		t.Fatalf("runNew failed: %v", err)
220-	}
221-
222-	// Get the bug ID
223-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
224-	if err != nil {
225-		t.Fatalf("failed to open repo: %v", err)
226-	}
227-
228-	var bugID string
229-	for streamedBug := range bug.ReadAll(repo) {
230-		if streamedBug.Err != nil {
231-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
232-		}
233-		b := streamedBug.Entity
234-		snap := b.Compile()
235-		if snap.Title == title {
236-			bugID = b.Id().String()
237-			break
238-		}
239-	}
240-	repo.Close()
241-
242-	// Try to add a comment with empty message (whitespace only)
243-	// This triggers the editor path, which returns "no comment provided"
244-	err = runComment(tmpDir, bugID, "   ")
245-	if err == nil {
246-		t.Error("expected error for empty message, got nil")
247-	}
248-
249-	// When empty message is provided, editor opens and returns "no comment provided"
250-	if !strings.Contains(err.Error(), "no comment provided") {
251-		t.Errorf("expected 'no comment provided' error, got: %v", err)
252-	}
253-}
254-
255-// TestCommentCommand_InvalidBugID tests error for invalid bug ID
256-func TestCommentCommand_InvalidBugID(t *testing.T) {
257-	tmpDir := t.TempDir()
258-
259-	// Initialize a git repo
260-	initCmd := exec.Command("git", "init")
261-	initCmd.Dir = tmpDir
262-	if err := initCmd.Run(); err != nil {
263-		t.Fatalf("failed to init git repo: %v", err)
264-	}
265-
266-	// Initialize git-bug identities
267-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
268-		t.Fatalf("failed to create identity: %v", err)
269-	}
270-
271-	// Try to add a comment to non-existent bug
272-	err := runComment(tmpDir, "nonexistent", "Test comment")
273-	if err == nil {
274-		t.Error("expected error for invalid bug ID, got nil")
275-	}
276-
277-	if !strings.Contains(err.Error(), "failed to resolve bug ID") {
278-		t.Errorf("expected 'failed to resolve bug ID' error, got: %v", err)
279-	}
280-}
281-
282-// TestCommentCommand_MultipleComments tests adding multiple comments
283-func TestCommentCommand_MultipleComments(t *testing.T) {
284-	tmpDir := t.TempDir()
285-
286-	// Initialize a git repo
287-	initCmd := exec.Command("git", "init")
288-	initCmd.Dir = tmpDir
289-	if err := initCmd.Run(); err != nil {
290-		t.Fatalf("failed to init git repo: %v", err)
291-	}
292-
293-	// Initialize git-bug identities
294-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
295-		t.Fatalf("failed to create identity: %v", err)
296-	}
297-
298-	// Create a bug
299-	title := "Test Bug for Multiple Comments"
300-	if err := runNew(tmpDir, title, ""); err != nil {
301-		t.Fatalf("runNew failed: %v", err)
302-	}
303-
304-	// Get the bug ID
305-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
306-	if err != nil {
307-		t.Fatalf("failed to open repo: %v", err)
308-	}
309-
310-	var bugID string
311-	for streamedBug := range bug.ReadAll(repo) {
312-		if streamedBug.Err != nil {
313-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
314-		}
315-		b := streamedBug.Entity
316-		snap := b.Compile()
317-		if snap.Title == title {
318-			bugID = b.Id().String()
319-			break
320-		}
321-	}
322-	repo.Close()
323-
324-	// Add multiple comments
325-	comments := []string{
326-		"First comment",
327-		"Second comment",
328-		"Third comment with **markdown**",
329-	}
330-
331-	for _, comment := range comments {
332-		if err := runComment(tmpDir, bugID, comment); err != nil {
333-			t.Fatalf("runComment failed: %v", err)
334-		}
335-	}
336-
337-	// Verify all comments were added
338-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
339-	if err != nil {
340-		t.Fatalf("failed to open repo: %v", err)
341-	}
342-	defer repo.Close()
343-
344-	b, err := bug.Read(repo, entity.Id(bugID))
345-	if err != nil {
346-		t.Fatalf("failed to read bug: %v", err)
347-	}
348-
349-	snap := b.Compile()
350-	// Original + 3 comments = 4 total
351-	if len(snap.Comments) != 4 {
352-		t.Errorf("expected 4 comments, got %d", len(snap.Comments))
353-	}
354-
355-	// Verify each comment (index 1-3 are our added comments)
356-	for i, expected := range comments {
357-		if snap.Comments[i+1].Message != expected {
358-			t.Errorf("comment %d message = %q, want %q", i+1, snap.Comments[i+1].Message, expected)
359-		}
360-	}
361-}
D cmd/bug/edit_integration_test.go
+0, -424
  1@@ -1,424 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os/exec"
  8-	"strings"
  9-	"testing"
 10-
 11-	"github.com/git-bug/git-bug/entities/bug"
 12-	"github.com/git-bug/git-bug/entity"
 13-	"github.com/git-bug/git-bug/repository"
 14-)
 15-
 16-// TestEditCommand_EditIssueTitle edits only the issue title
 17-func TestEditCommand_EditIssueTitle(t *testing.T) {
 18-	tmpDir := t.TempDir()
 19-
 20-	// Initialize a git repo
 21-	initCmd := exec.Command("git", "init")
 22-	initCmd.Dir = tmpDir
 23-	if err := initCmd.Run(); err != nil {
 24-		t.Fatalf("failed to init git repo: %v", err)
 25-	}
 26-
 27-	// Initialize git-bug identities
 28-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 29-		t.Fatalf("failed to create identity: %v", err)
 30-	}
 31-
 32-	// Create a bug
 33-	title := "Original Title"
 34-	message := "Original description"
 35-	if err := runNew(tmpDir, title, message); err != nil {
 36-		t.Fatalf("runNew failed: %v", err)
 37-	}
 38-
 39-	// Get the bug ID
 40-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 41-	if err != nil {
 42-		t.Fatalf("failed to open repo: %v", err)
 43-	}
 44-
 45-	var bugID string
 46-	for streamedBug := range bug.ReadAll(repo) {
 47-		if streamedBug.Err != nil {
 48-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
 49-		}
 50-		b := streamedBug.Entity
 51-		snap := b.Compile()
 52-		if snap.Title == title {
 53-			bugID = b.Id().String()
 54-			break
 55-		}
 56-	}
 57-	repo.Close()
 58-
 59-	if bugID == "" {
 60-		t.Fatal("could not find created bug")
 61-	}
 62-
 63-	// Edit only the title
 64-	newTitle := "Updated Title"
 65-	if err := runEdit(tmpDir, bugID, newTitle, ""); err != nil {
 66-		t.Fatalf("runEdit failed: %v", err)
 67-	}
 68-
 69-	// Verify title was updated
 70-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
 71-	if err != nil {
 72-		t.Fatalf("failed to open repo: %v", err)
 73-	}
 74-	defer repo.Close()
 75-
 76-	b, err := bug.Read(repo, entity.Id(bugID))
 77-	if err != nil {
 78-		t.Fatalf("failed to read bug: %v", err)
 79-	}
 80-
 81-	snap := b.Compile()
 82-	if snap.Title != newTitle {
 83-		t.Errorf("title = %q, want %q", snap.Title, newTitle)
 84-	}
 85-	// Description should remain unchanged
 86-	if len(snap.Comments) == 0 || snap.Comments[0].Message != message {
 87-		t.Errorf("description was changed unexpectedly")
 88-	}
 89-}
 90-
 91-// TestEditCommand_EditIssueDescription edits only the issue description
 92-func TestEditCommand_EditIssueDescription(t *testing.T) {
 93-	tmpDir := t.TempDir()
 94-
 95-	// Initialize a git repo
 96-	initCmd := exec.Command("git", "init")
 97-	initCmd.Dir = tmpDir
 98-	if err := initCmd.Run(); err != nil {
 99-		t.Fatalf("failed to init git repo: %v", err)
100-	}
101-
102-	// Initialize git-bug identities
103-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
104-		t.Fatalf("failed to create identity: %v", err)
105-	}
106-
107-	// Create a bug
108-	title := "Original Title"
109-	message := "Original description"
110-	if err := runNew(tmpDir, title, message); err != nil {
111-		t.Fatalf("runNew failed: %v", err)
112-	}
113-
114-	// Get the bug ID
115-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
116-	if err != nil {
117-		t.Fatalf("failed to open repo: %v", err)
118-	}
119-
120-	var bugID string
121-	for streamedBug := range bug.ReadAll(repo) {
122-		if streamedBug.Err != nil {
123-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
124-		}
125-		b := streamedBug.Entity
126-		snap := b.Compile()
127-		if snap.Title == title {
128-			bugID = b.Id().String()
129-			break
130-		}
131-	}
132-	repo.Close()
133-
134-	if bugID == "" {
135-		t.Fatal("could not find created bug")
136-	}
137-
138-	// Edit only the description
139-	newMessage := "Updated description"
140-	if err := runEdit(tmpDir, bugID, "", newMessage); err != nil {
141-		t.Fatalf("runEdit failed: %v", err)
142-	}
143-
144-	// Verify description was updated
145-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
146-	if err != nil {
147-		t.Fatalf("failed to open repo: %v", err)
148-	}
149-	defer repo.Close()
150-
151-	b, err := bug.Read(repo, entity.Id(bugID))
152-	if err != nil {
153-		t.Fatalf("failed to read bug: %v", err)
154-	}
155-
156-	snap := b.Compile()
157-	// Title should remain unchanged
158-	if snap.Title != title {
159-		t.Errorf("title was changed unexpectedly")
160-	}
161-	if len(snap.Comments) == 0 || snap.Comments[0].Message != newMessage {
162-		t.Errorf("description = %q, want %q", snap.Comments[0].Message, newMessage)
163-	}
164-}
165-
166-// TestEditCommand_EditIssueBoth edits both title and description
167-func TestEditCommand_EditIssueBoth(t *testing.T) {
168-	tmpDir := t.TempDir()
169-
170-	// Initialize a git repo
171-	initCmd := exec.Command("git", "init")
172-	initCmd.Dir = tmpDir
173-	if err := initCmd.Run(); err != nil {
174-		t.Fatalf("failed to init git repo: %v", err)
175-	}
176-
177-	// Initialize git-bug identities
178-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
179-		t.Fatalf("failed to create identity: %v", err)
180-	}
181-
182-	// Create a bug
183-	if err := runNew(tmpDir, "Original Title", "Original description"); err != nil {
184-		t.Fatalf("runNew failed: %v", err)
185-	}
186-
187-	// Get the bug ID
188-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
189-	if err != nil {
190-		t.Fatalf("failed to open repo: %v", err)
191-	}
192-
193-	var bugID string
194-	for streamedBug := range bug.ReadAll(repo) {
195-		if streamedBug.Err != nil {
196-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
197-		}
198-		b := streamedBug.Entity
199-		snap := b.Compile()
200-		if snap.Title == "Original Title" {
201-			bugID = b.Id().String()
202-			break
203-		}
204-	}
205-	repo.Close()
206-
207-	// Edit both title and description
208-	newTitle := "Updated Title"
209-	newMessage := "Updated description"
210-	if err := runEdit(tmpDir, bugID, newTitle, newMessage); err != nil {
211-		t.Fatalf("runEdit failed: %v", err)
212-	}
213-
214-	// Verify both were updated
215-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
216-	if err != nil {
217-		t.Fatalf("failed to open repo: %v", err)
218-	}
219-	defer repo.Close()
220-
221-	b, err := bug.Read(repo, entity.Id(bugID))
222-	if err != nil {
223-		t.Fatalf("failed to read bug: %v", err)
224-	}
225-
226-	snap := b.Compile()
227-	if snap.Title != newTitle {
228-		t.Errorf("title = %q, want %q", snap.Title, newTitle)
229-	}
230-	if len(snap.Comments) == 0 || snap.Comments[0].Message != newMessage {
231-		t.Errorf("description = %q, want %q", snap.Comments[0].Message, newMessage)
232-	}
233-}
234-
235-// TestEditCommand_EditComment edits a comment
236-func TestEditCommand_EditComment(t *testing.T) {
237-	tmpDir := t.TempDir()
238-
239-	// Initialize a git repo
240-	initCmd := exec.Command("git", "init")
241-	initCmd.Dir = tmpDir
242-	if err := initCmd.Run(); err != nil {
243-		t.Fatalf("failed to init git repo: %v", err)
244-	}
245-
246-	// Initialize git-bug identities
247-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
248-		t.Fatalf("failed to create identity: %v", err)
249-	}
250-
251-	// Create a bug
252-	if err := runNew(tmpDir, "Test Bug", "Description"); err != nil {
253-		t.Fatalf("runNew failed: %v", err)
254-	}
255-
256-	// Get the bug ID
257-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
258-	if err != nil {
259-		t.Fatalf("failed to open repo: %v", err)
260-	}
261-
262-	var bugID string
263-	for streamedBug := range bug.ReadAll(repo) {
264-		if streamedBug.Err != nil {
265-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
266-		}
267-		b := streamedBug.Entity
268-		snap := b.Compile()
269-		if snap.Title == "Test Bug" {
270-			bugID = b.Id().String()
271-			break
272-		}
273-	}
274-	repo.Close()
275-
276-	// Add a comment
277-	originalComment := "Original comment text"
278-	if err := runComment(tmpDir, bugID, originalComment); err != nil {
279-		t.Fatalf("runComment failed: %v", err)
280-	}
281-
282-	// Get the comment ID
283-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
284-	if err != nil {
285-		t.Fatalf("failed to open repo: %v", err)
286-	}
287-
288-	b, err := bug.Read(repo, entity.Id(bugID))
289-	if err != nil {
290-		t.Fatalf("failed to read bug: %v", err)
291-	}
292-	snap := b.Compile()
293-	if len(snap.Comments) < 2 {
294-		t.Fatal("expected at least 2 comments")
295-	}
296-	commentID := snap.Comments[1].CombinedId().String()
297-	repo.Close()
298-
299-	// Edit the comment
300-	newComment := "Updated comment text"
301-	if err := runEdit(tmpDir, commentID, "", newComment); err != nil {
302-		t.Fatalf("runEdit failed: %v", err)
303-	}
304-
305-	// Verify comment was updated
306-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
307-	if err != nil {
308-		t.Fatalf("failed to open repo: %v", err)
309-	}
310-	defer repo.Close()
311-
312-	b, err = bug.Read(repo, entity.Id(bugID))
313-	if err != nil {
314-		t.Fatalf("failed to read bug: %v", err)
315-	}
316-
317-	snap = b.Compile()
318-	if len(snap.Comments) < 2 {
319-		t.Fatal("expected at least 2 comments")
320-	}
321-	if snap.Comments[1].Message != newComment {
322-		t.Errorf("comment = %q, want %q", snap.Comments[1].Message, newComment)
323-	}
324-}
325-
326-// TestEditCommand_InvalidID tests error for invalid ID
327-func TestEditCommand_InvalidID(t *testing.T) {
328-	tmpDir := t.TempDir()
329-
330-	// Initialize a git repo
331-	initCmd := exec.Command("git", "init")
332-	initCmd.Dir = tmpDir
333-	if err := initCmd.Run(); err != nil {
334-		t.Fatalf("failed to init git repo: %v", err)
335-	}
336-
337-	// Initialize git-bug identities
338-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
339-		t.Fatalf("failed to create identity: %v", err)
340-	}
341-
342-	// Try to edit non-existent ID
343-	err := runEdit(tmpDir, "nonexistent123", "Title", "Message")
344-	if err == nil {
345-		t.Error("expected error for invalid ID, got nil")
346-	}
347-
348-	if !strings.Contains(err.Error(), "failed to resolve ID") {
349-		t.Errorf("expected 'failed to resolve ID' error, got: %v", err)
350-	}
351-}
352-
353-// TestEditCommand_WithShortID tests editing using a short ID
354-func TestEditCommand_WithShortID(t *testing.T) {
355-	tmpDir := t.TempDir()
356-
357-	// Initialize a git repo
358-	initCmd := exec.Command("git", "init")
359-	initCmd.Dir = tmpDir
360-	if err := initCmd.Run(); err != nil {
361-		t.Fatalf("failed to init git repo: %v", err)
362-	}
363-
364-	// Initialize git-bug identities
365-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
366-		t.Fatalf("failed to create identity: %v", err)
367-	}
368-
369-	// Create a bug
370-	if err := runNew(tmpDir, "Original Title", "Original description"); err != nil {
371-		t.Fatalf("runNew failed: %v", err)
372-	}
373-
374-	// Get the bug ID and short ID
375-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
376-	if err != nil {
377-		t.Fatalf("failed to open repo: %v", err)
378-	}
379-
380-	var bugID, shortID string
381-	for streamedBug := range bug.ReadAll(repo) {
382-		if streamedBug.Err != nil {
383-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
384-		}
385-		b := streamedBug.Entity
386-		snap := b.Compile()
387-		if snap.Title == "Original Title" {
388-			bugID = b.Id().String()
389-			if len(bugID) > 7 {
390-				shortID = bugID[:7]
391-			} else {
392-				shortID = bugID
393-			}
394-			break
395-		}
396-	}
397-	repo.Close()
398-
399-	if shortID == "" {
400-		t.Fatal("could not find created bug")
401-	}
402-
403-	// Edit using short ID
404-	newTitle := "Updated Title"
405-	if err := runEdit(tmpDir, shortID, newTitle, ""); err != nil {
406-		t.Fatalf("runEdit with short ID failed: %v", err)
407-	}
408-
409-	// Verify title was updated
410-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
411-	if err != nil {
412-		t.Fatalf("failed to open repo: %v", err)
413-	}
414-	defer repo.Close()
415-
416-	b, err := bug.Read(repo, entity.Id(bugID))
417-	if err != nil {
418-		t.Fatalf("failed to read bug: %v", err)
419-	}
420-
421-	snap := b.Compile()
422-	if snap.Title != newTitle {
423-		t.Errorf("title = %q, want %q", snap.Title, newTitle)
424-	}
425-}
D cmd/bug/edit_test.go
+0, -150
  1@@ -1,150 +0,0 @@
  2-package main
  3-
  4-import (
  5-	"testing"
  6-)
  7-
  8-func TestParseEditContent_IssueFormat(t *testing.T) {
  9-	tests := []struct {
 10-		name        string
 11-		content     string
 12-		wantTitle   string
 13-		wantMessage string
 14-		wantErr     bool
 15-	}{
 16-		{
 17-			name:        "simple title and message",
 18-			content:     "Title\n\nMessage body",
 19-			wantTitle:   "Title",
 20-			wantMessage: "\nMessage body",
 21-			wantErr:     false,
 22-		},
 23-		{
 24-			name:        "title with comment lines",
 25-			content:     ";; comment\nTitle\n\nMessage",
 26-			wantTitle:   "Title",
 27-			wantMessage: "Message",
 28-			wantErr:     false,
 29-		},
 30-		{
 31-			name:        "multiline message",
 32-			content:     "Title\n\nLine 1\nLine 2\nLine 3",
 33-			wantTitle:   "Title",
 34-			wantMessage: "\nLine 1\nLine 2\nLine 3",
 35-			wantErr:     false,
 36-		},
 37-		{
 38-			name:        "title only no message",
 39-			content:     "Title",
 40-			wantTitle:   "Title",
 41-			wantMessage: "",
 42-			wantErr:     false,
 43-		},
 44-		{
 45-			name:        "empty content",
 46-			content:     "",
 47-			wantTitle:   "",
 48-			wantMessage: "",
 49-			wantErr:     true,
 50-		},
 51-		{
 52-			name:        "only comment lines",
 53-			content:     ";; comment 1\n;; comment 2",
 54-			wantTitle:   "",
 55-			wantMessage: "",
 56-			wantErr:     true,
 57-		},
 58-		{
 59-			name:        "title with leading/trailing whitespace in lines",
 60-			content:     "  Title  \n\n  Line 1  \n  Line 2  ",
 61-			wantTitle:   "Title",
 62-			wantMessage: "\n  Line 1  \n  Line 2  ",
 63-			wantErr:     false,
 64-		},
 65-	}
 66-
 67-	for _, tt := range tests {
 68-		t.Run(tt.name, func(t *testing.T) {
 69-			gotTitle, gotMessage, err := parseEditContent(tt.content, false)
 70-			if (err != nil) != tt.wantErr {
 71-				t.Errorf("parseEditContent() error = %v, wantErr %v", err, tt.wantErr)
 72-				return
 73-			}
 74-			if gotTitle != tt.wantTitle {
 75-				t.Errorf("parseEditContent() title = %q, want %q", gotTitle, tt.wantTitle)
 76-			}
 77-			if gotMessage != tt.wantMessage {
 78-				t.Errorf("parseEditContent() message = %q, want %q", gotMessage, tt.wantMessage)
 79-			}
 80-		})
 81-	}
 82-}
 83-
 84-func TestParseEditContent_CommentFormat(t *testing.T) {
 85-	tests := []struct {
 86-		name        string
 87-		content     string
 88-		wantTitle   string
 89-		wantMessage string
 90-		wantErr     bool
 91-	}{
 92-		{
 93-			name:        "simple comment",
 94-			content:     "Comment text",
 95-			wantTitle:   "",
 96-			wantMessage: "Comment text",
 97-			wantErr:     false,
 98-		},
 99-		{
100-			name:        "multiline comment",
101-			content:     "Line 1\nLine 2\nLine 3",
102-			wantTitle:   "",
103-			wantMessage: "Line 1\nLine 2\nLine 3",
104-			wantErr:     false,
105-		},
106-		{
107-			name:        "comment with instruction lines",
108-			content:     ";; instruction\nComment text",
109-			wantTitle:   "",
110-			wantMessage: "Comment text",
111-			wantErr:     false,
112-		},
113-		{
114-			name:        "empty content",
115-			content:     "",
116-			wantTitle:   "",
117-			wantMessage: "",
118-			wantErr:     true,
119-		},
120-		{
121-			name:        "only whitespace",
122-			content:     "   \n   ",
123-			wantTitle:   "",
124-			wantMessage: "",
125-			wantErr:     true,
126-		},
127-		{
128-			name:        "only comment lines",
129-			content:     ";; comment 1\n;; comment 2",
130-			wantTitle:   "",
131-			wantMessage: "",
132-			wantErr:     true,
133-		},
134-	}
135-
136-	for _, tt := range tests {
137-		t.Run(tt.name, func(t *testing.T) {
138-			gotTitle, gotMessage, err := parseEditContent(tt.content, true)
139-			if (err != nil) != tt.wantErr {
140-				t.Errorf("parseEditContent() error = %v, wantErr %v", err, tt.wantErr)
141-				return
142-			}
143-			if gotTitle != tt.wantTitle {
144-				t.Errorf("parseEditContent() title = %q, want %q", gotTitle, tt.wantTitle)
145-			}
146-			if gotMessage != tt.wantMessage {
147-				t.Errorf("parseEditContent() message = %q, want %q", gotMessage, tt.wantMessage)
148-			}
149-		})
150-	}
151-}
D cmd/bug/init_integration_test.go
+0, -390
  1@@ -1,390 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os"
  8-	"os/exec"
  9-	"path/filepath"
 10-	"strings"
 11-	"testing"
 12-
 13-	"github.com/git-bug/git-bug/entities/identity"
 14-	"github.com/git-bug/git-bug/repository"
 15-)
 16-
 17-// setupGitRepo creates a git repo with an origin remote for testing
 18-func setupGitRepo(t *testing.T) string {
 19-	tmpDir := t.TempDir()
 20-
 21-	// Initialize a git repo
 22-	initCmd := exec.Command("git", "init")
 23-	initCmd.Dir = tmpDir
 24-	if err := initCmd.Run(); err != nil {
 25-		t.Fatalf("failed to init git repo: %v", err)
 26-	}
 27-	// Add origin remote for bug init
 28-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
 29-	remoteCmd.Dir = tmpDir
 30-	if err := remoteCmd.Run(); err != nil {
 31-		t.Fatalf("failed to add origin remote: %v", err)
 32-	}
 33-
 34-	// Configure git user for the repo (needed for git operations)
 35-	exec.Command("git", "-C", tmpDir, "config", "user.email", "[email protected]").Run()
 36-	exec.Command("git", "-C", tmpDir, "config", "user.name", "Test User").Run()
 37-
 38-	return tmpDir
 39-}
 40-
 41-// setupGitRepoWithInit creates a git repo with origin remote and runs bug init
 42-func setupGitRepoWithInit(t *testing.T) string {
 43-	tmpDir := setupGitRepo(t)
 44-
 45-	// Initialize git-bug
 46-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
 47-		t.Fatalf("runInit failed: %v", err)
 48-	}
 49-
 50-	return tmpDir
 51-}
 52-
 53-// TestInitCommand_WithJJ tests init when jj config is available
 54-func TestInitCommand_WithJJ(t *testing.T) {
 55-	tmpDir := setupGitRepo(t)
 56-
 57-	// Create .jj directory and config file directly
 58-	jjDir := filepath.Join(tmpDir, ".jj")
 59-	if err := os.MkdirAll(jjDir, 0755); err != nil {
 60-		t.Fatalf("failed to create .jj directory: %v", err)
 61-	}
 62-
 63-	// Write jj config file directly
 64-	configContent := `[user]
 65-name = "JJ Test User"
 66-email = "[email protected]"
 67-`
 68-	configPath := filepath.Join(jjDir, "config.toml")
 69-	if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
 70-		t.Fatalf("failed to write jj config: %v", err)
 71-	}
 72-
 73-	// Run init
 74-	err := runInit(tmpDir)
 75-	if err != nil {
 76-		t.Fatalf("runInit failed: %v", err)
 77-	}
 78-
 79-	// Verify identities were created
 80-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 81-	if err != nil {
 82-		t.Fatalf("failed to open repo: %v", err)
 83-	}
 84-	defer repo.Close()
 85-
 86-	ids, err := identity.ListLocalIds(repo)
 87-	if err != nil {
 88-		t.Fatalf("ListLocalIds failed: %v", err)
 89-	}
 90-
 91-	// Should have at least 2 identities (user + agent)
 92-	if len(ids) < 2 {
 93-		t.Errorf("expected at least 2 identities, got %d", len(ids))
 94-	}
 95-
 96-	// Check if user identity is set
 97-	isSet, err := identity.IsUserIdentitySet(repo)
 98-	if err != nil {
 99-		t.Fatalf("IsUserIdentitySet failed: %v", err)
100-	}
101-	if !isSet {
102-		t.Error("expected user identity to be set")
103-	}
104-
105-	// Verify refspecs were configured
106-	config, err := parseGitConfig(tmpDir)
107-	if err != nil {
108-		t.Fatalf("failed to parse git config: %v", err)
109-	}
110-
111-	originSection := `remote "origin"`
112-	if _, exists := config[originSection]; !exists {
113-		t.Fatal("origin remote not found in config")
114-	}
115-
116-	// Check fetch refspecs
117-	hasBugsFetch := false
118-	hasIdentitiesFetch := false
119-	for _, refspec := range config[originSection]["fetch"] {
120-		if refspec == "+refs/bugs/*:refs/remotes/origin/bugs/*" {
121-			hasBugsFetch = true
122-		}
123-		if refspec == "+refs/identities/*:refs/remotes/origin/identities/*" {
124-			hasIdentitiesFetch = true
125-		}
126-	}
127-	if !hasBugsFetch {
128-		t.Error("bugs fetch refspec not configured")
129-	}
130-	if !hasIdentitiesFetch {
131-		t.Error("identities fetch refspec not configured")
132-	}
133-
134-	// Check push refspecs
135-	hasBugsPush := false
136-	hasIdentitiesPush := false
137-	for _, refspec := range config[originSection]["push"] {
138-		if refspec == "+refs/bugs/*:refs/bugs/*" {
139-			hasBugsPush = true
140-		}
141-		if refspec == "+refs/identities/*:refs/identities/*" {
142-			hasIdentitiesPush = true
143-		}
144-	}
145-	if !hasBugsPush {
146-		t.Error("bugs push refspec not configured")
147-	}
148-	if !hasIdentitiesPush {
149-		t.Error("identities push refspec not configured")
150-	}
151-}
152-
153-// TestInitCommand_WithoutJJ tests init without jj config (interactive mode)
154-func TestInitCommand_WithoutJJ(t *testing.T) {
155-	tmpDir := setupGitRepo(t)
156-
157-	// Simulate user input for interactive mode
158-	input := "Interactive User\[email protected]\n"
159-
160-	// Run init with simulated input
161-	err := runInitWithReader(tmpDir, strings.NewReader(input))
162-	if err != nil {
163-		t.Fatalf("runInitWithReader failed: %v", err)
164-	}
165-
166-	// Verify identities were created
167-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
168-	if err != nil {
169-		t.Fatalf("failed to open repo: %v", err)
170-	}
171-	defer repo.Close()
172-
173-	ids, err := identity.ListLocalIds(repo)
174-	if err != nil {
175-		t.Fatalf("ListLocalIds failed: %v", err)
176-	}
177-
178-	// Should have at least 2 identities (user + agent)
179-	if len(ids) < 2 {
180-		t.Errorf("expected at least 2 identities, got %d", len(ids))
181-	}
182-
183-	// Verify the user identity has the correct info
184-	isSet, err := identity.IsUserIdentitySet(repo)
185-	if err != nil {
186-		t.Fatalf("IsUserIdentitySet failed: %v", err)
187-	}
188-	if !isSet {
189-		t.Error("expected user identity to be set")
190-	}
191-
192-	userIdentity, err := identity.GetUserIdentity(repo)
193-	if err != nil {
194-		t.Fatalf("GetUserIdentity failed: %v", err)
195-	}
196-
197-	if userIdentity.Name() != "Interactive User" {
198-		t.Errorf("expected name 'Interactive User', got %q", userIdentity.Name())
199-	}
200-
201-	if userIdentity.Email() != "[email protected]" {
202-		t.Errorf("expected email '[email protected]', got %q", userIdentity.Email())
203-	}
204-}
205-
206-// TestInitCommand_EmptyNameError tests that empty name is rejected
207-func TestInitCommand_EmptyNameError(t *testing.T) {
208-	tmpDir := setupGitRepo(t)
209-
210-	// Simulate empty name input
211-	input := "\n"
212-
213-	err := runInitWithReader(tmpDir, strings.NewReader(input))
214-	if err == nil {
215-		t.Error("expected error for empty name, got nil")
216-	}
217-
218-	if !strings.Contains(err.Error(), "name cannot be empty") {
219-		t.Errorf("expected 'name cannot be empty' error, got: %v", err)
220-	}
221-}
222-
223-// TestInitCommand_NoOriginRemote tests that init fails without origin remote
224-func TestInitCommand_NoOriginRemote(t *testing.T) {
225-	tmpDir := t.TempDir()
226-
227-	// Initialize a git repo WITHOUT origin remote
228-	initCmd := exec.Command("git", "init")
229-	initCmd.Dir = tmpDir
230-	if err := initCmd.Run(); err != nil {
231-		t.Fatalf("failed to init git repo: %v", err)
232-	}
233-
234-	// Configure git user
235-	exec.Command("git", "-C", tmpDir, "config", "user.email", "[email protected]").Run()
236-	exec.Command("git", "-C", tmpDir, "config", "user.name", "Test User").Run()
237-
238-	// Create jj config to avoid interactive mode
239-	jjDir := filepath.Join(tmpDir, ".jj")
240-	if err := os.MkdirAll(jjDir, 0755); err != nil {
241-		t.Fatalf("failed to create .jj directory: %v", err)
242-	}
243-	configContent := `[user]
244-name = "JJ Test User"
245-email = "[email protected]"
246-`
247-	configPath := filepath.Join(jjDir, "config.toml")
248-	if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
249-		t.Fatalf("failed to write jj config: %v", err)
250-	}
251-
252-	// Run init - should fail with ErrNoOriginRemote
253-	err := runInit(tmpDir)
254-	if err == nil {
255-		t.Fatal("expected error when no origin remote, got nil")
256-	}
257-
258-	// Check that it's the specific ErrNoOriginRemote type
259-	if _, ok := err.(ErrNoOriginRemote); !ok {
260-		t.Errorf("expected ErrNoOriginRemote, got: %T - %v", err, err)
261-	}
262-}
263-
264-// TestInitCommand_IdempotentIdentities tests that running init twice doesn't duplicate identities
265-func TestInitCommand_IdempotentIdentities(t *testing.T) {
266-	tmpDir := setupGitRepo(t)
267-
268-	// Create jj config
269-	jjDir := filepath.Join(tmpDir, ".jj")
270-	if err := os.MkdirAll(jjDir, 0755); err != nil {
271-		t.Fatalf("failed to create .jj directory: %v", err)
272-	}
273-	configContent := `[user]
274-name = "JJ Test User"
275-email = "[email protected]"
276-`
277-	configPath := filepath.Join(jjDir, "config.toml")
278-	if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
279-		t.Fatalf("failed to write jj config: %v", err)
280-	}
281-
282-	// Run init first time
283-	err := runInit(tmpDir)
284-	if err != nil {
285-		t.Fatalf("first runInit failed: %v", err)
286-	}
287-
288-	// Count identities after first run
289-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
290-	if err != nil {
291-		t.Fatalf("failed to open repo: %v", err)
292-	}
293-
294-	ids1, err := identity.ListLocalIds(repo)
295-	if err != nil {
296-		t.Fatalf("ListLocalIds failed: %v", err)
297-	}
298-	repo.Close()
299-
300-	// Run init second time
301-	err = runInit(tmpDir)
302-	if err != nil {
303-		t.Fatalf("second runInit failed: %v", err)
304-	}
305-
306-	// Count identities after second run
307-	repo, err = repository.OpenGoGitRepo(tmpDir, "", nil)
308-	if err != nil {
309-		t.Fatalf("failed to open repo: %v", err)
310-	}
311-	defer repo.Close()
312-
313-	ids2, err := identity.ListLocalIds(repo)
314-	if err != nil {
315-		t.Fatalf("ListLocalIds failed: %v", err)
316-	}
317-
318-	// Should have same number of identities
319-	if len(ids1) != len(ids2) {
320-		t.Errorf("identities count changed after second init: first=%d, second=%d", len(ids1), len(ids2))
321-	}
322-
323-	// Verify identities are the same
324-	idMap := make(map[string]bool)
325-	for _, id := range ids1 {
326-		idMap[id.String()] = true
327-	}
328-	for _, id := range ids2 {
329-		if !idMap[id.String()] {
330-			t.Errorf("new identity found after second init: %s", id.String())
331-		}
332-	}
333-}
334-
335-// TestInitCommand_IdempotentRefspecs tests that running init twice doesn't duplicate refspecs
336-func TestInitCommand_IdempotentRefspecs(t *testing.T) {
337-	tmpDir := setupGitRepo(t)
338-
339-	// Create jj config
340-	jjDir := filepath.Join(tmpDir, ".jj")
341-	if err := os.MkdirAll(jjDir, 0755); err != nil {
342-		t.Fatalf("failed to create .jj directory: %v", err)
343-	}
344-	configContent := `[user]
345-name = "JJ Test User"
346-email = "[email protected]"
347-`
348-	configPath := filepath.Join(jjDir, "config.toml")
349-	if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
350-		t.Fatalf("failed to write jj config: %v", err)
351-	}
352-
353-	// Run init first time
354-	err := runInit(tmpDir)
355-	if err != nil {
356-		t.Fatalf("first runInit failed: %v", err)
357-	}
358-
359-	// Get refspec count after first run
360-	config1, err := parseGitConfig(tmpDir)
361-	if err != nil {
362-		t.Fatalf("failed to parse git config: %v", err)
363-	}
364-
365-	originSection := `remote "origin"`
366-	fetchCount1 := len(config1[originSection]["fetch"])
367-	pushCount1 := len(config1[originSection]["push"])
368-
369-	// Run init second time
370-	err = runInit(tmpDir)
371-	if err != nil {
372-		t.Fatalf("second runInit failed: %v", err)
373-	}
374-
375-	// Get refspec count after second run
376-	config2, err := parseGitConfig(tmpDir)
377-	if err != nil {
378-		t.Fatalf("failed to parse git config: %v", err)
379-	}
380-
381-	fetchCount2 := len(config2[originSection]["fetch"])
382-	pushCount2 := len(config2[originSection]["push"])
383-
384-	// Should have same number of refspecs
385-	if fetchCount1 != fetchCount2 {
386-		t.Errorf("fetch refspecs count changed: first=%d, second=%d", fetchCount1, fetchCount2)
387-	}
388-	if pushCount1 != pushCount2 {
389-		t.Errorf("push refspecs count changed: first=%d, second=%d", pushCount1, pushCount2)
390-	}
391-}
D cmd/bug/main.go
+0, -2544
   1@@ -1,2544 +0,0 @@
   2-package main
   3-
   4-import (
   5-	"bufio"
   6-	"fmt"
   7-	"io"
   8-	"os"
   9-	"os/exec"
  10-	"path/filepath"
  11-	"sort"
  12-	"strings"
  13-	"time"
  14-
  15-	"github.com/charmbracelet/lipgloss"
  16-	"github.com/dustin/go-humanize"
  17-	"github.com/git-bug/git-bug/entities/bug"
  18-	"github.com/git-bug/git-bug/entities/identity"
  19-	"github.com/git-bug/git-bug/entity"
  20-	"github.com/git-bug/git-bug/repository"
  21-	"github.com/spf13/cobra"
  22-)
  23-
  24-// BugIssue represents a git-bug issue for display
  25-type BugIssue struct {
  26-	FullID    string
  27-	ShortID   string
  28-	Title     string
  29-	Labels    []string
  30-	CreatedAt time.Time
  31-	Status    string
  32-}
  33-
  34-// LoadBugs loads all issues from the git-bug repository
  35-func LoadBugs(repoPath string) ([]BugIssue, error) {
  36-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
  37-	if err != nil {
  38-		return nil, fmt.Errorf("failed to open repository: %w", err)
  39-	}
  40-
  41-	// Generate short IDs for all issues
  42-	gen := NewShortIDGenerator(repoPath)
  43-	shortIDMap, err := gen.Generate()
  44-	if err != nil {
  45-		return nil, fmt.Errorf("failed to generate short IDs: %w", err)
  46-	}
  47-
  48-	var issues []BugIssue
  49-
  50-	for streamedBug := range bug.ReadAll(repo) {
  51-		if streamedBug.Err != nil {
  52-			continue // Skip errors, process what we can
  53-		}
  54-
  55-		b := streamedBug.Entity
  56-		snap := b.Compile()
  57-
  58-		fullID := b.Id().String()
  59-		shortID, _ := shortIDMap.GetShortID(fullID)
  60-
  61-		labels := make([]string, len(snap.Labels))
  62-		for i, label := range snap.Labels {
  63-			labels[i] = string(label)
  64-		}
  65-
  66-		issues = append(issues, BugIssue{
  67-			FullID:    fullID,
  68-			ShortID:   shortID,
  69-			Title:     snap.Title,
  70-			Labels:    labels,
  71-			CreatedAt: snap.CreateTime,
  72-			Status:    snap.Status.String(),
  73-		})
  74-	}
  75-
  76-	return issues, nil
  77-}
  78-
  79-// FilterSpec represents a filter criteria
  80-type FilterSpec struct {
  81-	Labels   []string      // List of labels that must all be present
  82-	AgeOp    string        // "<" or ">"
  83-	AgeValue time.Duration // The duration value for age comparison
  84-}
  85-
  86-// ParseFilter parses a filter string like "label:bug" or "age:<10d"
  87-func ParseFilter(filterStr string) (*FilterSpec, error) {
  88-	if filterStr == "" {
  89-		return nil, nil
  90-	}
  91-
  92-	spec := &FilterSpec{}
  93-
  94-	// Handle label filter: label:value or label:value1,value2,...
  95-	if strings.HasPrefix(filterStr, "label:") {
  96-		labelStr := strings.TrimPrefix(filterStr, "label:")
  97-		// Split by comma to support multiple labels
  98-		labels := strings.Split(labelStr, ",")
  99-		// Trim whitespace from each label
 100-		for i, label := range labels {
 101-			labels[i] = strings.TrimSpace(label)
 102-		}
 103-		spec.Labels = labels
 104-		return spec, nil
 105-	}
 106-
 107-	// Handle age filter: age:<duration>, age:>duration, or age:duration
 108-	if strings.HasPrefix(filterStr, "age:") {
 109-		ageStr := strings.TrimPrefix(filterStr, "age:")
 110-
 111-		// Check for operator
 112-		spec.AgeOp = "<" // Default operator
 113-		if strings.HasPrefix(ageStr, "<") {
 114-			ageStr = strings.TrimPrefix(ageStr, "<")
 115-		} else if strings.HasPrefix(ageStr, ">") {
 116-			spec.AgeOp = ">"
 117-			ageStr = strings.TrimPrefix(ageStr, ">")
 118-		}
 119-
 120-		// Parse duration like "10d", "1h", etc.
 121-		duration, err := time.ParseDuration(ageStr)
 122-		if err != nil {
 123-			// Try to parse days (e.g., "10d" -> "240h")
 124-			if strings.HasSuffix(ageStr, "d") {
 125-				daysStr := strings.TrimSuffix(ageStr, "d")
 126-				var days int
 127-				if _, err := fmt.Sscanf(daysStr, "%d", &days); err == nil {
 128-					duration = time.Duration(days) * 24 * time.Hour
 129-				} else {
 130-					return nil, fmt.Errorf("invalid age filter: %s", filterStr)
 131-				}
 132-			} else {
 133-				return nil, fmt.Errorf("invalid age filter: %s", filterStr)
 134-			}
 135-		}
 136-		spec.AgeValue = duration
 137-		return spec, nil
 138-	}
 139-
 140-	return nil, fmt.Errorf("invalid filter format: %s", filterStr)
 141-}
 142-
 143-// ApplyFilter filters issues based on the filter spec
 144-func ApplyFilter(issues []BugIssue, spec *FilterSpec) []BugIssue {
 145-	if spec == nil {
 146-		return issues
 147-	}
 148-
 149-	var filtered []BugIssue
 150-	now := time.Now()
 151-
 152-	for _, issue := range issues {
 153-		// Check label filter - must have ALL specified labels
 154-		if len(spec.Labels) > 0 {
 155-			hasAllLabels := true
 156-			for _, requiredLabel := range spec.Labels {
 157-				found := false
 158-				for _, label := range issue.Labels {
 159-					if label == requiredLabel {
 160-						found = true
 161-						break
 162-					}
 163-				}
 164-				if !found {
 165-					hasAllLabels = false
 166-					break
 167-				}
 168-			}
 169-			if !hasAllLabels {
 170-				continue
 171-			}
 172-		}
 173-
 174-		// Check age filter
 175-		if spec.AgeValue > 0 {
 176-			age := now.Sub(issue.CreatedAt)
 177-			if spec.AgeOp == "<" {
 178-				// Bugs newer than AgeValue
 179-				if age > spec.AgeValue {
 180-					continue
 181-				}
 182-			} else if spec.AgeOp == ">" {
 183-				// Bugs older than AgeValue
 184-				if age < spec.AgeValue {
 185-					continue
 186-				}
 187-			}
 188-		}
 189-
 190-		filtered = append(filtered, issue)
 191-	}
 192-
 193-	return filtered
 194-}
 195-
 196-// SortSpec represents sort criteria
 197-type SortSpec struct {
 198-	Field     string // "id" or "age"
 199-	Direction string // "asc" or "desc"
 200-}
 201-
 202-// ParseSort parses a sort string like "id:asc" or "age:desc"
 203-func ParseSort(sortStr string) (*SortSpec, error) {
 204-	if sortStr == "" {
 205-		// Default: age:desc (newest first)
 206-		return &SortSpec{Field: "age", Direction: "desc"}, nil
 207-	}
 208-
 209-	parts := strings.Split(sortStr, ":")
 210-	if len(parts) != 2 {
 211-		return nil, fmt.Errorf("invalid sort format: %s (expected field:direction)", sortStr)
 212-	}
 213-
 214-	field := strings.ToLower(parts[0])
 215-	direction := strings.ToLower(parts[1])
 216-
 217-	if field != "id" && field != "age" {
 218-		return nil, fmt.Errorf("invalid sort field: %s (expected 'id' or 'age')", field)
 219-	}
 220-
 221-	if direction != "asc" && direction != "desc" {
 222-		return nil, fmt.Errorf("invalid sort direction: %s (expected 'asc' or 'desc')", direction)
 223-	}
 224-
 225-	return &SortSpec{Field: field, Direction: direction}, nil
 226-}
 227-
 228-// ApplySort sorts issues based on the sort spec
 229-func ApplySort(issues []BugIssue, spec *SortSpec) {
 230-	if spec == nil {
 231-		return
 232-	}
 233-
 234-	sort.Slice(issues, func(i, j int) bool {
 235-		var less bool
 236-
 237-		switch spec.Field {
 238-		case "id":
 239-			less = issues[i].FullID < issues[j].FullID
 240-		case "age":
 241-			less = issues[i].CreatedAt.After(issues[j].CreatedAt) // Newer first
 242-		}
 243-
 244-		if spec.Direction == "desc" {
 245-			return !less
 246-		}
 247-		return less
 248-	})
 249-}
 250-
 251-// formatID renders the ID with the short portion in purple
 252-// Shows at least 7 characters, with the short ID portion colored
 253-func formatID(fullID, shortID string) string {
 254-	purpleStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("#9932CC")) // Purple
 255-	normalStyle := lipgloss.NewStyle()
 256-
 257-	// Always show at least 7 characters
 258-	displayLen := 7
 259-	if len(shortID) > displayLen {
 260-		displayLen = len(shortID)
 261-	}
 262-	if displayLen > len(fullID) {
 263-		displayLen = len(fullID)
 264-	}
 265-
 266-	shortLen := len(shortID)
 267-	if shortLen > displayLen {
 268-		shortLen = displayLen
 269-	}
 270-
 271-	return purpleStyle.Render(fullID[:shortLen]) + normalStyle.Render(fullID[shortLen:displayLen])
 272-}
 273-
 274-// stripANSI removes ANSI escape codes from a string
 275-func stripANSI(s string) string {
 276-	result := make([]rune, 0, len(s))
 277-	inEscape := false
 278-	for _, r := range s {
 279-		if inEscape {
 280-			if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') {
 281-				inEscape = false
 282-			}
 283-			continue
 284-		}
 285-		if r == '\x1b' {
 286-			inEscape = true
 287-			continue
 288-		}
 289-		result = append(result, r)
 290-	}
 291-	return string(result)
 292-}
 293-
 294-// printTable prints a simple text table with styled output
 295-// Columns are dynamically sized with at least 2 spaces between them
 296-func printTable(issues []BugIssue) error {
 297-	// First pass: calculate column widths based on content
 298-	const minColSpacing = 2
 299-	const maxSummaryLen = 70
 300-
 301-	idWidth := len("ID")
 302-	summaryWidth := len("Summary")
 303-	labelsWidth := len("Labels")
 304-	ageWidth := len("Age")
 305-
 306-	// Pre-calculate all formatted values and widths
 307-	type rowData struct {
 308-		id      string
 309-		summary string
 310-		labels  string
 311-		age     string
 312-	}
 313-
 314-	rows := make([]rowData, len(issues))
 315-
 316-	for i, issue := range issues {
 317-		// Format ID (with ANSI codes)
 318-		id := formatID(issue.FullID, issue.ShortID)
 319-		idVisualLen := len(stripANSI(id))
 320-		if idVisualLen > idWidth {
 321-			idWidth = idVisualLen
 322-		}
 323-
 324-		// Format summary (truncate at 70 chars)
 325-		summary := issue.Title
 326-		if len(summary) > maxSummaryLen {
 327-			summary = summary[:maxSummaryLen-3] + "..."
 328-		}
 329-		if len(summary) > summaryWidth {
 330-			summaryWidth = len(summary)
 331-		}
 332-
 333-		// Format labels
 334-		labels := strings.Join(issue.Labels, ", ")
 335-		if labels == "" {
 336-			labels = "-"
 337-		}
 338-		if len(labels) > labelsWidth {
 339-			labelsWidth = len(labels)
 340-		}
 341-
 342-		// Format age
 343-		age := humanize.Time(issue.CreatedAt)
 344-		if len(age) > ageWidth {
 345-			ageWidth = len(age)
 346-		}
 347-
 348-		rows[i] = rowData{id: id, summary: summary, labels: labels, age: age}
 349-	}
 350-
 351-	// Print header
 352-	fmt.Printf("%s%s%s%s%s%s%s\n",
 353-		padRight("ID", idWidth),
 354-		strings.Repeat(" ", minColSpacing),
 355-		padRight("Summary", summaryWidth),
 356-		strings.Repeat(" ", minColSpacing),
 357-		padRight("Labels", labelsWidth),
 358-		strings.Repeat(" ", minColSpacing),
 359-		padRight("Age", ageWidth),
 360-	)
 361-
 362-	// Print separator
 363-	totalWidth := idWidth + minColSpacing + summaryWidth + minColSpacing + labelsWidth + minColSpacing + ageWidth
 364-	fmt.Println(strings.Repeat("-", totalWidth))
 365-
 366-	// Print rows
 367-	for _, row := range rows {
 368-		fmt.Printf("%s%s%s%s%s%s%s\n",
 369-			padRight(row.id, idWidth),
 370-			strings.Repeat(" ", minColSpacing),
 371-			padRight(row.summary, summaryWidth),
 372-			strings.Repeat(" ", minColSpacing),
 373-			padRight(row.labels, labelsWidth),
 374-			strings.Repeat(" ", minColSpacing),
 375-			padRight(row.age, ageWidth),
 376-		)
 377-	}
 378-
 379-	return nil
 380-}
 381-
 382-// padRight pads a string to the specified width, accounting for ANSI codes
 383-func padRight(s string, width int) string {
 384-	visualLen := len(stripANSI(s))
 385-	padding := width - visualLen
 386-	if padding < 0 {
 387-		padding = 0
 388-	}
 389-	return s + strings.Repeat(" ", padding)
 390-}
 391-
 392-// promptUser prompts the user for input with the given message
 393-// It reads from stdin and returns the trimmed input
 394-func promptUser(prompt string) (string, error) {
 395-	fmt.Print(prompt)
 396-	scanner := bufio.NewScanner(os.Stdin)
 397-	if scanner.Scan() {
 398-		return strings.TrimSpace(scanner.Text()), nil
 399-	}
 400-	if err := scanner.Err(); err != nil {
 401-		return "", fmt.Errorf("failed to read input: %w", err)
 402-	}
 403-	return "", nil
 404-}
 405-
 406-// invalidateGitBugCache deletes the git-bug cache directories to force a rebuild.
 407-// This ensures the `bug` command and `git-bug` binary stay in sync when either
 408-// is used to modify issues or identities. Failures are silent (best effort).
 409-func invalidateGitBugCache(repoPath string) {
 410-	gitBugPath := filepath.Join(repoPath, ".git", "git-bug")
 411-
 412-	// Remove cache directory (serialized excerpts)
 413-	cachePath := filepath.Join(gitBugPath, "cache")
 414-	_ = os.RemoveAll(cachePath)
 415-
 416-	// Remove indexes directory (bleve search indexes)
 417-	indexesPath := filepath.Join(gitBugPath, "indexes")
 418-	_ = os.RemoveAll(indexesPath)
 419-}
 420-
 421-// promptUserWithScanner prompts the user using the provided scanner
 422-// It prints the prompt and returns the trimmed input
 423-func promptUserWithScanner(prompt string, scanner *bufio.Scanner) (string, error) {
 424-	fmt.Print(prompt)
 425-	if scanner.Scan() {
 426-		return strings.TrimSpace(scanner.Text()), nil
 427-	}
 428-	if err := scanner.Err(); err != nil {
 429-		return "", fmt.Errorf("failed to read input: %w", err)
 430-	}
 431-	return "", nil
 432-}
 433-
 434-// getEditor returns the editor command to use for interactive editing
 435-// Checks $EDITOR environment variable, falls back to 'vi' or 'notepad'
 436-func getEditor() string {
 437-	if editor := os.Getenv("EDITOR"); editor != "" {
 438-		return editor
 439-	}
 440-	// Default editors based on OS
 441-	if os.PathSeparator == '\\' {
 442-		return "notepad"
 443-	}
 444-	return "vi"
 445-}
 446-
 447-// launchEditorWithTemplate opens the user's preferred editor with a custom template
 448-// Returns the content written by the user
 449-func launchEditorWithTemplate(template string) (string, error) {
 450-	// Create temporary file
 451-	tmpFile, err := os.CreateTemp("", "bug-edit-*.txt")
 452-	if err != nil {
 453-		return "", fmt.Errorf("failed to create temp file: %w", err)
 454-	}
 455-	tmpPath := tmpFile.Name()
 456-	defer os.Remove(tmpPath)
 457-
 458-	if _, err := tmpFile.WriteString(template); err != nil {
 459-		tmpFile.Close()
 460-		return "", fmt.Errorf("failed to write template: %w", err)
 461-	}
 462-	tmpFile.Close()
 463-
 464-	// Launch editor
 465-	editor := getEditor()
 466-	cmd := exec.Command(editor, tmpPath)
 467-	cmd.Stdin = os.Stdin
 468-	cmd.Stdout = os.Stdout
 469-	cmd.Stderr = os.Stderr
 470-
 471-	if err := cmd.Run(); err != nil {
 472-		return "", fmt.Errorf("editor failed: %w", err)
 473-	}
 474-
 475-	// Read the result
 476-	content, err := os.ReadFile(tmpPath)
 477-	if err != nil {
 478-		return "", fmt.Errorf("failed to read edited file: %w", err)
 479-	}
 480-
 481-	return string(content), nil
 482-}
 483-
 484-// launchEditor opens the user's preferred editor with a temporary file for new issues
 485-// Returns the content written by the user
 486-func launchEditor(initialContent string) (string, error) {
 487-	// Write initial content with instructions
 488-	template := `;; Enter your issue title on the first line (required)
 489-;; Enter the description below the title
 490-;; Lines starting with ;; will be ignored
 491-;; Save and close the editor when done
 492-;;
 493-` + initialContent
 494-
 495-	return launchEditorWithTemplate(template)
 496-}
 497-
 498-// parseEditorContent parses editor output into title and message
 499-// First non-empty line is title, rest is message
 500-// Lines starting with ;; are treated as comments and ignored
 501-func parseEditorContent(content string) (title, message string, err error) {
 502-	lines := strings.Split(content, "\n")
 503-	var nonCommentLines []string
 504-	commentsFiltered := false
 505-
 506-	// Filter out comment lines and collect non-empty lines
 507-	for _, line := range lines {
 508-		trimmed := strings.TrimSpace(line)
 509-		if strings.HasPrefix(trimmed, ";;") {
 510-			commentsFiltered = true
 511-			continue
 512-		}
 513-		nonCommentLines = append(nonCommentLines, line)
 514-	}
 515-
 516-	// Find first non-empty line for title
 517-	titleIdx := -1
 518-	for i, line := range nonCommentLines {
 519-		if strings.TrimSpace(line) != "" {
 520-			title = strings.TrimSpace(line)
 521-			titleIdx = i
 522-			break
 523-		}
 524-	}
 525-
 526-	if titleIdx == -1 {
 527-		return "", "", fmt.Errorf("no title provided")
 528-	}
 529-
 530-	// Rest is the message (preserve original formatting)
 531-	if titleIdx+1 < len(nonCommentLines) {
 532-		messageLines := nonCommentLines[titleIdx+1:]
 533-		startIdx := 0
 534-		// Trim leading empty lines only if comments were filtered
 535-		if commentsFiltered {
 536-			for startIdx < len(messageLines) && strings.TrimSpace(messageLines[startIdx]) == "" {
 537-				startIdx++
 538-			}
 539-		}
 540-		// Trim trailing empty lines from message
 541-		endIdx := len(messageLines)
 542-		for endIdx > startIdx && strings.TrimSpace(messageLines[endIdx-1]) == "" {
 543-			endIdx--
 544-		}
 545-		message = strings.Join(messageLines[startIdx:endIdx], "\n")
 546-	}
 547-
 548-	return title, message, nil
 549-}
 550-
 551-// parseEditContent parses editor output for edit operations
 552-// It handles two formats:
 553-// 1. Issue format: title on first line, blank line, then description
 554-// 2. Comment format: single message (when parsing comment content)
 555-// For issue format, returns title and message
 556-// For comment format, set isComment=true to skip title parsing
 557-func parseEditContent(content string, isComment bool) (title, message string, err error) {
 558-	lines := strings.Split(content, "\n")
 559-	var nonCommentLines []string
 560-	commentsFiltered := false
 561-
 562-	// Filter out comment lines and collect non-empty lines
 563-	for _, line := range lines {
 564-		trimmed := strings.TrimSpace(line)
 565-		if strings.HasPrefix(trimmed, ";;") {
 566-			commentsFiltered = true
 567-			continue
 568-		}
 569-		nonCommentLines = append(nonCommentLines, line)
 570-	}
 571-
 572-	if isComment {
 573-		// Comment format: just extract the message
 574-		startIdx := 0
 575-		for startIdx < len(nonCommentLines) && strings.TrimSpace(nonCommentLines[startIdx]) == "" {
 576-			startIdx++
 577-		}
 578-		if startIdx >= len(nonCommentLines) {
 579-			return "", "", fmt.Errorf("no content provided")
 580-		}
 581-		endIdx := len(nonCommentLines)
 582-		for endIdx > startIdx && strings.TrimSpace(nonCommentLines[endIdx-1]) == "" {
 583-			endIdx--
 584-		}
 585-		message = strings.Join(nonCommentLines[startIdx:endIdx], "\n")
 586-		return "", message, nil
 587-	}
 588-
 589-	// Issue format: title on first line, description follows
 590-	// Find first non-empty line for title
 591-	titleIdx := -1
 592-	for i, line := range nonCommentLines {
 593-		if strings.TrimSpace(line) != "" {
 594-			title = strings.TrimSpace(line)
 595-			titleIdx = i
 596-			break
 597-		}
 598-	}
 599-
 600-	if titleIdx == -1 {
 601-		return "", "", fmt.Errorf("no title provided")
 602-	}
 603-
 604-	// Rest is the message (preserve original formatting)
 605-	if titleIdx+1 < len(nonCommentLines) {
 606-		messageLines := nonCommentLines[titleIdx+1:]
 607-		startIdx := 0
 608-		// Trim leading empty lines only if comments were filtered
 609-		if commentsFiltered {
 610-			for startIdx < len(messageLines) && strings.TrimSpace(messageLines[startIdx]) == "" {
 611-				startIdx++
 612-			}
 613-		}
 614-		// Trim trailing empty lines from message
 615-		endIdx := len(messageLines)
 616-		for endIdx > startIdx && strings.TrimSpace(messageLines[endIdx-1]) == "" {
 617-			endIdx--
 618-		}
 619-		message = strings.Join(messageLines[startIdx:endIdx], "\n")
 620-	}
 621-
 622-	return title, message, nil
 623-}
 624-
 625-// detectJJConfig attempts to get user.name and user.email from jj config
 626-// Returns empty strings if .jj directory doesn't exist or config is not found
 627-// Note: We read the config file directly because 'jj config get' walks up the
 628-// directory tree and may find parent repo configs, which is not what we want.
 629-func detectJJConfig(repoPath string) (name, email string, err error) {
 630-	configPath := filepath.Join(repoPath, ".jj", "config.toml")
 631-
 632-	// Check if the config file exists
 633-	if _, err := os.Stat(configPath); os.IsNotExist(err) {
 634-		return "", "", nil
 635-	}
 636-
 637-	// Read the config file
 638-	content, err := os.ReadFile(configPath)
 639-	if err != nil {
 640-		return "", "", nil
 641-	}
 642-
 643-	// Parse the TOML content line by line to extract user.name and user.email
 644-	// We do simple parsing to avoid adding a TOML dependency
 645-	lines := strings.Split(string(content), "\n")
 646-	inUserSection := false
 647-
 648-	for _, line := range lines {
 649-		trimmed := strings.TrimSpace(line)
 650-
 651-		// Check if we're entering the [user] section
 652-		if trimmed == "[user]" {
 653-			inUserSection = true
 654-			continue
 655-		}
 656-
 657-		// Check if we're leaving the [user] section (new section starts)
 658-		if strings.HasPrefix(trimmed, "[") && trimmed != "[user]" {
 659-			inUserSection = false
 660-			continue
 661-		}
 662-
 663-		// Extract name and email from the user section
 664-		if inUserSection {
 665-			if strings.HasPrefix(trimmed, "name") {
 666-				parts := strings.SplitN(trimmed, "=", 2)
 667-				if len(parts) == 2 {
 668-					name = strings.Trim(strings.TrimSpace(parts[1]), `"`)
 669-				}
 670-			}
 671-			if strings.HasPrefix(trimmed, "email") {
 672-				parts := strings.SplitN(trimmed, "=", 2)
 673-				if len(parts) == 2 {
 674-					email = strings.Trim(strings.TrimSpace(parts[1]), `"`)
 675-				}
 676-			}
 677-		}
 678-	}
 679-
 680-	return name, email, nil
 681-}
 682-
 683-// createIdentity creates a new git-bug identity with the given name and email
 684-// If setAsUser is true, the identity will be set as the current user identity
 685-func createIdentity(repoPath, name, email string, setAsUser bool) error {
 686-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
 687-	if err != nil {
 688-		return fmt.Errorf("failed to open repository: %w", err)
 689-	}
 690-	defer repo.Close()
 691-
 692-	newIdentity, err := identity.NewIdentity(repo, name, email)
 693-	if err != nil {
 694-		return fmt.Errorf("failed to create identity: %w", err)
 695-	}
 696-
 697-	if err := newIdentity.Commit(repo); err != nil {
 698-		return fmt.Errorf("failed to commit identity: %w", err)
 699-	}
 700-
 701-	if setAsUser {
 702-		if err := identity.SetUserIdentity(repo, newIdentity); err != nil {
 703-			return fmt.Errorf("failed to set user identity: %w", err)
 704-		}
 705-	}
 706-
 707-	return nil
 708-}
 709-
 710-// getAgentIdentity retrieves the agent identity from the repository
 711-// The agent identity is created during 'bug init' with name "agent" and empty email
 712-func getAgentIdentity(repo repository.Repo) (*identity.Identity, error) {
 713-	// List all local identities
 714-	ids, err := identity.ListLocalIds(repo)
 715-	if err != nil {
 716-		return nil, fmt.Errorf("failed to list identities: %w", err)
 717-	}
 718-
 719-	// Find the identity with name "agent"
 720-	for _, id := range ids {
 721-		i, err := identity.ReadLocal(repo, id)
 722-		if err != nil {
 723-			continue // Skip identities we can't read
 724-		}
 725-		if i.Name() == "agent" {
 726-			return i, nil
 727-		}
 728-	}
 729-
 730-	return nil, fmt.Errorf("agent identity not found. Run 'bug init' first")
 731-}
 732-
 733-// checkIdentitiesExist checks if user and agent identities already exist
 734-// Returns (userExists, agentExists, error)
 735-func checkIdentitiesExist(repoPath string) (bool, bool, error) {
 736-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
 737-	if err != nil {
 738-		return false, false, fmt.Errorf("failed to open repository: %w", err)
 739-	}
 740-	defer repo.Close()
 741-
 742-	// Check if user identity is set
 743-	userExists, err := identity.IsUserIdentitySet(repo)
 744-	if err != nil {
 745-		return false, false, fmt.Errorf("failed to check user identity: %w", err)
 746-	}
 747-
 748-	// Check if agent identity exists
 749-	agentExists := false
 750-	if userExists {
 751-		_, err := getAgentIdentity(repo)
 752-		if err == nil {
 753-			agentExists = true
 754-		}
 755-	}
 756-
 757-	return userExists, agentExists, nil
 758-}
 759-
 760-// parseGitConfig reads and parses the git config file
 761-// Returns a map of section names to their key-value pairs
 762-// For sections with multiple values (like refspec), values are stored as a slice
 763-func parseGitConfig(repoPath string) (map[string]map[string][]string, error) {
 764-	configPath := filepath.Join(repoPath, ".git", "config")
 765-
 766-	data, err := os.ReadFile(configPath)
 767-	if err != nil {
 768-		if os.IsNotExist(err) {
 769-			return make(map[string]map[string][]string), nil
 770-		}
 771-		return nil, fmt.Errorf("failed to read git config: %w", err)
 772-	}
 773-
 774-	config := make(map[string]map[string][]string)
 775-	var currentSection string
 776-
 777-	lines := strings.Split(string(data), "\n")
 778-	for _, line := range lines {
 779-		trimmed := strings.TrimSpace(line)
 780-
 781-		// Skip empty lines and comments
 782-		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
 783-			continue
 784-		}
 785-
 786-		// Check for section header like [remote "origin"]
 787-		if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
 788-			sectionContent := trimmed[1 : len(trimmed)-1]
 789-			currentSection = sectionContent
 790-			if _, exists := config[currentSection]; !exists {
 791-				config[currentSection] = make(map[string][]string)
 792-			}
 793-			continue
 794-		}
 795-
 796-		// Parse key-value pairs within a section
 797-		if currentSection != "" && strings.Contains(trimmed, "=") {
 798-			parts := strings.SplitN(trimmed, "=", 2)
 799-			if len(parts) == 2 {
 800-				key := strings.TrimSpace(parts[0])
 801-				value := strings.TrimSpace(parts[1])
 802-				config[currentSection][key] = append(config[currentSection][key], value)
 803-			}
 804-		}
 805-	}
 806-
 807-	return config, nil
 808-}
 809-
 810-// writeGitConfig writes the git config back to file
 811-func writeGitConfig(repoPath string, config map[string]map[string][]string) error {
 812-	configPath := filepath.Join(repoPath, ".git", "config")
 813-
 814-	var buf strings.Builder
 815-
 816-	for section, values := range config {
 817-		buf.WriteString("[" + section + "]\n")
 818-		for key, vals := range values {
 819-			for _, val := range vals {
 820-				buf.WriteString("\t" + key + " = " + val + "\n")
 821-			}
 822-		}
 823-		buf.WriteString("\n")
 824-	}
 825-
 826-	if err := os.WriteFile(configPath, []byte(buf.String()), 0644); err != nil {
 827-		return fmt.Errorf("failed to write git config: %w", err)
 828-	}
 829-
 830-	return nil
 831-}
 832-
 833-// hasRefspec checks if a refspec is already configured
 834-func hasRefspec(config map[string][]string, refspec string) bool {
 835-	for _, existing := range config["fetch"] {
 836-		if existing == refspec {
 837-			return true
 838-		}
 839-	}
 840-	for _, existing := range config["push"] {
 841-		if existing == refspec {
 842-			return true
 843-		}
 844-	}
 845-	return false
 846-}
 847-
 848-// configureOriginRefspecs adds bug and identity refspecs to origin remote if not present
 849-// Returns true if any changes were made, false if all refspecs already exist
 850-func configureOriginRefspecs(repoPath string) (bool, error) {
 851-	config, err := parseGitConfig(repoPath)
 852-	if err != nil {
 853-		return false, err
 854-	}
 855-
 856-	// Check if origin remote exists
 857-	originSection := `remote "origin"`
 858-	if _, exists := config[originSection]; !exists {
 859-		return false, fmt.Errorf("no 'origin' remote configured")
 860-	}
 861-
 862-	// Define the refspecs we want to add
 863-	fetchRefspecs := []string{
 864-		"+refs/bugs/*:refs/remotes/origin/bugs/*",
 865-		"+refs/identities/*:refs/remotes/origin/identities/*",
 866-	}
 867-	pushRefspecs := []string{
 868-		"+refs/bugs/*:refs/bugs/*",
 869-		"+refs/identities/*:refs/identities/*",
 870-	}
 871-
 872-	changed := false
 873-
 874-	// Add fetch refspecs if not present
 875-	for _, refspec := range fetchRefspecs {
 876-		if !hasRefspec(config[originSection], refspec) {
 877-			config[originSection]["fetch"] = append(config[originSection]["fetch"], refspec)
 878-			changed = true
 879-		}
 880-	}
 881-
 882-	// Add push refspecs if not present
 883-	for _, refspec := range pushRefspecs {
 884-		if !hasRefspec(config[originSection], refspec) {
 885-			config[originSection]["push"] = append(config[originSection]["push"], refspec)
 886-			changed = true
 887-		}
 888-	}
 889-
 890-	if !changed {
 891-		return false, nil
 892-	}
 893-
 894-	if err := writeGitConfig(repoPath, config); err != nil {
 895-		return false, err
 896-	}
 897-
 898-	return true, nil
 899-}
 900-
 901-// resolveBugID resolves a bug ID (short or full) to a bug entity
 902-// It uses ShortIDMap to handle short ID resolution
 903-func resolveBugID(repoPath, idStr string) (*bug.Bug, error) {
 904-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
 905-	if err != nil {
 906-		return nil, fmt.Errorf("failed to open repository: %w", err)
 907-	}
 908-	defer repo.Close()
 909-
 910-	// Generate short ID map for resolution
 911-	gen := NewShortIDGenerator(repoPath)
 912-	shortIDMap, err := gen.Generate()
 913-	if err != nil {
 914-		return nil, fmt.Errorf("failed to generate short IDs: %w", err)
 915-	}
 916-
 917-	// Try to resolve the ID (could be short or full)
 918-	fullID, err := shortIDMap.GetFullID(idStr)
 919-	if err != nil {
 920-		return nil, fmt.Errorf("failed to resolve bug ID %q: %w", idStr, err)
 921-	}
 922-
 923-	// Read the bug using the full ID
 924-	b, err := bug.Read(repo, entity.Id(fullID))
 925-	if err != nil {
 926-		return nil, fmt.Errorf("failed to read bug %q: %w", idStr, err)
 927-	}
 928-
 929-	return b, nil
 930-}
 931-
 932-// resolveCommentID resolves a comment ID (short or full) and returns the bug and comment
 933-// It uses ShortIDMap to handle short ID resolution
 934-func resolveCommentID(repoPath, idStr string) (*bug.Bug, *bug.Comment, error) {
 935-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
 936-	if err != nil {
 937-		return nil, nil, fmt.Errorf("failed to open repository: %w", err)
 938-	}
 939-	defer repo.Close()
 940-
 941-	// Generate short ID map for resolution
 942-	gen := NewShortIDGenerator(repoPath)
 943-	shortIDMap, err := gen.Generate()
 944-	if err != nil {
 945-		return nil, nil, fmt.Errorf("failed to generate short IDs: %w", err)
 946-	}
 947-
 948-	// Try to resolve the ID (could be short or full)
 949-	fullID, err := shortIDMap.GetFullID(idStr)
 950-	if err != nil {
 951-		return nil, nil, fmt.Errorf("failed to resolve comment ID %q: %w", idStr, err)
 952-	}
 953-
 954-	// Parse the combined ID to get bug ID prefix and comment ID prefix
 955-	combinedID := entity.CombinedId(fullID)
 956-	bugIDPrefix, _ := entity.SeparateIds(string(combinedID))
 957-
 958-	// SeparateIds returns prefixes, not full IDs. We need to look up the full bug ID.
 959-	// The bug ID prefix is 50 characters, but we need the full 64-character ID.
 960-	fullBugID, err := shortIDMap.GetFullID(bugIDPrefix)
 961-	if err != nil {
 962-		return nil, nil, fmt.Errorf("failed to resolve bug ID from prefix %q: %w", bugIDPrefix, err)
 963-	}
 964-
 965-	// Read the bug using the full bug ID
 966-	b, err := bug.Read(repo, entity.Id(fullBugID))
 967-	if err != nil {
 968-		return nil, nil, fmt.Errorf("failed to read bug for comment %q: %w", idStr, err)
 969-	}
 970-
 971-	// Search for the comment in the bug
 972-	snap := b.Compile()
 973-	comment, err := snap.SearchComment(combinedID)
 974-	if err != nil {
 975-		return nil, nil, fmt.Errorf("comment %q not found in bug: %w", idStr, err)
 976-	}
 977-
 978-	return b, comment, nil
 979-}
 980-
 981-// IDType represents the type of ID resolved
 982-type IDType int
 983-
 984-const (
 985-	IDTypeBug IDType = iota
 986-	IDTypeComment
 987-)
 988-
 989-// ResolvedID holds the result of ID resolution
 990-type ResolvedID struct {
 991-	Type    IDType
 992-	Bug     *bug.Bug
 993-	Comment *bug.Comment
 994-	ID      string // Full ID
 995-}
 996-
 997-// resolveID attempts to resolve an ID as a bug ID first, then as a comment ID
 998-// Returns ResolvedID with the type and appropriate data
 999-func resolveID(repoPath, idStr string) (*ResolvedID, error) {
1000-	// Try to resolve as bug ID first
1001-	b, err := resolveBugID(repoPath, idStr)
1002-	if err == nil {
1003-		return &ResolvedID{
1004-			Type: IDTypeBug,
1005-			Bug:  b,
1006-			ID:   b.Id().String(),
1007-		}, nil
1008-	}
1009-
1010-	// Try to resolve as comment ID
1011-	b, comment, err := resolveCommentID(repoPath, idStr)
1012-	if err == nil {
1013-		return &ResolvedID{
1014-			Type:    IDTypeComment,
1015-			Bug:     b,
1016-			Comment: comment,
1017-			ID:      comment.CombinedId().String(),
1018-		}, nil
1019-	}
1020-
1021-	// Neither resolved - return combined error
1022-	return nil, fmt.Errorf("failed to resolve ID %q as bug or comment: %w", idStr, err)
1023-}
1024-
1025-// launchCommentEditor opens the user's preferred editor for comment input
1026-// Returns the content written by the user with comment lines filtered
1027-func launchCommentEditor(initialContent string) (string, error) {
1028-	// Write initial content with instructions
1029-	template := `;; Enter your comment below
1030-;; Lines starting with ;; will be ignored
1031-;; You can use markdown formatting
1032-;; Save and close the editor when done
1033-;;
1034-` + initialContent
1035-
1036-	return launchEditorWithTemplate(template)
1037-}
1038-
1039-// runRead reads and displays a bug/issue with all its comments
1040-// Outputs structured metadata followed by description and all comments
1041-func runRead(repoPath, bugIDStr string) error {
1042-	// Resolve bug ID
1043-	b, err := resolveBugID(repoPath, bugIDStr)
1044-	if err != nil {
1045-		return err
1046-	}
1047-
1048-	// Compile bug snapshot
1049-	snap := b.Compile()
1050-
1051-	// Get the first comment (description) for author info
1052-	var authorName, authorEmail string
1053-	var createdAt time.Time
1054-	if len(snap.Comments) > 0 {
1055-		firstComment := snap.Comments[0]
1056-		authorName = firstComment.Author.Name()
1057-		authorEmail = firstComment.Author.Email()
1058-		// Parse the formatted time string
1059-		createdAt, _ = time.Parse("Mon Jan 2 15:04:05 2006 -0700", firstComment.FormatTime())
1060-	}
1061-	if createdAt.IsZero() {
1062-		createdAt = snap.CreateTime
1063-	}
1064-
1065-	// Format author string (name only if no email)
1066-	authorStr := authorName
1067-	if authorEmail != "" {
1068-		authorStr = fmt.Sprintf("%s <%s>", authorName, authorEmail)
1069-	}
1070-
1071-	// Format labels
1072-	labelsStr := "-"
1073-	if len(snap.Labels) > 0 {
1074-		labels := make([]string, len(snap.Labels))
1075-		for i, label := range snap.Labels {
1076-			labels[i] = string(label)
1077-		}
1078-		labelsStr = strings.Join(labels, ", ")
1079-	}
1080-
1081-	// Print metadata header
1082-	fmt.Printf("Title: %s\n", snap.Title)
1083-	fmt.Printf("Author: %s\n", authorStr)
1084-	fmt.Printf("Created: %s\n", createdAt.Format(time.RFC3339))
1085-	fmt.Printf("Status: %s\n", snap.Status.String())
1086-	fmt.Printf("Labels: %s\n", labelsStr)
1087-	fmt.Println()
1088-
1089-	// Print description (first comment)
1090-	if len(snap.Comments) > 0 {
1091-		description := snap.Comments[0].Message
1092-		if description != "" {
1093-			fmt.Println(description)
1094-		}
1095-
1096-		// Print additional comments
1097-		for i := 1; i < len(snap.Comments); i++ {
1098-			comment := snap.Comments[i]
1099-			commentID := comment.CombinedId().String()
1100-			if len(commentID) > 7 {
1101-				commentID = commentID[:7]
1102-			}
1103-
1104-			commentAuthor := comment.Author.Name()
1105-			commentDate := comment.FormatTime()
1106-
1107-			fmt.Println()
1108-			fmt.Printf("Comment %s made by %s on %s:\n\n", commentID, commentAuthor, commentDate)
1109-			fmt.Println(comment.Message)
1110-		}
1111-	}
1112-
1113-	return nil
1114-}
1115-
1116-// parseCommentContent parses editor output for comments
1117-// Lines starting with ;; are treated as comments and ignored
1118-func parseCommentContent(content string) (string, error) {
1119-	lines := strings.Split(content, "\n")
1120-	var nonCommentLines []string
1121-
1122-	// Filter out comment lines
1123-	for _, line := range lines {
1124-		trimmed := strings.TrimSpace(line)
1125-		if strings.HasPrefix(trimmed, ";;") {
1126-			continue
1127-		}
1128-		nonCommentLines = append(nonCommentLines, line)
1129-	}
1130-
1131-	// Find first non-empty line
1132-	startIdx := 0
1133-	for startIdx < len(nonCommentLines) && strings.TrimSpace(nonCommentLines[startIdx]) == "" {
1134-		startIdx++
1135-	}
1136-
1137-	// If no content left, return error
1138-	if startIdx >= len(nonCommentLines) {
1139-		return "", fmt.Errorf("no comment provided")
1140-	}
1141-
1142-	// Trim trailing empty lines
1143-	endIdx := len(nonCommentLines)
1144-	for endIdx > startIdx && strings.TrimSpace(nonCommentLines[endIdx-1]) == "" {
1145-		endIdx--
1146-	}
1147-
1148-	// Join remaining lines
1149-	message := strings.Join(nonCommentLines[startIdx:endIdx], "\n")
1150-
1151-	return message, nil
1152-}
1153-
1154-// runNew creates a new bug/issue in the repository
1155-// If title is empty, opens editor for interactive input
1156-func runNew(repoPath, title, message string) error {
1157-	// If no title provided via flags, open editor
1158-	if title == "" && message == "" {
1159-		content, err := launchEditor("")
1160-		if err != nil {
1161-			return err
1162-		}
1163-		title, message, err = parseEditorContent(content)
1164-		if err != nil {
1165-			return err
1166-		}
1167-	} else if title == "" {
1168-		// Message provided but no title - still need editor
1169-		content, err := launchEditor(message)
1170-		if err != nil {
1171-			return err
1172-		}
1173-		title, message, err = parseEditorContent(content)
1174-		if err != nil {
1175-			return err
1176-		}
1177-	}
1178-	// If only title provided, message can be empty (that's OK)
1179-
1180-	// Validate title
1181-	if strings.TrimSpace(title) == "" {
1182-		return fmt.Errorf("title cannot be empty")
1183-	}
1184-
1185-	// Open repository
1186-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1187-	if err != nil {
1188-		return fmt.Errorf("failed to open repository: %w", err)
1189-	}
1190-	defer repo.Close()
1191-
1192-	// Get user identity
1193-	author, err := identity.GetUserIdentity(repo)
1194-	if err != nil {
1195-		return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
1196-	}
1197-
1198-	// Create the bug
1199-	unixTime := time.Now().Unix()
1200-	newBug, _, err := bug.Create(author, unixTime, title, message, nil, nil)
1201-	if err != nil {
1202-		return fmt.Errorf("failed to create bug: %w", err)
1203-	}
1204-
1205-	// Commit the bug to the repository
1206-	if err := newBug.Commit(repo); err != nil {
1207-		return fmt.Errorf("failed to commit bug: %w", err)
1208-	}
1209-
1210-	// Get the bug ID and display first 7 characters (like bug ls table)
1211-	bugID := newBug.Id().String()
1212-	if len(bugID) > 7 {
1213-		bugID = bugID[:7]
1214-	}
1215-
1216-	fmt.Printf("Created bug %s: %s\n", bugID, title)
1217-
1218-	// Invalidate cache so git-bug sees the new bug
1219-	invalidateGitBugCache(repoPath)
1220-
1221-	return nil
1222-}
1223-
1224-// runInit creates initial identities in the repository
1225-// It attempts to get user info from jj config if .jj exists, otherwise prompts the user
1226-// It also creates an agent identity
1227-func runInit(repoPath string) error {
1228-	return runInitWithReader(repoPath, os.Stdin)
1229-}
1230-
1231-// ErrNoOriginRemote is returned when no origin remote is configured
1232-type ErrNoOriginRemote struct{}
1233-
1234-func (e ErrNoOriginRemote) Error() string {
1235-	return "no 'origin' remote configured"
1236-}
1237-
1238-// runInitWithReader is the testable version that accepts an io.Reader for stdin
1239-func runInitWithReader(repoPath string, reader io.Reader) error {
1240-	// Check if identities already exist (idempotent behavior)
1241-	userExists, agentExists, err := checkIdentitiesExist(repoPath)
1242-	if err != nil {
1243-		return err
1244-	}
1245-
1246-	// Track if we need to prompt for user info
1247-	var name, email string
1248-
1249-	if userExists {
1250-		fmt.Println("User identity already exists, skipping creation")
1251-	} else {
1252-		// Try to get user info from jj config
1253-		name, email, err = detectJJConfig(repoPath)
1254-		if err != nil {
1255-			return fmt.Errorf("failed to detect jj config: %w", err)
1256-		}
1257-
1258-		// If jj config was found, use it
1259-		if name != "" && email != "" {
1260-			fmt.Printf("Creating user identity from jj config: %s <%s>\n", name, email)
1261-		} else {
1262-			// Prompt user for name and email
1263-			fmt.Println("No jj config found. Please provide your information:")
1264-
1265-			// Create a single scanner to use for all reads
1266-			scanner := bufio.NewScanner(reader)
1267-
1268-			name, err = promptUserWithScanner("Name: ", scanner)
1269-			if err != nil {
1270-				return fmt.Errorf("failed to read name: %w", err)
1271-			}
1272-			if name == "" {
1273-				return fmt.Errorf("name cannot be empty")
1274-			}
1275-
1276-			email, err = promptUserWithScanner("Email: ", scanner)
1277-			if err != nil {
1278-				return fmt.Errorf("failed to read email: %w", err)
1279-			}
1280-		}
1281-
1282-		// Create user identity
1283-		fmt.Printf("Creating user identity: %s <%s>\n", name, email)
1284-		if err := createIdentity(repoPath, name, email, true); err != nil {
1285-			return fmt.Errorf("failed to create user identity: %w", err)
1286-		}
1287-		fmt.Println("User identity created successfully")
1288-	}
1289-
1290-	if agentExists {
1291-		fmt.Println("Agent identity already exists, skipping creation")
1292-	} else {
1293-		// Create agent identity
1294-		// Empty email is intentional per spec: equivalent to `git-bug user new --name 'agent' --email ''`
1295-		fmt.Println("Creating agent identity...")
1296-		if err := createIdentity(repoPath, "agent", "", false); err != nil {
1297-			return fmt.Errorf("failed to create agent identity: %w", err)
1298-		}
1299-		fmt.Println("Agent identity created successfully")
1300-	}
1301-
1302-	// Configure git remote refspecs for origin
1303-	configured, err := configureOriginRefspecs(repoPath)
1304-	if err != nil {
1305-		if err.Error() == "no 'origin' remote configured" {
1306-			return ErrNoOriginRemote{}
1307-		}
1308-		return fmt.Errorf("failed to configure git refspecs: %w", err)
1309-	}
1310-
1311-	if configured {
1312-		fmt.Println("Configured git to automatically sync bugs and identities with origin")
1313-	} else if userExists && agentExists {
1314-		// All identities exist and refspecs are configured - nothing to do
1315-		fmt.Println("Nothing to do")
1316-	}
1317-
1318-	// Invalidate cache so git-bug sees the new identities
1319-	invalidateGitBugCache(repoPath)
1320-
1321-	return nil
1322-}
1323-
1324-var (
1325-	repoPath        string
1326-	filterFlag      string
1327-	sortFlag        string
1328-	newTitle        string
1329-	newMessage      string
1330-	agentTitle      string
1331-	agentMessage    string
1332-	commentMessage  string
1333-	commentBugID    string
1334-	agentCommentMsg string
1335-	editTitle       string
1336-	editMessage     string
1337-	agentEditTitle  string
1338-	agentEditMsg    string
1339-	statusFlag      string
1340-)
1341-
1342-var rootCmd = &cobra.Command{
1343-	Use:   "bug",
1344-	Short: "A CLI for git-bug issue tracking",
1345-	Long: `bug is a simplified CLI interface to git-bug issues and comments.
1346-It provides an easy way to list and manage issues in your git repository.`,
1347-}
1348-
1349-var listCmd = &cobra.Command{
1350-	Use:     "list",
1351-	Aliases: []string{"ls"},
1352-	Short:   "List all issues in the repository",
1353-	Long: `Display a table of all git-bug issues with their ID, summary, labels, and age.
1354-
1355-Filtering:
1356-  --filter label:bug         Show only issues with label "bug"
1357-  --filter age:<10d          Show only issues newer than 10 days
1358-
1359-Sorting:
1360-  --sort id:asc              Sort by ID (ascending)
1361-  --sort id:desc             Sort by ID (descending)
1362-  --sort age:asc             Sort by age, oldest first
1363-  --sort age:desc            Sort by age, newest first (default)
1364-
1365-Status Filtering:
1366-  -S, --status open          Show only open issues (default)
1367-  -S, --status closed        Show only closed issues
1368-  -S, --status all           Show all issues`,
1369-	RunE: func(cmd *cobra.Command, args []string) error {
1370-		// Load issues
1371-		issues, err := LoadBugs(repoPath)
1372-		if err != nil {
1373-			return err
1374-		}
1375-
1376-		if len(issues) == 0 {
1377-			fmt.Println("No Issues")
1378-			return nil
1379-		}
1380-
1381-		// Parse and apply filter
1382-		filterSpec, err := ParseFilter(filterFlag)
1383-		if err != nil {
1384-			return err
1385-		}
1386-		issues = ApplyFilter(issues, filterSpec)
1387-
1388-		// Apply status filter
1389-		if statusFlag != "" && strings.ToLower(statusFlag) != "all" {
1390-			statusLower := strings.ToLower(statusFlag)
1391-			if statusLower != "open" && statusLower != "closed" {
1392-				return fmt.Errorf("invalid status: %s (expected 'open', 'closed', or 'all')", statusFlag)
1393-			}
1394-			var filtered []BugIssue
1395-			for _, issue := range issues {
1396-				if strings.ToLower(issue.Status) == statusLower {
1397-					filtered = append(filtered, issue)
1398-				}
1399-			}
1400-			issues = filtered
1401-		}
1402-
1403-		// Parse and apply sort
1404-		sortSpec, err := ParseSort(sortFlag)
1405-		if err != nil {
1406-			return err
1407-		}
1408-		ApplySort(issues, sortSpec)
1409-
1410-		if len(issues) == 0 {
1411-			fmt.Println("No Issues")
1412-			return nil
1413-		}
1414-
1415-		return printTable(issues)
1416-	},
1417-}
1418-
1419-var initCmd = &cobra.Command{
1420-	Use:   "init",
1421-	Short: "Initialize git-bug identities in the repository",
1422-	Long: `Initialize git-bug by creating initial identities in the repository.
1423-
1424-If a .jj directory exists, the command will attempt to read user.name and user.email
1425-from jj config and create a user identity. Otherwise, it will prompt you interactively
1426-for your name and email. It also creates an agent identity.
1427-
1428-This command also configures git push/fetch refspecs for the 'origin' remote
1429-to automatically sync bugs and identities.
1430-
1431-Examples:
1432-  bug init                    # Initialize in current directory
1433-  bug init --repo /path/to/repo  # Initialize in specific repository`,
1434-	RunE: func(cmd *cobra.Command, args []string) error {
1435-		err := runInit(repoPath)
1436-		if err != nil {
1437-			// Check for special error types
1438-			if _, ok := err.(ErrNoOriginRemote); ok {
1439-				fmt.Fprintln(os.Stderr, "Error: No 'origin' remote configured.")
1440-				fmt.Fprintln(os.Stderr, "")
1441-				fmt.Fprintln(os.Stderr, "Please add a remote with: git remote add origin <url>")
1442-				fmt.Fprintln(os.Stderr, "Then run 'bug init' again to configure bug push/fetch refspecs.")
1443-				os.Exit(1)
1444-			}
1445-			return err
1446-		}
1447-		return nil
1448-	},
1449-}
1450-
1451-var newCmd = &cobra.Command{
1452-	Use:   "new",
1453-	Short: "Create a new issue/bug",
1454-	Long: `Create a new issue in the git-bug repository.
1455-
1456-You can provide the title and message via flags:
1457-  bug new --title "Bug title" --message "Detailed description"
1458-  bug new -t "Bug title" -m "Detailed description"
1459-
1460-Or launch an editor to write them interactively:
1461-  bug new
1462-
1463-The editor will open with a template. The first non-empty line becomes the title,
1464-and the rest becomes the description.`,
1465-	RunE: func(cmd *cobra.Command, args []string) error {
1466-		return runNew(repoPath, newTitle, newMessage)
1467-	},
1468-}
1469-
1470-var agentCmd = &cobra.Command{
1471-	Use:   "agent",
1472-	Short: "Agent commands for automated bug creation",
1473-	Long: `Commands for creating bugs as the agent identity.
1474-
1475-These commands are designed for automated/agent use and are non-interactive.
1476-All agent commands use the agent identity created during 'bug init'.
1477-
1478-Example:
1479-  bug agent new --title "Automated bug" --message "Found by CI"`,
1480-}
1481-
1482-var agentNewCmd = &cobra.Command{
1483-	Use:   "new",
1484-	Short: "Create a new issue as the agent",
1485-	Long: `Create a new issue in the git-bug repository using the agent identity.
1486-
1487-This command is non-interactive and requires both --title and --message flags.
1488-It uses the agent identity created during 'bug init'.
1489-
1490-Example:
1491-  bug agent new --title "CI Failure" --message "Build failed on commit abc123"
1492-  bug agent new -t "Bug found" -m "Automated scan detected issue"`,
1493-	RunE: func(cmd *cobra.Command, args []string) error {
1494-		return runAgentNew(repoPath, agentTitle, agentMessage)
1495-	},
1496-}
1497-
1498-var commentCmd = &cobra.Command{
1499-	Use:   "comment [bugid]",
1500-	Short: "Add a comment to an existing bug",
1501-	Long: `Add a comment to an existing bug in the git-bug repository.
1502-
1503-You can provide the comment via the message flag:
1504-  bug comment abc1234 --message "This is my comment"
1505-  bug comment abc1234 -m "This is my comment"
1506-
1507-Or launch an editor to write it interactively:
1508-  bug comment abc1234
1509-
1510-The editor will open with a template. Lines starting with ;; are ignored.
1511-You can use markdown formatting in your comment.`,
1512-	Args: cobra.ExactArgs(1),
1513-	RunE: func(cmd *cobra.Command, args []string) error {
1514-		commentBugID = args[0]
1515-		return runComment(repoPath, commentBugID, commentMessage)
1516-	},
1517-}
1518-
1519-var editCmd = &cobra.Command{
1520-	Use:   "edit [bugid|commentid]",
1521-	Short: "Edit an issue or comment",
1522-	Long: `Edit an existing issue or comment in the git-bug repository.
1523-
1524-The ID can be a bug ID (full or short) or a comment ID (full or short).
1525-The command first tries to resolve the ID as a bug, then as a comment.
1526-
1527-For issues:
1528-  bug edit abc1234                    # Open editor with current title and description
1529-  bug edit abc1234 -t "New Title"     # Update only the title
1530-  bug edit abc1234 -m "New desc"      # Update only the description
1531-  bug edit abc1234 -t "Title" -m "Desc"  # Update both
1532-
1533-For comments:
1534-  bug edit def5678                    # Open editor with current comment text
1535-  bug edit def5678 -m "New text"      # Update the comment
1536-
1537-When using the editor:
1538-  - For issues: title on first line, blank line, then description
1539-  - For comments: edit the comment text directly
1540-  - Lines starting with ;; are ignored (instructions)`,
1541-	Args: cobra.ExactArgs(1),
1542-	RunE: func(cmd *cobra.Command, args []string) error {
1543-		return runEdit(repoPath, args[0], editTitle, editMessage)
1544-	},
1545-}
1546-
1547-var readCmd = &cobra.Command{
1548-	Use:     "read [bugid]",
1549-	Aliases: []string{"show"},
1550-	Short:   "Display a bug/issue with all comments",
1551-	Long: `Display a bug/issue and all its comments in a structured format.
1552-
1553-The output includes metadata (title, author, creation date, status, labels)
1554-followed by the description and all comments.
1555-
1556-Examples:
1557-  bug read abc1234           # Display bug with ID prefix abc1234
1558-  bug show abc1234           # Same as above (alias)
1559-  bug read --repo /path abc  # Display bug from specific repository`,
1560-	Args: cobra.ExactArgs(1),
1561-	RunE: func(cmd *cobra.Command, args []string) error {
1562-		return runRead(repoPath, args[0])
1563-	},
1564-}
1565-
1566-var rmCmd = &cobra.Command{
1567-	Use:     "rm [bugid|commentid]",
1568-	Aliases: []string{"remove"},
1569-	Short:   "Remove a bug/issue by its ID",
1570-	Long: `Remove a bug/issue from the git-bug repository.
1571-
1572-The ID can be a bug ID (full or short). Comments cannot be removed individually;
1573-use 'bug rm <bug-id>' to remove the entire issue including all its comments.
1574-
1575-WARNING: This action is permanent and cannot be undone.
1576-
1577-Examples:
1578-  bug rm abc1234                    # Remove bug with short ID abc1234
1579-  bug rm abc1234567890abcdef        # Remove bug with full ID
1580-  bug remove abc1234                # Same as above (alias)
1581-
1582-Note: If a comment ID is provided, the command will return an error explaining
1583-that individual comments cannot be removed.`,
1584-	Args: cobra.ExactArgs(1),
1585-	RunE: func(cmd *cobra.Command, args []string) error {
1586-		return runRemove(repoPath, args[0])
1587-	},
1588-}
1589-
1590-var agentRmCmd = &cobra.Command{
1591-	Use:     "rm [bugid]",
1592-	Aliases: []string{"remove"},
1593-	Short:   "Remove a bug/issue as the agent",
1594-	Long: `Remove a bug/issue from the git-bug repository using the agent identity.
1595-
1596-This command is non-interactive and uses the agent identity created during 'bug init'.
1597-Comments cannot be removed individually; use 'bug agent rm <bug-id>' to remove
1598-	the entire issue including all its comments.
1599-
1600-WARNING: This action is permanent and cannot be undone.
1601-
1602-Examples:
1603-  bug agent rm abc1234              # Remove bug with short ID abc1234
1604-  bug agent remove abc1234          # Same as above (alias)`,
1605-	Args: cobra.ExactArgs(1),
1606-	RunE: func(cmd *cobra.Command, args []string) error {
1607-		return runRemove(repoPath, args[0])
1608-	},
1609-}
1610-
1611-var openCmd = &cobra.Command{
1612-	Use:   "open [bugid]",
1613-	Short: "Open a closed bug/issue",
1614-	Long: `Open a bug/issue by changing its status from closed to open.
1615-
1616-The ID can be a bug ID (full or short). This command changes the bug's
1617-	status to "open".
1618-
1619-Examples:
1620-  bug open abc1234                    # Open bug with short ID abc1234
1621-  bug open abc1234567890abcdef        # Open bug with full ID`,
1622-	Args: cobra.ExactArgs(1),
1623-	RunE: func(cmd *cobra.Command, args []string) error {
1624-		return runOpen(repoPath, args[0])
1625-	},
1626-}
1627-
1628-var closeCmd = &cobra.Command{
1629-	Use:   "close [bugid]",
1630-	Short: "Close an open bug/issue",
1631-	Long: `Close a bug/issue by changing its status from open to closed.
1632-
1633-The ID can be a bug ID (full or short). This command changes the bug's
1634-	status to "closed".
1635-
1636-Examples:
1637-  bug close abc1234                    # Close bug with short ID abc1234
1638-  bug close abc1234567890abcdef        # Close bug with full ID`,
1639-	Args: cobra.ExactArgs(1),
1640-	RunE: func(cmd *cobra.Command, args []string) error {
1641-		return runClose(repoPath, args[0])
1642-	},
1643-}
1644-
1645-var agentCommentCmd = &cobra.Command{
1646-	Use:   "comment [bugid]",
1647-	Short: "Add a comment to a bug as the agent",
1648-	Long: `Add a comment to an existing bug using the agent identity.
1649-
1650-This command is non-interactive and requires the --message flag.
1651-It uses the agent identity created during 'bug init'.
1652-
1653-Example:
1654-  bug agent comment abc1234 --message "Automated analysis complete"
1655-  bug agent comment abc1234 -m "Build passed all tests"`,
1656-	Args: cobra.ExactArgs(1),
1657-	RunE: func(cmd *cobra.Command, args []string) error {
1658-		commentBugID = args[0]
1659-		return runAgentComment(repoPath, commentBugID, agentCommentMsg)
1660-	},
1661-}
1662-
1663-var agentEditCmd = &cobra.Command{
1664-	Use:   "edit [bugid|commentid]",
1665-	Short: "Edit an issue or comment as the agent",
1666-	Long: `Edit an existing issue or comment using the agent identity.
1667-
1668-This command is non-interactive and requires at least one of --title or --message.
1669-It uses the agent identity created during 'bug init'.
1670-
1671-For issues:
1672-  bug agent edit abc1234 --title "New Title"
1673-  bug agent edit abc1234 --message "New description"
1674-  bug agent edit abc1234 --title "Title" --message "Description"
1675-
1676-For comments:
1677-  bug agent edit def5678 --message "Updated comment text"`,
1678-	Args: cobra.ExactArgs(1),
1679-	RunE: func(cmd *cobra.Command, args []string) error {
1680-		return runAgentEdit(repoPath, args[0], agentEditTitle, agentEditMsg)
1681-	},
1682-}
1683-
1684-var agentOpenCmd = &cobra.Command{
1685-	Use:   "open [bugid]",
1686-	Short: "Open a closed bug/issue as the agent",
1687-	Long: `Open a bug/issue by changing its status from closed to open.
1688-
1689-This command uses the agent identity created during 'bug init'.
1690-It is non-interactive and designed for automated/agent use.
1691-
1692-Examples:
1693-  bug agent open abc1234              # Open bug with short ID abc1234`,
1694-	Args: cobra.ExactArgs(1),
1695-	RunE: func(cmd *cobra.Command, args []string) error {
1696-		return runAgentOpen(repoPath, args[0])
1697-	},
1698-}
1699-
1700-var agentCloseCmd = &cobra.Command{
1701-	Use:   "close [bugid]",
1702-	Short: "Close an open bug/issue as the agent",
1703-	Long: `Close a bug/issue by changing its status from open to closed.
1704-
1705-This command uses the agent identity created during 'bug init'.
1706-It is non-interactive and designed for automated/agent use.
1707-
1708-Examples:
1709-  bug agent close abc1234              # Close bug with short ID abc1234`,
1710-	Args: cobra.ExactArgs(1),
1711-	RunE: func(cmd *cobra.Command, args []string) error {
1712-		return runAgentClose(repoPath, args[0])
1713-	},
1714-}
1715-
1716-var agentReadCmd = &cobra.Command{
1717-	Use:   "read [bugid]",
1718-	Short: "Display a bug/issue with all comments",
1719-	Long: `Display a bug/issue and all its comments in a structured format.
1720-
1721-This is the same as 'bug read' but accessible under the agent subcommand.
1722-The output includes metadata (title, author, creation date, status, labels)
1723-followed by the description and all comments.
1724-
1725-Examples:
1726-  bug agent read abc1234     # Display bug with ID prefix abc1234`,
1727-	Args: cobra.ExactArgs(1),
1728-	RunE: func(cmd *cobra.Command, args []string) error {
1729-		return runRead(repoPath, args[0])
1730-	},
1731-}
1732-
1733-func init() {
1734-	rootCmd.PersistentFlags().StringVar(&repoPath, "repo", ".", "path to git repository")
1735-	listCmd.Flags().StringVarP(&filterFlag, "filter", "f", "", "filter issues (label:value or age:<duration>)")
1736-	listCmd.Flags().StringVarP(&sortFlag, "sort", "s", "", "sort issues (field:direction, default: age:desc)")
1737-	listCmd.Flags().StringVarP(&statusFlag, "status", "S", "open", "filter by status (open|closed|all)")
1738-
1739-	newCmd.Flags().StringVarP(&newTitle, "title", "t", "", "issue title")
1740-	newCmd.Flags().StringVarP(&newMessage, "message", "m", "", "issue description/message")
1741-
1742-	// Agent new command flags - both required, no shorthand to force explicit usage
1743-	agentNewCmd.Flags().StringVar(&agentTitle, "title", "", "issue title (required)")
1744-	agentNewCmd.Flags().StringVar(&agentMessage, "message", "", "issue description (required)")
1745-	agentNewCmd.MarkFlagRequired("title")
1746-	agentNewCmd.MarkFlagRequired("message")
1747-
1748-	// Comment command flags - message is optional (opens editor if not provided)
1749-	commentCmd.Flags().StringVarP(&commentMessage, "message", "m", "", "comment message (opens editor if not provided)")
1750-
1751-	// Agent comment command flags - message is required (non-interactive)
1752-	agentCommentCmd.Flags().StringVarP(&agentCommentMsg, "message", "m", "", "comment message (required)")
1753-	agentCommentCmd.MarkFlagRequired("message")
1754-
1755-	// Edit command flags - both optional (opens editor if neither provided)
1756-	editCmd.Flags().StringVarP(&editTitle, "title", "t", "", "new issue title")
1757-	editCmd.Flags().StringVarP(&editMessage, "message", "m", "", "new issue description or comment text")
1758-
1759-	// Agent edit command flags - at least one required
1760-	agentEditCmd.Flags().StringVar(&agentEditTitle, "title", "", "new issue title")
1761-	agentEditCmd.Flags().StringVarP(&agentEditMsg, "message", "m", "", "new issue description or comment text")
1762-
1763-	rootCmd.AddCommand(listCmd)
1764-	rootCmd.AddCommand(initCmd)
1765-	rootCmd.AddCommand(newCmd)
1766-	rootCmd.AddCommand(commentCmd)
1767-	rootCmd.AddCommand(editCmd)
1768-	rootCmd.AddCommand(readCmd)
1769-	rootCmd.AddCommand(rmCmd)    // NEW: Add rm command
1770-	rootCmd.AddCommand(termCmd)  // NEW: Add term command
1771-	rootCmd.AddCommand(openCmd)  // NEW: Add open command
1772-	rootCmd.AddCommand(closeCmd) // NEW: Add close command
1773-
1774-	// Add agent command with its subcommands
1775-	agentCmd.AddCommand(agentNewCmd)
1776-	agentCmd.AddCommand(agentCommentCmd)
1777-	agentCmd.AddCommand(agentEditCmd)
1778-	agentCmd.AddCommand(agentReadCmd)
1779-	agentCmd.AddCommand(agentRmCmd)    // NEW: Add agent rm command
1780-	agentCmd.AddCommand(agentOpenCmd)  // NEW: Add agent open command
1781-	agentCmd.AddCommand(agentCloseCmd) // NEW: Add agent close command
1782-	rootCmd.AddCommand(agentCmd)
1783-}
1784-
1785-// runAgentNew creates a new bug/issue using the agent identity
1786-// Both title and message are required - this is non-interactive
1787-func runAgentNew(repoPath, title, message string) error {
1788-	// Validate inputs (both required for agent commands)
1789-	if strings.TrimSpace(title) == "" {
1790-		return fmt.Errorf("title cannot be empty")
1791-	}
1792-	if strings.TrimSpace(message) == "" {
1793-		return fmt.Errorf("message cannot be empty")
1794-	}
1795-
1796-	// Open repository
1797-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1798-	if err != nil {
1799-		return fmt.Errorf("failed to open repository: %w", err)
1800-	}
1801-	defer repo.Close()
1802-
1803-	// Get agent identity (not user identity)
1804-	author, err := getAgentIdentity(repo)
1805-	if err != nil {
1806-		return err
1807-	}
1808-
1809-	// Create the bug
1810-	unixTime := time.Now().Unix()
1811-	newBug, _, err := bug.Create(author, unixTime, title, message, nil, nil)
1812-	if err != nil {
1813-		return fmt.Errorf("failed to create bug: %w", err)
1814-	}
1815-
1816-	// Commit the bug to persist it
1817-	if err := newBug.Commit(repo); err != nil {
1818-		return fmt.Errorf("failed to commit bug: %w", err)
1819-	}
1820-
1821-	// Get the bug ID and display first 7 characters (like bug ls table)
1822-	bugID := newBug.Id().String()
1823-	if len(bugID) > 7 {
1824-		bugID = bugID[:7]
1825-	}
1826-
1827-	fmt.Printf("Created bug %s: %s\n", bugID, title)
1828-
1829-	// Invalidate cache so git-bug sees the new bug
1830-	invalidateGitBugCache(repoPath)
1831-
1832-	return nil
1833-}
1834-
1835-// runComment adds a comment to an existing bug
1836-// If message is empty, opens editor for interactive input
1837-func runComment(repoPath, bugIDStr, message string) error {
1838-	// If no message provided, open editor
1839-	if strings.TrimSpace(message) == "" {
1840-		content, err := launchCommentEditor("")
1841-		if err != nil {
1842-			return err
1843-		}
1844-		message, err = parseCommentContent(content)
1845-		if err != nil {
1846-			return err
1847-		}
1848-	}
1849-
1850-	// Validate message
1851-	if strings.TrimSpace(message) == "" {
1852-		return fmt.Errorf("comment cannot be empty")
1853-	}
1854-
1855-	// Resolve bug ID
1856-	b, err := resolveBugID(repoPath, bugIDStr)
1857-	if err != nil {
1858-		return err
1859-	}
1860-
1861-	// Open repository for identity lookup
1862-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1863-	if err != nil {
1864-		return fmt.Errorf("failed to open repository: %w", err)
1865-	}
1866-	defer repo.Close()
1867-
1868-	// Get user identity
1869-	author, err := identity.GetUserIdentity(repo)
1870-	if err != nil {
1871-		return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
1872-	}
1873-
1874-	// Add the comment
1875-	unixTime := time.Now().Unix()
1876-	commentID, _, err := bug.AddComment(b, author, unixTime, message, nil, nil)
1877-	if err != nil {
1878-		return fmt.Errorf("failed to add comment: %w", err)
1879-	}
1880-
1881-	// Commit the bug to persist the comment
1882-	if err = b.Commit(repo); err != nil {
1883-		return fmt.Errorf("failed to commit comment: %w", err)
1884-	}
1885-
1886-	// Get IDs for display (first 7 characters)
1887-	displayCommentID := commentID.String()
1888-	displayBugID := b.Id().String()
1889-	if len(displayCommentID) > 7 {
1890-		displayCommentID = displayCommentID[:7]
1891-	}
1892-	if len(displayBugID) > 7 {
1893-		displayBugID = displayBugID[:7]
1894-	}
1895-
1896-	// Display success message
1897-	fmt.Printf("Created comment %s for bug %s\n", displayCommentID, displayBugID)
1898-
1899-	// Invalidate cache so git-bug sees the new comment
1900-	invalidateGitBugCache(repoPath)
1901-
1902-	return nil
1903-}
1904-
1905-// runAgentComment adds a comment to an existing bug as the agent
1906-// Message is required - this is non-interactive
1907-func runAgentComment(repoPath, bugIDStr, message string) error {
1908-	// Validate message (required for agent commands)
1909-	if strings.TrimSpace(message) == "" {
1910-		return fmt.Errorf("message cannot be empty")
1911-	}
1912-
1913-	// Resolve bug ID
1914-	b, err := resolveBugID(repoPath, bugIDStr)
1915-	if err != nil {
1916-		return err
1917-	}
1918-
1919-	// Open repository for identity lookup
1920-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1921-	if err != nil {
1922-		return fmt.Errorf("failed to open repository: %w", err)
1923-	}
1924-	defer repo.Close()
1925-
1926-	// Get agent identity
1927-	author, err := getAgentIdentity(repo)
1928-	if err != nil {
1929-		return err
1930-	}
1931-
1932-	// Add the comment
1933-	unixTime := time.Now().Unix()
1934-	commentID, _, err := bug.AddComment(b, author, unixTime, message, nil, nil)
1935-	if err != nil {
1936-		return fmt.Errorf("failed to add comment: %w", err)
1937-	}
1938-
1939-	// Commit the bug to persist the comment
1940-	if err = b.Commit(repo); err != nil {
1941-		return fmt.Errorf("failed to commit comment: %w", err)
1942-	}
1943-
1944-	// Get IDs for display (first 7 characters)
1945-	displayCommentID := commentID.String()
1946-	displayBugID := b.Id().String()
1947-	if len(displayCommentID) > 7 {
1948-		displayCommentID = displayCommentID[:7]
1949-	}
1950-	if len(displayBugID) > 7 {
1951-		displayBugID = displayBugID[:7]
1952-	}
1953-
1954-	// Display success message
1955-	fmt.Printf("Created comment %s for bug %s\n", displayCommentID, displayBugID)
1956-
1957-	// Invalidate cache so git-bug sees the new comment
1958-	invalidateGitBugCache(repoPath)
1959-
1960-	return nil
1961-}
1962-
1963-// runEdit edits an issue or comment
1964-// If title and message flags are provided, uses them directly
1965-// Otherwise, opens an editor with current content pre-populated
1966-func runEdit(repoPath, idStr, newTitle, newMessage string) error {
1967-	// Resolve the ID (bug or comment)
1968-	resolved, err := resolveID(repoPath, idStr)
1969-	if err != nil {
1970-		return err
1971-	}
1972-
1973-	// Open repository for identity lookup
1974-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
1975-	if err != nil {
1976-		return fmt.Errorf("failed to open repository: %w", err)
1977-	}
1978-	defer repo.Close()
1979-
1980-	// Get user identity
1981-	author, err := identity.GetUserIdentity(repo)
1982-	if err != nil {
1983-		return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
1984-	}
1985-
1986-	unixTime := time.Now().Unix()
1987-
1988-	var editErr error
1989-	switch resolved.Type {
1990-	case IDTypeBug:
1991-		editErr = editBug(repo, resolved.Bug, author, unixTime, newTitle, newMessage)
1992-	case IDTypeComment:
1993-		editErr = editComment(repo, resolved.Bug, resolved.Comment, author, unixTime, newMessage)
1994-	default:
1995-		return fmt.Errorf("unknown ID type")
1996-	}
1997-
1998-	if editErr == nil {
1999-		// Invalidate cache so git-bug sees the changes
2000-		invalidateGitBugCache(repoPath)
2001-	}
2002-
2003-	return editErr
2004-}
2005-
2006-// editBug edits an issue's title and/or description
2007-func editBug(repo repository.ClockedRepo, b *bug.Bug, author identity.Interface, unixTime int64, newTitle, newMessage string) error {
2008-	snap := b.Compile()
2009-	currentTitle := snap.Title
2010-	currentMessage := ""
2011-	if len(snap.Comments) > 0 {
2012-		currentMessage = snap.Comments[0].Message
2013-	}
2014-
2015-	// If no flags provided, open editor with current content
2016-	if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
2017-		// Prepare template: title on first line, blank line, description
2018-		template := fmt.Sprintf("%s\n\n%s", currentTitle, currentMessage)
2019-
2020-		// Add edit instructions
2021-		template = `;; Edit the title on the first line (required)
2022-;; Edit the description below the blank line
2023-;; Lines starting with ;; will be ignored
2024-;; Save and close the editor when done
2025-;;
2026-` + template
2027-
2028-		content, err := launchEditorWithTemplate(template)
2029-		if err != nil {
2030-			return err
2031-		}
2032-		newTitle, newMessage, err = parseEditContent(content, false)
2033-		if err != nil {
2034-			return err
2035-		}
2036-	} else {
2037-		// Flags provided - use current values for fields not specified
2038-		if strings.TrimSpace(newTitle) == "" {
2039-			newTitle = currentTitle
2040-		}
2041-		if strings.TrimSpace(newMessage) == "" {
2042-			newMessage = currentMessage
2043-		}
2044-	}
2045-
2046-	// Validate title
2047-	if strings.TrimSpace(newTitle) == "" {
2048-		return fmt.Errorf("title cannot be empty")
2049-	}
2050-
2051-	// Apply changes
2052-	changes := false
2053-
2054-	// Update title if changed
2055-	if newTitle != currentTitle {
2056-		_, err := bug.SetTitle(b, author, unixTime, newTitle, nil)
2057-		if err != nil {
2058-			return fmt.Errorf("failed to update title: %w", err)
2059-		}
2060-		changes = true
2061-	}
2062-
2063-	// Update description (first comment) if changed
2064-	if newMessage != currentMessage {
2065-		_, _, err := bug.EditCreateComment(b, author, unixTime, newMessage, nil, nil)
2066-		if err != nil {
2067-			return fmt.Errorf("failed to update description: %w", err)
2068-		}
2069-		changes = true
2070-	}
2071-
2072-	// Commit changes if any
2073-	if changes {
2074-		if err := b.Commit(repo); err != nil {
2075-			return fmt.Errorf("failed to commit changes: %w", err)
2076-		}
2077-	}
2078-
2079-	// Get short ID for display
2080-	bugID := b.Id().String()
2081-	if len(bugID) > 7 {
2082-		bugID = bugID[:7]
2083-	}
2084-
2085-	fmt.Printf("Updated bug %s: %s\n", bugID, newTitle)
2086-	return nil
2087-}
2088-
2089-// editComment edits an existing comment
2090-func editComment(repo repository.ClockedRepo, b *bug.Bug, comment *bug.Comment, author identity.Interface, unixTime int64, newMessage string) error {
2091-	currentMessage := comment.Message
2092-
2093-	// If no message provided, open editor with current content
2094-	if strings.TrimSpace(newMessage) == "" {
2095-		// Prepare template: current comment content
2096-		template := currentMessage
2097-
2098-		// Add edit instructions
2099-		template = `;; Edit your comment below
2100-;; Lines starting with ;; will be ignored
2101-;; You can use markdown formatting
2102-;; Save and close the editor when done
2103-;;
2104-` + template
2105-
2106-		content, err := launchEditorWithTemplate(template)
2107-		if err != nil {
2108-			return err
2109-		}
2110-		_, newMessage, err = parseEditContent(content, true)
2111-		if err != nil {
2112-			return err
2113-		}
2114-	}
2115-
2116-	// Validate message
2117-	if strings.TrimSpace(newMessage) == "" {
2118-		return fmt.Errorf("comment cannot be empty")
2119-	}
2120-
2121-	// Only update if changed
2122-	if newMessage != currentMessage {
2123-		// Get the target comment ID (the operation ID for this comment)
2124-		targetID := comment.TargetId()
2125-
2126-		_, _, err := bug.EditComment(b, author, unixTime, targetID, newMessage, nil, nil)
2127-		if err != nil {
2128-			return fmt.Errorf("failed to update comment: %w", err)
2129-		}
2130-
2131-		// Commit the changes
2132-		if err := b.Commit(repo); err != nil {
2133-			return fmt.Errorf("failed to commit changes: %w", err)
2134-		}
2135-	}
2136-
2137-	// Get IDs for display
2138-	bugID := b.Id().String()
2139-	commentID := comment.CombinedId().String()
2140-	if len(bugID) > 7 {
2141-		bugID = bugID[:7]
2142-	}
2143-	if len(commentID) > 7 {
2144-		commentID = commentID[:7]
2145-	}
2146-
2147-	fmt.Printf("Updated comment %s in bug %s\n", commentID, bugID)
2148-	return nil
2149-}
2150-
2151-// runAgentEdit edits an issue or comment as the agent identity
2152-// This is non-interactive and requires at least one of --title or --message
2153-func runAgentEdit(repoPath, idStr, newTitle, newMessage string) error {
2154-	// Validate that at least one field is being updated
2155-	if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
2156-		return fmt.Errorf("either --title or --message must be provided")
2157-	}
2158-
2159-	// Resolve the ID (bug or comment)
2160-	resolved, err := resolveID(repoPath, idStr)
2161-	if err != nil {
2162-		return err
2163-	}
2164-
2165-	// Open repository for identity lookup
2166-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2167-	if err != nil {
2168-		return fmt.Errorf("failed to open repository: %w", err)
2169-	}
2170-	defer repo.Close()
2171-
2172-	// Get agent identity
2173-	author, err := getAgentIdentity(repo)
2174-	if err != nil {
2175-		return err
2176-	}
2177-
2178-	unixTime := time.Now().Unix()
2179-
2180-	var editErr error
2181-	switch resolved.Type {
2182-	case IDTypeBug:
2183-		editErr = editBugAgent(repo, resolved.Bug, author, unixTime, newTitle, newMessage)
2184-	case IDTypeComment:
2185-		// For comments, only message is applicable
2186-		if strings.TrimSpace(newMessage) == "" {
2187-			return fmt.Errorf("--message is required when editing a comment")
2188-		}
2189-		editErr = editCommentAgent(repo, resolved.Bug, resolved.Comment, author, unixTime, newMessage)
2190-	default:
2191-		return fmt.Errorf("unknown ID type")
2192-	}
2193-
2194-	if editErr == nil {
2195-		// Invalidate cache so git-bug sees the changes
2196-		invalidateGitBugCache(repoPath)
2197-	}
2198-
2199-	return editErr
2200-}
2201-
2202-// editBugAgent edits an issue as the agent (non-interactive)
2203-func editBugAgent(repo repository.ClockedRepo, b *bug.Bug, author identity.Interface, unixTime int64, newTitle, newMessage string) error {
2204-	snap := b.Compile()
2205-	currentTitle := snap.Title
2206-	currentMessage := ""
2207-	if len(snap.Comments) > 0 {
2208-		currentMessage = snap.Comments[0].Message
2209-	}
2210-
2211-	// For bugs, require at least one field to be set
2212-	if strings.TrimSpace(newTitle) == "" && strings.TrimSpace(newMessage) == "" {
2213-		return fmt.Errorf("either --title or --message must be provided")
2214-	}
2215-
2216-	// Use current values for fields not provided
2217-	if strings.TrimSpace(newTitle) == "" {
2218-		newTitle = currentTitle
2219-	}
2220-	if strings.TrimSpace(newMessage) == "" {
2221-		newMessage = currentMessage
2222-	}
2223-
2224-	changes := false
2225-
2226-	// Update title if changed
2227-	if newTitle != currentTitle {
2228-		_, err := bug.SetTitle(b, author, unixTime, newTitle, nil)
2229-		if err != nil {
2230-			return fmt.Errorf("failed to update title: %w", err)
2231-		}
2232-		changes = true
2233-	}
2234-
2235-	// Update description (first comment) if changed
2236-	if newMessage != currentMessage {
2237-		_, _, err := bug.EditCreateComment(b, author, unixTime, newMessage, nil, nil)
2238-		if err != nil {
2239-			return fmt.Errorf("failed to update description: %w", err)
2240-		}
2241-		changes = true
2242-	}
2243-
2244-	// Commit changes if any
2245-	if changes {
2246-		if err := b.Commit(repo); err != nil {
2247-			return fmt.Errorf("failed to commit changes: %w", err)
2248-		}
2249-	}
2250-
2251-	// Get short ID for display
2252-	bugID := b.Id().String()
2253-	if len(bugID) > 7 {
2254-		bugID = bugID[:7]
2255-	}
2256-
2257-	fmt.Printf("Updated bug %s: %s\n", bugID, newTitle)
2258-	return nil
2259-}
2260-
2261-// editCommentAgent edits a comment as the agent (non-interactive)
2262-func editCommentAgent(repo repository.ClockedRepo, b *bug.Bug, comment *bug.Comment, author identity.Interface, unixTime int64, newMessage string) error {
2263-	currentMessage := comment.Message
2264-
2265-	// Validate message
2266-	if strings.TrimSpace(newMessage) == "" {
2267-		return fmt.Errorf("message cannot be empty")
2268-	}
2269-
2270-	// Only update if changed
2271-	if newMessage != currentMessage {
2272-		// Get the target comment ID (the operation ID for this comment)
2273-		targetID := comment.TargetId()
2274-
2275-		_, _, err := bug.EditComment(b, author, unixTime, targetID, newMessage, nil, nil)
2276-		if err != nil {
2277-			return fmt.Errorf("failed to update comment: %w", err)
2278-		}
2279-
2280-		// Commit the changes
2281-		if err := b.Commit(repo); err != nil {
2282-			return fmt.Errorf("failed to commit changes: %w", err)
2283-		}
2284-	}
2285-
2286-	// Get IDs for display
2287-	bugID := b.Id().String()
2288-	commentID := comment.CombinedId().String()
2289-	if len(bugID) > 7 {
2290-		bugID = bugID[:7]
2291-	}
2292-	if len(commentID) > 7 {
2293-		commentID = commentID[:7]
2294-	}
2295-
2296-	fmt.Printf("Updated comment %s in bug %s\n", commentID, bugID)
2297-	return nil
2298-}
2299-
2300-// runOpen opens a bug by changing its status to Open
2301-func runOpen(repoPath, bugIDStr string) error {
2302-	// Resolve bug ID
2303-	b, err := resolveBugID(repoPath, bugIDStr)
2304-	if err != nil {
2305-		return err
2306-	}
2307-
2308-	// Open repository for identity lookup
2309-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2310-	if err != nil {
2311-		return fmt.Errorf("failed to open repository: %w", err)
2312-	}
2313-	defer repo.Close()
2314-
2315-	// Get user identity
2316-	author, err := identity.GetUserIdentity(repo)
2317-	if err != nil {
2318-		return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
2319-	}
2320-
2321-	// Open the bug
2322-	unixTime := time.Now().Unix()
2323-	_, err = bug.Open(b, author, unixTime, nil)
2324-	if err != nil {
2325-		return fmt.Errorf("failed to open bug: %w", err)
2326-	}
2327-
2328-	// Commit the changes
2329-	if err := b.Commit(repo); err != nil {
2330-		return fmt.Errorf("failed to commit changes: %w", err)
2331-	}
2332-
2333-	// Get short ID for display
2334-	bugID := b.Id().String()
2335-	if len(bugID) > 7 {
2336-		bugID = bugID[:7]
2337-	}
2338-
2339-	fmt.Printf("Opened bug %s: %s\n", bugID, b.Compile().Title)
2340-
2341-	// Invalidate cache so git-bug sees the changes
2342-	invalidateGitBugCache(repoPath)
2343-
2344-	return nil
2345-}
2346-
2347-// runClose closes a bug by changing its status to Closed
2348-func runClose(repoPath, bugIDStr string) error {
2349-	// Resolve bug ID
2350-	b, err := resolveBugID(repoPath, bugIDStr)
2351-	if err != nil {
2352-		return err
2353-	}
2354-
2355-	// Open repository for identity lookup
2356-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2357-	if err != nil {
2358-		return fmt.Errorf("failed to open repository: %w", err)
2359-	}
2360-	defer repo.Close()
2361-
2362-	// Get user identity
2363-	author, err := identity.GetUserIdentity(repo)
2364-	if err != nil {
2365-		return fmt.Errorf("failed to get user identity: %w\n\nRun 'bug init' first to set up identities", err)
2366-	}
2367-
2368-	// Close the bug
2369-	unixTime := time.Now().Unix()
2370-	_, err = bug.Close(b, author, unixTime, nil)
2371-	if err != nil {
2372-		return fmt.Errorf("failed to close bug: %w", err)
2373-	}
2374-
2375-	// Commit the changes
2376-	if err := b.Commit(repo); err != nil {
2377-		return fmt.Errorf("failed to commit changes: %w", err)
2378-	}
2379-
2380-	// Get short ID for display
2381-	bugID := b.Id().String()
2382-	if len(bugID) > 7 {
2383-		bugID = bugID[:7]
2384-	}
2385-
2386-	fmt.Printf("Closed bug %s: %s\n", bugID, b.Compile().Title)
2387-
2388-	// Invalidate cache so git-bug sees the changes
2389-	invalidateGitBugCache(repoPath)
2390-
2391-	return nil
2392-}
2393-
2394-// runAgentOpen opens a bug using the agent identity
2395-func runAgentOpen(repoPath, bugIDStr string) error {
2396-	// Resolve bug ID
2397-	b, err := resolveBugID(repoPath, bugIDStr)
2398-	if err != nil {
2399-		return err
2400-	}
2401-
2402-	// Open repository for identity lookup
2403-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2404-	if err != nil {
2405-		return fmt.Errorf("failed to open repository: %w", err)
2406-	}
2407-	defer repo.Close()
2408-
2409-	// Get agent identity
2410-	author, err := getAgentIdentity(repo)
2411-	if err != nil {
2412-		return err
2413-	}
2414-
2415-	// Open the bug
2416-	unixTime := time.Now().Unix()
2417-	_, err = bug.Open(b, author, unixTime, nil)
2418-	if err != nil {
2419-		return fmt.Errorf("failed to open bug: %w", err)
2420-	}
2421-
2422-	// Commit the changes
2423-	if err := b.Commit(repo); err != nil {
2424-		return fmt.Errorf("failed to commit changes: %w", err)
2425-	}
2426-
2427-	// Get short ID for display
2428-	bugID := b.Id().String()
2429-	if len(bugID) > 7 {
2430-		bugID = bugID[:7]
2431-	}
2432-
2433-	fmt.Printf("Opened bug %s: %s\n", bugID, b.Compile().Title)
2434-
2435-	// Invalidate cache so git-bug sees the changes
2436-	invalidateGitBugCache(repoPath)
2437-
2438-	return nil
2439-}
2440-
2441-// runAgentClose closes a bug using the agent identity
2442-func runAgentClose(repoPath, bugIDStr string) error {
2443-	// Resolve bug ID
2444-	b, err := resolveBugID(repoPath, bugIDStr)
2445-	if err != nil {
2446-		return err
2447-	}
2448-
2449-	// Open repository for identity lookup
2450-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2451-	if err != nil {
2452-		return fmt.Errorf("failed to open repository: %w", err)
2453-	}
2454-	defer repo.Close()
2455-
2456-	// Get agent identity
2457-	author, err := getAgentIdentity(repo)
2458-	if err != nil {
2459-		return err
2460-	}
2461-
2462-	// Close the bug
2463-	unixTime := time.Now().Unix()
2464-	_, err = bug.Close(b, author, unixTime, nil)
2465-	if err != nil {
2466-		return fmt.Errorf("failed to close bug: %w", err)
2467-	}
2468-
2469-	// Commit the changes
2470-	if err := b.Commit(repo); err != nil {
2471-		return fmt.Errorf("failed to commit changes: %w", err)
2472-	}
2473-
2474-	// Get short ID for display
2475-	bugID := b.Id().String()
2476-	if len(bugID) > 7 {
2477-		bugID = bugID[:7]
2478-	}
2479-
2480-	fmt.Printf("Closed bug %s: %s\n", bugID, b.Compile().Title)
2481-
2482-	// Invalidate cache so git-bug sees the changes
2483-	invalidateGitBugCache(repoPath)
2484-
2485-	return nil
2486-}
2487-
2488-// runRemove removes a bug by its ID
2489-// Resolves ID as bug first, then as comment (though comments cannot be removed individually)
2490-func runRemove(repoPath, idStr string) error {
2491-	// Resolve the ID (bug or comment)
2492-	resolved, err := resolveID(repoPath, idStr)
2493-	if err != nil {
2494-		return err
2495-	}
2496-
2497-	// Open repository
2498-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
2499-	if err != nil {
2500-		return fmt.Errorf("failed to open repository: %w", err)
2501-	}
2502-	defer repo.Close()
2503-
2504-	switch resolved.Type {
2505-	case IDTypeBug:
2506-		// Remove the bug
2507-		if err := bug.Remove(repo, resolved.Bug.Id()); err != nil {
2508-			return fmt.Errorf("failed to remove bug: %w", err)
2509-		}
2510-
2511-		// Get short ID for display
2512-		bugID := resolved.Bug.Id().String()
2513-		if len(bugID) > 7 {
2514-			bugID = bugID[:7]
2515-		}
2516-
2517-		fmt.Printf("Removed bug %s: %s\n", bugID, resolved.Bug.Compile().Title)
2518-
2519-	case IDTypeComment:
2520-		// Comments cannot be removed individually in git-bug
2521-		return fmt.Errorf("removing individual comments is not supported; use 'bug rm <bug-id>' to remove the entire issue")
2522-	default:
2523-		return fmt.Errorf("unknown ID type")
2524-	}
2525-
2526-	// Invalidate cache so git-bug sees the changes
2527-	invalidateGitBugCache(repoPath)
2528-
2529-	return nil
2530-}
2531-
2532-func main() {
2533-	// Get absolute path for repo
2534-	if repoPath == "." {
2535-		if cwd, err := os.Getwd(); err == nil {
2536-			repoPath = cwd
2537-		}
2538-	}
2539-	repoPath, _ = filepath.Abs(repoPath)
2540-
2541-	if err := rootCmd.Execute(); err != nil {
2542-		fmt.Fprintln(os.Stderr, err)
2543-		os.Exit(1)
2544-	}
2545-}
D cmd/bug/main_test.go
+0, -453
  1@@ -1,453 +0,0 @@
  2-package main
  3-
  4-import (
  5-	"bufio"
  6-	"os"
  7-	"os/exec"
  8-	"path/filepath"
  9-	"runtime"
 10-	"strings"
 11-	"testing"
 12-
 13-	"github.com/git-bug/git-bug/entities/identity"
 14-	"github.com/git-bug/git-bug/repository"
 15-)
 16-
 17-func TestPromptUser(t *testing.T) {
 18-	tests := []struct {
 19-		name        string
 20-		input       string
 21-		prompt      string
 22-		expected    string
 23-		expectError bool
 24-	}{
 25-		{
 26-			name:     "simple input",
 27-			input:    "Test User\n",
 28-			prompt:   "Enter name: ",
 29-			expected: "Test User",
 30-		},
 31-		{
 32-			name:     "input with spaces",
 33-			input:    "John Doe\n",
 34-			prompt:   "Enter name: ",
 35-			expected: "John Doe",
 36-		},
 37-		{
 38-			name:     "empty input",
 39-			input:    "\n",
 40-			prompt:   "Enter name: ",
 41-			expected: "",
 42-		},
 43-		{
 44-			name:     "input with trailing newline",
 45-			input:    "[email protected]\n",
 46-			prompt:   "Enter email: ",
 47-			expected: "[email protected]",
 48-		},
 49-	}
 50-
 51-	for _, tt := range tests {
 52-		t.Run(tt.name, func(t *testing.T) {
 53-			// Create a scanner from the test input string
 54-			scanner := bufio.NewScanner(strings.NewReader(tt.input))
 55-
 56-			// Call promptUserWithScanner with our test scanner
 57-			result, err := promptUserWithScanner(tt.prompt, scanner)
 58-
 59-			if tt.expectError {
 60-				if err == nil {
 61-					t.Errorf("expected error, got nil")
 62-				}
 63-				return
 64-			}
 65-
 66-			if err != nil {
 67-				t.Errorf("unexpected error: %v", err)
 68-				return
 69-			}
 70-
 71-			if result != tt.expected {
 72-				t.Errorf("promptUser() = %q, want %q", result, tt.expected)
 73-			}
 74-		})
 75-	}
 76-}
 77-
 78-func TestDetectJJConfig(t *testing.T) {
 79-	// Test case 1: No .jj directory
 80-	t.Run("no_jj_directory", func(t *testing.T) {
 81-		tmpDir := t.TempDir()
 82-		name, email, err := detectJJConfig(tmpDir)
 83-		if err != nil {
 84-			t.Fatalf("unexpected error: %v", err)
 85-		}
 86-		if name != "" || email != "" {
 87-			t.Errorf("expected empty name and email, got name=%q, email=%q", name, email)
 88-		}
 89-	})
 90-
 91-	// Test case 2: .jj directory exists (would need actual jj config in practice)
 92-	t.Run("jj_directory_exists", func(t *testing.T) {
 93-		// This test documents the expected behavior when .jj exists
 94-		// In a real scenario, it would try to run jj commands
 95-		tmpDir := t.TempDir()
 96-		jjDir := filepath.Join(tmpDir, ".jj")
 97-		if err := os.MkdirAll(jjDir, 0755); err != nil {
 98-			t.Fatalf("failed to create .jj directory: %v", err)
 99-		}
100-		// We expect the function to attempt jj config commands
101-		// Since jj is not available in test env, we just verify the directory detection
102-		_, _, err := detectJJConfig(tmpDir)
103-		// We expect an error since jj binary won't be available
104-		if err == nil {
105-			t.Skip("jj not available in test environment, skipping")
106-		}
107-	})
108-}
109-
110-func TestCreateIdentity(t *testing.T) {
111-	// Create a temporary directory for a test git repo
112-	tmpDir := t.TempDir()
113-
114-	// Initialize a git repo
115-	initCmd := exec.Command("git", "init")
116-	initCmd.Dir = tmpDir
117-	if err := initCmd.Run(); err != nil {
118-		t.Fatalf("failed to init git repo: %v", err)
119-	}
120-
121-	// Test creating an identity
122-	err := createIdentity(tmpDir, "Test User", "[email protected]", true)
123-	if err != nil {
124-		t.Fatalf("createIdentity failed: %v", err)
125-	}
126-
127-	// Verify identity was created by checking if we can get it
128-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
129-	if err != nil {
130-		t.Fatalf("failed to open repo: %v", err)
131-	}
132-	defer repo.Close()
133-
134-	isSet, err := identity.IsUserIdentitySet(repo)
135-	if err != nil {
136-		t.Fatalf("IsUserIdentitySet failed: %v", err)
137-	}
138-	if !isSet {
139-		t.Error("expected user identity to be set")
140-	}
141-
142-	userIdentity, err := identity.GetUserIdentity(repo)
143-	if err != nil {
144-		t.Fatalf("GetUserIdentity failed: %v", err)
145-	}
146-
147-	if userIdentity.Name() != "Test User" {
148-		t.Errorf("expected name 'Test User', got %q", userIdentity.Name())
149-	}
150-
151-	if userIdentity.Email() != "[email protected]" {
152-		t.Errorf("expected email '[email protected]', got %q", userIdentity.Email())
153-	}
154-}
155-
156-func TestInitCommand_InteractiveMode(t *testing.T) {
157-	// Create a temporary directory for a test git repo
158-	tmpDir := t.TempDir()
159-
160-	// Initialize a git repo
161-	initCmd := exec.Command("git", "init")
162-	initCmd.Dir = tmpDir
163-	if err := initCmd.Run(); err != nil {
164-		t.Fatalf("failed to init git repo: %v", err)
165-	}
166-
167-	// Add origin remote (required for bug init)
168-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
169-	remoteCmd.Dir = tmpDir
170-	if err := remoteCmd.Run(); err != nil {
171-		t.Fatalf("failed to add origin remote: %v", err)
172-	}
173-
174-	// Test the init function with a mock input (simulating interactive mode)
175-	// Since we don't have jj, we'll test the interactive path by mocking stdin
176-	input := "Interactive User\[email protected]\n"
177-	r, w, _ := os.Pipe()
178-	go func() {
179-		w.WriteString(input)
180-		w.Close()
181-	}()
182-
183-	// We need to test runInitWithReader to inject our stdin
184-	err := runInitWithReader(tmpDir, r)
185-	if err != nil {
186-		t.Fatalf("runInitWithReader failed: %v", err)
187-	}
188-
189-	// Verify repo was opened
190-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
191-	if err != nil {
192-		t.Fatalf("failed to open repo: %v", err)
193-	}
194-	defer repo.Close()
195-
196-	// Check if identities were created
197-	ids, err := identity.ListLocalIds(repo)
198-	if err != nil {
199-		t.Fatalf("ListLocalIds failed: %v", err)
200-	}
201-
202-	// Should have at least 2 identities (user + agent)
203-	if len(ids) < 2 {
204-		t.Errorf("expected at least 2 identities (user + agent), got %d", len(ids))
205-	}
206-
207-	// Verify the user identity has the expected name and email
208-	userIdentity, err := identity.GetUserIdentity(repo)
209-	if err != nil {
210-		t.Fatalf("GetUserIdentity failed: %v", err)
211-	}
212-
213-	if userIdentity.Name() != "Interactive User" {
214-		t.Errorf("expected user name 'Interactive User', got %q", userIdentity.Name())
215-	}
216-
217-	if userIdentity.Email() != "[email protected]" {
218-		t.Errorf("expected user email '[email protected]', got %q", userIdentity.Email())
219-	}
220-
221-	// Verify the agent identity exists with empty email
222-	// The agent identity should be one of the identities with name "agent" and empty email
223-	foundAgent := false
224-	for _, id := range ids {
225-		i, err := identity.ReadLocal(repo, id)
226-		if err != nil {
227-			t.Fatalf("failed to read identity %s: %v", id, err)
228-		}
229-		if i.Name() == "agent" && i.Email() == "" {
230-			foundAgent = true
231-			break
232-		}
233-	}
234-	if !foundAgent {
235-		t.Error("expected to find an agent identity with empty email")
236-	}
237-}
238-
239-func TestParseEditorContent(t *testing.T) {
240-	tests := []struct {
241-		name            string
242-		content         string
243-		expectedTitle   string
244-		expectedMessage string
245-		expectError     bool
246-	}{
247-		{
248-			name:            "simple title only",
249-			content:         "Bug title\n",
250-			expectedTitle:   "Bug title",
251-			expectedMessage: "",
252-		},
253-		{
254-			name:            "title with description",
255-			content:         "Bug title\n\nThis is a description\nwith multiple lines\n",
256-			expectedTitle:   "Bug title",
257-			expectedMessage: "\nThis is a description\nwith multiple lines",
258-		},
259-		{
260-			name:            "with comment lines",
261-			content:         ";; This is a comment\nBug title\n\nDescription here\n;; Another comment",
262-			expectedTitle:   "Bug title",
263-			expectedMessage: "Description here",
264-		},
265-		{
266-			name:        "empty content",
267-			content:     "",
268-			expectError: true,
269-		},
270-		{
271-			name:        "only comments",
272-			content:     ";; Comment 1\n;; Comment 2\n",
273-			expectError: true,
274-		},
275-		{
276-			name:            "leading empty lines",
277-			content:         "\n\n  \nActual title\nDescription",
278-			expectedTitle:   "Actual title",
279-			expectedMessage: "Description",
280-		},
281-		{
282-			name:            "markdown headers preserved",
283-			content:         "Bug title\n\n# Header 1\n## Header 2\nContent here",
284-			expectedTitle:   "Bug title",
285-			expectedMessage: "\n# Header 1\n## Header 2\nContent here",
286-		},
287-	}
288-
289-	for _, tt := range tests {
290-		t.Run(tt.name, func(t *testing.T) {
291-			title, message, err := parseEditorContent(tt.content)
292-
293-			if tt.expectError {
294-				if err == nil {
295-					t.Errorf("expected error, got nil")
296-				}
297-				return
298-			}
299-
300-			if err != nil {
301-				t.Errorf("unexpected error: %v", err)
302-				return
303-			}
304-
305-			if title != tt.expectedTitle {
306-				t.Errorf("title = %q, want %q", title, tt.expectedTitle)
307-			}
308-
309-			if message != tt.expectedMessage {
310-				t.Errorf("message = %q, want %q", message, tt.expectedMessage)
311-			}
312-		})
313-	}
314-}
315-
316-func TestParseCommentContent(t *testing.T) {
317-	tests := []struct {
318-		name            string
319-		content         string
320-		expectedMessage string
321-		expectError     bool
322-	}{
323-		{
324-			name:            "simple comment",
325-			content:         "This is a comment\n",
326-			expectedMessage: "This is a comment",
327-		},
328-		{
329-			name:            "multiline comment",
330-			content:         "First line\nSecond line\nThird line\n",
331-			expectedMessage: "First line\nSecond line\nThird line",
332-		},
333-		{
334-			name:            "with comment lines",
335-			content:         ";; This is a comment\nActual content\n;; Another comment",
336-			expectedMessage: "Actual content",
337-		},
338-		{
339-			name:        "only comment lines",
340-			content:     ";; Comment 1\n;; Comment 2\n",
341-			expectError: true,
342-		},
343-		{
344-			name:        "empty content",
345-			content:     "",
346-			expectError: true,
347-		},
348-		{
349-			name:        "only whitespace",
350-			content:     "   \n\n   \n",
351-			expectError: true,
352-		},
353-		{
354-			name:            "leading empty lines",
355-			content:         "\n\n  \nActual content\nMore content",
356-			expectedMessage: "Actual content\nMore content",
357-		},
358-		{
359-			name:            "trailing empty lines",
360-			content:         "Actual content\nMore content\n\n  \n",
361-			expectedMessage: "Actual content\nMore content",
362-		},
363-		{
364-			name:            "markdown content",
365-			content:         "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
366-			expectedMessage: "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
367-		},
368-		{
369-			name:            "mixed comments and content",
370-			content:         ";; Instructions here\n;; More instructions\n\nActual comment\n;; Hidden comment\nMore content\n;; End comment",
371-			expectedMessage: "Actual comment\nMore content",
372-		},
373-	}
374-
375-	for _, tt := range tests {
376-		t.Run(tt.name, func(t *testing.T) {
377-			message, err := parseCommentContent(tt.content)
378-
379-			if tt.expectError {
380-				if err == nil {
381-					t.Errorf("expected error, got nil")
382-				}
383-				return
384-			}
385-
386-			if err != nil {
387-				t.Errorf("unexpected error: %v", err)
388-				return
389-			}
390-
391-			if message != tt.expectedMessage {
392-				t.Errorf("message = %q, want %q", message, tt.expectedMessage)
393-			}
394-		})
395-	}
396-}
397-
398-func TestGetEditor(t *testing.T) {
399-	// Save original EDITOR value
400-	originalEditor := os.Getenv("EDITOR")
401-	defer os.Setenv("EDITOR", originalEditor)
402-
403-	tests := []struct {
404-		name      string
405-		editorEnv string
406-		check     func(t *testing.T, got string)
407-	}{
408-		{
409-			name:      "with EDITOR set",
410-			editorEnv: "vim",
411-			check: func(t *testing.T, got string) {
412-				if !strings.Contains(got, "vim") {
413-					t.Errorf("getEditor() = %q, want to contain 'vim'", got)
414-				}
415-			},
416-		},
417-		{
418-			name:      "with EDITOR set to path",
419-			editorEnv: "/usr/bin/nano",
420-			check: func(t *testing.T, got string) {
421-				if !strings.Contains(got, "nano") {
422-					t.Errorf("getEditor() = %q, want to contain 'nano'", got)
423-				}
424-			},
425-		},
426-		{
427-			name:      "with EDITOR empty uses default",
428-			editorEnv: "",
429-			check: func(t *testing.T, got string) {
430-				if got == "" {
431-					t.Error("expected default editor, got empty string")
432-				}
433-				// Check it returns expected default based on platform
434-				if runtime.GOOS == "windows" {
435-					if got != "notepad" {
436-						t.Errorf("expected 'notepad' on Windows, got %q", got)
437-					}
438-				} else {
439-					if got != "vi" {
440-						t.Errorf("expected 'vi' on Unix, got %q", got)
441-					}
442-				}
443-			},
444-		},
445-	}
446-
447-	for _, tt := range tests {
448-		t.Run(tt.name, func(t *testing.T) {
449-			os.Setenv("EDITOR", tt.editorEnv)
450-			got := getEditor()
451-			tt.check(t, got)
452-		})
453-	}
454-}
D cmd/bug/new_integration_test.go
+0, -169
  1@@ -1,169 +0,0 @@
  2-//go:build integration
  3-
  4-package main
  5-
  6-import (
  7-	"os/exec"
  8-	"strings"
  9-	"testing"
 10-
 11-	"github.com/git-bug/git-bug/entities/bug"
 12-	"github.com/git-bug/git-bug/repository"
 13-)
 14-
 15-// TestNewCommand_WithFlags creates a bug using command-line flags
 16-func TestNewCommand_WithFlags(t *testing.T) {
 17-	tmpDir := t.TempDir()
 18-
 19-	// Initialize a git repo
 20-	initCmd := exec.Command("git", "init")
 21-	initCmd.Dir = tmpDir
 22-	if err := initCmd.Run(); err != nil {
 23-		t.Fatalf("failed to init git repo: %v", err)
 24-	}
 25-
 26-	// Initialize git-bug identities
 27-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 28-		t.Fatalf("failed to create identity: %v", err)
 29-	}
 30-
 31-	// Create a bug using flags
 32-	title := "Test Bug Title"
 33-	message := "Test bug description"
 34-	if err := runNew(tmpDir, title, message); err != nil {
 35-		t.Fatalf("runNew failed: %v", err)
 36-	}
 37-
 38-	// Verify bug was created
 39-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 40-	if err != nil {
 41-		t.Fatalf("failed to open repo: %v", err)
 42-	}
 43-	defer repo.Close()
 44-
 45-	// Count bugs
 46-	bugCount := 0
 47-	for streamedBug := range bug.ReadAll(repo) {
 48-		if streamedBug.Err != nil {
 49-			t.Fatalf("failed to read bug: %v", streamedBug.Err)
 50-		}
 51-		b := streamedBug.Entity
 52-		snap := b.Compile()
 53-
 54-		if snap.Title == title {
 55-			bugCount++
 56-			if snap.Comments[0].Message != message {
 57-				t.Errorf("message = %q, want %q", snap.Comments[0].Message, message)
 58-			}
 59-			if snap.Author.Name() != "Test User" {
 60-				t.Errorf("author = %q, want %q", snap.Author.Name(), "Test User")
 61-			}
 62-		}
 63-	}
 64-
 65-	if bugCount != 1 {
 66-		t.Errorf("expected 1 bug with title %q, found %d", title, bugCount)
 67-	}
 68-}
 69-
 70-// TestNewCommand_TitleOnly creates a bug with only title
 71-func TestNewCommand_TitleOnly(t *testing.T) {
 72-	tmpDir := t.TempDir()
 73-
 74-	// Initialize a git repo
 75-	initCmd := exec.Command("git", "init")
 76-	initCmd.Dir = tmpDir
 77-	if err := initCmd.Run(); err != nil {
 78-		t.Fatalf("failed to init git repo: %v", err)
 79-	}
 80-
 81-	// Initialize git-bug identities
 82-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
 83-		t.Fatalf("failed to create identity: %v", err)
 84-	}
 85-
 86-	// Create a bug with title only
 87-	title := "Title Only Bug"
 88-	if err := runNew(tmpDir, title, ""); err != nil {
 89-		t.Fatalf("runNew failed: %v", err)
 90-	}
 91-
 92-	// Verify bug was created
 93-	repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
 94-	if err != nil {
 95-		t.Fatalf("failed to open repo: %v", err)
 96-	}
 97-	defer repo.Close()
 98-
 99-	// Find the bug
100-	found := false
101-	for streamedBug := range bug.ReadAll(repo) {
102-		if streamedBug.Err != nil {
103-			continue
104-		}
105-		b := streamedBug.Entity
106-		snap := b.Compile()
107-
108-		if snap.Title == title {
109-			found = true
110-			if snap.Comments[0].Message != "" {
111-				t.Errorf("expected empty message, got %q", snap.Comments[0].Message)
112-			}
113-			break
114-		}
115-	}
116-
117-	if !found {
118-		t.Errorf("bug with title %q not found", title)
119-	}
120-}
121-
122-// TestNewCommand_NoIdentityError tests error when identity not set
123-func TestNewCommand_NoIdentityError(t *testing.T) {
124-	tmpDir := t.TempDir()
125-
126-	// Initialize a git repo (but no git-bug identities)
127-	initCmd := exec.Command("git", "init")
128-	initCmd.Dir = tmpDir
129-	if err := initCmd.Run(); err != nil {
130-		t.Fatalf("failed to init git repo: %v", err)
131-	}
132-
133-	// Try to create a bug without identity
134-	err := runNew(tmpDir, "Test Title", "Test message")
135-	if err == nil {
136-		t.Error("expected error when no identity is set, got nil")
137-	}
138-
139-	if !strings.Contains(err.Error(), "failed to get user identity") {
140-		t.Errorf("expected 'failed to get user identity' error, got: %v", err)
141-	}
142-}
143-
144-// TestNewCommand_EmptyTitleError tests error for empty title
145-func TestNewCommand_EmptyTitleError(t *testing.T) {
146-	tmpDir := t.TempDir()
147-
148-	// Initialize a git repo
149-	initCmd := exec.Command("git", "init")
150-	initCmd.Dir = tmpDir
151-	if err := initCmd.Run(); err != nil {
152-		t.Fatalf("failed to init git repo: %v", err)
153-	}
154-
155-	// Initialize git-bug identities
156-	if err := createIdentity(tmpDir, "Test User", "[email protected]", true); err != nil {
157-		t.Fatalf("failed to create identity: %v", err)
158-	}
159-
160-	// Try to create a bug with empty title (this would normally trigger editor)
161-	// For direct testing, we test the validation after editor parsing
162-	err := runNew(tmpDir, "   ", "") // Whitespace-only title
163-	if err == nil {
164-		t.Error("expected error for empty title, got nil")
165-	}
166-
167-	if !strings.Contains(err.Error(), "title cannot be empty") {
168-		t.Errorf("expected 'title cannot be empty' error, got: %v", err)
169-	}
170-}
D cmd/bug/read_integration_test.go
+0, -66
 1@@ -1,66 +0,0 @@
 2-//go:build integration
 3-
 4-package main
 5-
 6-import (
 7-	"os/exec"
 8-	"strings"
 9-	"testing"
10-)
11-
12-// TestReadCommand_Integration tests the read command functionality
13-func TestReadCommand_Integration(t *testing.T) {
14-	// Create a temporary directory for our test repo
15-	tmpDir := t.TempDir()
16-
17-	// Initialize a git repo
18-	initCmd := exec.Command("git", "init")
19-	initCmd.Dir = tmpDir
20-	if err := initCmd.Run(); err != nil {
21-		t.Fatalf("failed to init git repo: %v", err)
22-	}
23-	// Add origin remote for bug init
24-	remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
25-	remoteCmd.Dir = tmpDir
26-	if err := remoteCmd.Run(); err != nil {
27-		t.Fatalf("failed to add origin remote: %v", err)
28-	}
29-
30-	// Initialize bug identities
31-	if err := runInitWithReader(tmpDir, strings.NewReader("Test User\[email protected]\n")); err != nil {
32-		t.Fatalf("Failed to init bug: %v", err)
33-	}
34-
35-	// Create a bug with a title and description
36-	title := "Test Issue for Read Command"
37-	message := "This is a test description for the read command."
38-	if err := runNew(tmpDir, title, message); err != nil {
39-		t.Fatalf("Failed to create bug: %v", err)
40-	}
41-
42-	// Load bugs to get the ID
43-	issues, err := LoadBugs(tmpDir)
44-	if err != nil {
45-		t.Fatalf("Failed to load bugs: %v", err)
46-	}
47-
48-	if len(issues) == 0 {
49-		t.Fatal("No issues found after creating bug")
50-	}
51-
52-	bugID := issues[0].ShortID
53-
54-	// Add a comment to the bug
55-	commentMsg := "This is a test comment."
56-	if err := runComment(tmpDir, bugID, commentMsg); err != nil {
57-		t.Fatalf("Failed to add comment: %v", err)
58-	}
59-
60-	// Test read command - this should not error
61-	// We can't easily capture stdout, but we can verify it doesn't panic or error
62-	// For a more complete test, we could refactor runRead to return a string
63-	err = runRead(tmpDir, bugID)
64-	if err != nil {
65-		t.Fatalf("runRead failed: %v", err)
66-	}
67-}
D cmd/bug/shortid.go
+0, -209
  1@@ -1,209 +0,0 @@
  2-package main
  3-
  4-import (
  5-	"errors"
  6-	"fmt"
  7-	"sort"
  8-	"strings"
  9-
 10-	"github.com/git-bug/git-bug/entities/bug"
 11-	"github.com/git-bug/git-bug/repository"
 12-)
 13-
 14-// Errors returned by ShortIDMap operations
 15-var (
 16-	ErrIDNotFound      = errors.New("full ID not found")
 17-	ErrAmbiguousPrefix = errors.New("short ID prefix is ambiguous")
 18-	ErrPrefixNotFound  = errors.New("short ID prefix not found")
 19-)
 20-
 21-// ShortIDMap holds the mapping between full IDs and their shortest unique prefixes
 22-type ShortIDMap struct {
 23-	// fullToShort maps full ID โ†’ shortest unique prefix
 24-	fullToShort map[string]string
 25-
 26-	// shortToFull maps shortest unique prefix โ†’ full ID
 27-	shortToFull map[string]string
 28-
 29-	// allFullIDs holds all full IDs for prefix matching
 30-	allFullIDs []string
 31-}
 32-
 33-// ShortIDGenerator creates ShortIDMap from git-bug artifacts (issues and comments)
 34-type ShortIDGenerator struct {
 35-	repoPath string
 36-}
 37-
 38-// NewShortIDGenerator creates a generator for the given repo path
 39-func NewShortIDGenerator(repoPath string) *ShortIDGenerator {
 40-	return &ShortIDGenerator{
 41-		repoPath: repoPath,
 42-	}
 43-}
 44-
 45-// diffPosition returns the first position where two strings differ
 46-// Returns the length of the shorter string if one is a prefix of the other
 47-func diffPosition(a, b string) int {
 48-	minLen := len(a)
 49-	if len(b) < minLen {
 50-		minLen = len(b)
 51-	}
 52-
 53-	for i := 0; i < minLen; i++ {
 54-		if a[i] != b[i] {
 55-			return i
 56-		}
 57-	}
 58-
 59-	return minLen
 60-}
 61-
 62-// findMinPrefix calculates the minimum unique prefix length for an ID at the given index
 63-// in a sorted slice of IDs. It compares with both previous and next neighbors.
 64-func findMinPrefix(sortedIDs []string, index int) int {
 65-	if len(sortedIDs) == 0 {
 66-		return 0
 67-	}
 68-
 69-	if len(sortedIDs) == 1 {
 70-		return 1
 71-	}
 72-
 73-	id := sortedIDs[index]
 74-	maxDiff := 0
 75-
 76-	// Compare with previous neighbor
 77-	if index > 0 {
 78-		prevDiff := diffPosition(id, sortedIDs[index-1])
 79-		if prevDiff > maxDiff {
 80-			maxDiff = prevDiff
 81-		}
 82-	}
 83-
 84-	// Compare with next neighbor
 85-	if index < len(sortedIDs)-1 {
 86-		nextDiff := diffPosition(id, sortedIDs[index+1])
 87-		if nextDiff > maxDiff {
 88-			maxDiff = nextDiff
 89-		}
 90-	}
 91-
 92-	// Minimum unique prefix length is maxDiff + 1
 93-	// But never exceed the full ID length
 94-	minLen := maxDiff + 1
 95-	if minLen > len(id) {
 96-		minLen = len(id)
 97-	}
 98-
 99-	return minLen
100-}
101-
102-// Generate builds a ShortIDMap from all artifacts in the repo.
103-// This queries git-bug for all issues and their comments.
104-// Issue IDs are 64-char SHA256 hashes, comment IDs are 64-char CombinedIds
105-// (interleaved BugID + OperationID). Both are unique across the repo.
106-func (g *ShortIDGenerator) Generate() (*ShortIDMap, error) {
107-	repo, err := repository.OpenGoGitRepo(g.repoPath, "", nil)
108-	if err != nil {
109-		return nil, fmt.Errorf("failed to open repository: %w", err)
110-	}
111-
112-	var allIDs []string
113-
114-	// Collect all issue IDs and comment IDs
115-	for streamedBug := range bug.ReadAll(repo) {
116-		if streamedBug.Err != nil {
117-			continue // Skip errors, process what we can
118-		}
119-
120-		b := streamedBug.Entity
121-
122-		// Add issue ID
123-		issueID := b.Id().String()
124-		allIDs = append(allIDs, issueID)
125-
126-		// Add comment IDs
127-		// Comment CombinedId is an interleaved ID (BugID + OperationID)
128-		// It's unique across the repo because Bug IDs are unique
129-		snap := b.Compile()
130-		for _, comment := range snap.Comments {
131-			commentID := comment.CombinedId().String()
132-			allIDs = append(allIDs, commentID)
133-		}
134-	}
135-
136-	if len(allIDs) == 0 {
137-		return &ShortIDMap{
138-			fullToShort: make(map[string]string),
139-			shortToFull: make(map[string]string),
140-			allFullIDs:  []string{},
141-		}, nil
142-	}
143-
144-	// Sort IDs to enable neighbor comparison
145-	sort.Strings(allIDs)
146-
147-	// Build mappings
148-	fullToShort := make(map[string]string)
149-	shortToFull := make(map[string]string)
150-
151-	for i, fullID := range allIDs {
152-		prefixLen := findMinPrefix(allIDs, i)
153-		shortID := fullID[:prefixLen]
154-
155-		fullToShort[fullID] = shortID
156-		shortToFull[shortID] = fullID
157-	}
158-
159-	return &ShortIDMap{
160-		fullToShort: fullToShort,
161-		shortToFull: shortToFull,
162-		allFullIDs:  allIDs,
163-	}, nil
164-}
165-
166-// GetShortID returns the shortest unique prefix for a full ID.
167-// Returns ErrIDNotFound if the full ID is not in the map.
168-func (m *ShortIDMap) GetShortID(fullID string) (string, error) {
169-	if m == nil {
170-		return "", ErrIDNotFound
171-	}
172-
173-	shortID, ok := m.fullToShort[fullID]
174-	if !ok {
175-		return "", fmt.Errorf("%w: %s", ErrIDNotFound, fullID)
176-	}
177-
178-	return shortID, nil
179-}
180-
181-// GetFullID returns the full ID for a short ID prefix.
182-// The prefix can be the exact short ID or a longer prefix of the full ID.
183-// Returns ErrPrefixNotFound if no ID matches, ErrAmbiguousPrefix if multiple match.
184-func (m *ShortIDMap) GetFullID(shortID string) (string, error) {
185-	if m == nil {
186-		return "", ErrPrefixNotFound
187-	}
188-
189-	// First, check if it's an exact match for a short ID
190-	if fullID, ok := m.shortToFull[shortID]; ok {
191-		return fullID, nil
192-	}
193-
194-	// Otherwise, search for all IDs that start with this prefix
195-	var matches []string
196-	for _, fullID := range m.allFullIDs {
197-		if strings.HasPrefix(fullID, shortID) {
198-			matches = append(matches, fullID)
199-		}
200-	}
201-
202-	switch len(matches) {
203-	case 0:
204-		return "", fmt.Errorf("%w: %s", ErrPrefixNotFound, shortID)
205-	case 1:
206-		return matches[0], nil
207-	default:
208-		return "", fmt.Errorf("%w: prefix %q matches %v", ErrAmbiguousPrefix, shortID, matches)
209-	}
210-}
D cmd/bug/shortid_integration_test.go
+0, -46
 1@@ -1,46 +0,0 @@
 2-//go:build integration
 3-// +build integration
 4-
 5-package main
 6-
 7-import (
 8-	"testing"
 9-)
10-
11-// TestGenerate_Integration tests the Generate method with a real git-bug repo.
12-// This test requires a git repository with git-bug data at the specified path.
13-// Run with: go test -tags=integration -v ./...
14-func TestGenerate_Integration(t *testing.T) {
15-	// This is a placeholder - in practice, you'd need to set up a test repo
16-	// or use an environment variable to specify the repo path
17-	repoPath := "."
18-
19-	gen := NewShortIDGenerator(repoPath)
20-	m, err := gen.Generate()
21-	if err != nil {
22-		// It's okay if there's no git-bug data, just skip
23-		t.Skipf("Skipping integration test - no git-bug data: %v", err)
24-	}
25-
26-	// Verify we can get short IDs for all full IDs
27-	for fullID, shortID := range m.fullToShort {
28-		gotShortID, err := m.GetShortID(fullID)
29-		if err != nil {
30-			t.Errorf("GetShortID(%q) failed: %v", fullID, err)
31-			continue
32-		}
33-		if gotShortID != shortID {
34-			t.Errorf("GetShortID(%q) = %q, want %q", fullID, gotShortID, shortID)
35-		}
36-
37-		// Verify we can reverse it
38-		gotFullID, err := m.GetFullID(shortID)
39-		if err != nil {
40-			t.Errorf("GetFullID(%q) failed: %v", shortID, err)
41-			continue
42-		}
43-		if gotFullID != fullID {
44-			t.Errorf("GetFullID(%q) = %q, want %q", shortID, gotFullID, fullID)
45-		}
46-	}
47-}
D cmd/bug/shortid_test.go
+0, -254
  1@@ -1,254 +0,0 @@
  2-package main
  3-
  4-import (
  5-	"errors"
  6-	"sort"
  7-	"testing"
  8-)
  9-
 10-func TestDiffPosition(t *testing.T) {
 11-	tests := []struct {
 12-		name     string
 13-		a        string
 14-		b        string
 15-		expected int
 16-	}{
 17-		{"identical strings", "abc", "abc", 3},
 18-		{"differs at start", "abc", "xyz", 0},
 19-		{"differs at position 1", "abc", "axc", 1},
 20-		{"differs at position 2", "abc", "abx", 2},
 21-		{"prefix of other", "abc", "abcd", 3},
 22-		{"empty strings", "", "", 0},
 23-		{"one empty", "abc", "", 0},
 24-		{"hex IDs", "87cghty", "87dfty4", 2},
 25-	}
 26-
 27-	for _, tt := range tests {
 28-		t.Run(tt.name, func(t *testing.T) {
 29-			result := diffPosition(tt.a, tt.b)
 30-			if result != tt.expected {
 31-				t.Errorf("diffPosition(%q, %q) = %d, want %d", tt.a, tt.b, result, tt.expected)
 32-			}
 33-		})
 34-	}
 35-}
 36-
 37-func TestFindMinPrefix(t *testing.T) {
 38-	tests := []struct {
 39-		name     string
 40-		ids      []string
 41-		index    int
 42-		expected int
 43-	}{
 44-		{"single ID", []string{"abc"}, 0, 1},
 45-		{"two IDs - first", []string{"87c", "87d"}, 0, 3},
 46-		{"two IDs - second", []string{"87c", "87d"}, 1, 3},
 47-		{"three IDs - first", []string{"87cghty", "87dfty4", "abcdef"}, 0, 3},
 48-		{"three IDs - middle", []string{"87cghty", "87dfty4", "abcdef"}, 1, 3},
 49-		{"three IDs - last", []string{"87cghty", "87dfty4", "abcdef"}, 2, 1},
 50-		{"all unique at start", []string{"a", "b", "c"}, 0, 1},
 51-		{"shared long prefix", []string{"abcd123", "abcd456", "abcd789"}, 1, 5},
 52-		{"empty slice", []string{}, 0, 0},
 53-	}
 54-
 55-	for _, tt := range tests {
 56-		t.Run(tt.name, func(t *testing.T) {
 57-			result := findMinPrefix(tt.ids, tt.index)
 58-			if result != tt.expected {
 59-				t.Errorf("findMinPrefix(%v, %d) = %d, want %d", tt.ids, tt.index, result, tt.expected)
 60-			}
 61-		})
 62-	}
 63-}
 64-
 65-func TestShortIDMap_GetShortID(t *testing.T) {
 66-	// Create a test map
 67-	m := &ShortIDMap{
 68-		fullToShort: map[string]string{
 69-			"87cghty": "87c",
 70-			"87dfty4": "87d",
 71-			"abcdef":  "a",
 72-		},
 73-		shortToFull: map[string]string{
 74-			"87c": "87cghty",
 75-			"87d": "87dfty4",
 76-			"a":   "abcdef",
 77-		},
 78-		allFullIDs: []string{"87cghty", "87dfty4", "abcdef"},
 79-	}
 80-
 81-	tests := []struct {
 82-		name    string
 83-		fullID  string
 84-		want    string
 85-		wantErr bool
 86-		errIs   error
 87-	}{
 88-		{"existing ID 1", "87cghty", "87c", false, nil},
 89-		{"existing ID 2", "87dfty4", "87d", false, nil},
 90-		{"existing ID 3", "abcdef", "a", false, nil},
 91-		{"non-existent ID", "nonexistent", "", true, ErrIDNotFound},
 92-	}
 93-
 94-	for _, tt := range tests {
 95-		t.Run(tt.name, func(t *testing.T) {
 96-			got, err := m.GetShortID(tt.fullID)
 97-			if (err != nil) != tt.wantErr {
 98-				t.Errorf("GetShortID() error = %v, wantErr %v", err, tt.wantErr)
 99-				return
100-			}
101-			if tt.wantErr && tt.errIs != nil && !errors.Is(err, tt.errIs) {
102-				t.Errorf("GetShortID() error = %v, want error Is %v", err, tt.errIs)
103-				return
104-			}
105-			if got != tt.want {
106-				t.Errorf("GetShortID() = %v, want %v", got, tt.want)
107-			}
108-		})
109-	}
110-}
111-
112-func TestShortIDMap_GetShortID_NilMap(t *testing.T) {
113-	var m *ShortIDMap
114-	_, err := m.GetShortID("test")
115-	if !errors.Is(err, ErrIDNotFound) {
116-		t.Errorf("expected ErrIDNotFound for nil map, got %v", err)
117-	}
118-}
119-
120-func TestShortIDMap_GetFullID(t *testing.T) {
121-	// Create a test map
122-	m := &ShortIDMap{
123-		fullToShort: map[string]string{
124-			"87cghty": "87c",
125-			"87dfty4": "87d",
126-			"abcdef":  "a",
127-		},
128-		shortToFull: map[string]string{
129-			"87c": "87cghty",
130-			"87d": "87dfty4",
131-			"a":   "abcdef",
132-		},
133-		allFullIDs: []string{"87cghty", "87dfty4", "abcdef"},
134-	}
135-
136-	tests := []struct {
137-		name    string
138-		shortID string
139-		want    string
140-		wantErr bool
141-		errIs   error
142-	}{
143-		{"exact short ID 1", "87c", "87cghty", false, nil},
144-		{"exact short ID 2", "87d", "87dfty4", false, nil},
145-		{"exact short ID 3", "a", "abcdef", false, nil},
146-		{"longer prefix", "87cg", "87cghty", false, nil},
147-		{"non-existent prefix", "xyz", "", true, ErrPrefixNotFound},
148-		{"ambiguous prefix", "87", "", true, ErrAmbiguousPrefix},
149-		{"empty prefix", "", "", true, ErrAmbiguousPrefix},
150-	}
151-
152-	for _, tt := range tests {
153-		t.Run(tt.name, func(t *testing.T) {
154-			got, err := m.GetFullID(tt.shortID)
155-			if (err != nil) != tt.wantErr {
156-				t.Errorf("GetFullID() error = %v, wantErr %v", err, tt.wantErr)
157-				return
158-			}
159-			if tt.wantErr && tt.errIs != nil && !errors.Is(err, tt.errIs) {
160-				t.Errorf("GetFullID() error = %v, want error Is %v", err, tt.errIs)
161-				return
162-			}
163-			if got != tt.want {
164-				t.Errorf("GetFullID() = %v, want %v", got, tt.want)
165-			}
166-		})
167-	}
168-}
169-
170-func TestShortIDMap_GetFullID_NilMap(t *testing.T) {
171-	var m *ShortIDMap
172-	_, err := m.GetFullID("test")
173-	if !errors.Is(err, ErrPrefixNotFound) {
174-		t.Errorf("expected ErrPrefixNotFound for nil map, got %v", err)
175-	}
176-}
177-
178-func TestShortIDMap_EdgeCases(t *testing.T) {
179-	tests := []struct {
180-		name        string
181-		ids         []string
182-		expectedMap map[string]string // fullID -> expected shortID
183-	}{
184-		{
185-			name:        "empty repo",
186-			ids:         []string{},
187-			expectedMap: map[string]string{},
188-		},
189-		{
190-			name: "single ID",
191-			ids:  []string{"abc123"},
192-			expectedMap: map[string]string{
193-				"abc123": "a",
194-			},
195-		},
196-		{
197-			name: "two IDs differ at start",
198-			ids:  []string{"abc", "xyz"},
199-			expectedMap: map[string]string{
200-				"abc": "a",
201-				"xyz": "x",
202-			},
203-		},
204-		{
205-			name: "shared long prefix",
206-			ids:  []string{"abcd123", "abcd456", "abcd789"},
207-			expectedMap: map[string]string{
208-				"abcd123": "abcd1",
209-				"abcd456": "abcd4",
210-				"abcd789": "abcd7",
211-			},
212-		},
213-		{
214-			name: "sequential hex IDs",
215-			ids:  []string{"87cghty", "87dfty4", "abcdef"},
216-			expectedMap: map[string]string{
217-				"87cghty": "87c",
218-				"87dfty4": "87d",
219-				"abcdef":  "a",
220-			},
221-		},
222-	}
223-
224-	for _, tt := range tests {
225-		t.Run(tt.name, func(t *testing.T) {
226-			// Sort the IDs like Generate does
227-			sort.Strings(tt.ids)
228-
229-			// Build the map
230-			fullToShort := make(map[string]string)
231-			for i, fullID := range tt.ids {
232-				prefixLen := findMinPrefix(tt.ids, i)
233-				shortID := fullID[:prefixLen]
234-				fullToShort[fullID] = shortID
235-			}
236-
237-			// Verify
238-			for fullID, expectedShort := range tt.expectedMap {
239-				if got := fullToShort[fullID]; got != expectedShort {
240-					t.Errorf("fullID %q: got shortID %q, want %q", fullID, got, expectedShort)
241-				}
242-			}
243-		})
244-	}
245-}
246-
247-func TestNewShortIDGenerator(t *testing.T) {
248-	gen := NewShortIDGenerator("/path/to/repo")
249-	if gen == nil {
250-		t.Fatal("NewShortIDGenerator returned nil")
251-	}
252-	if gen.repoPath != "/path/to/repo" {
253-		t.Errorf("repoPath = %q, want %q", gen.repoPath, "/path/to/repo")
254-	}
255-}
D cmd/bug/term.go
+0, -64
 1@@ -1,64 +0,0 @@
 2-package main
 3-
 4-import (
 5-	"fmt"
 6-
 7-	"github.com/git-bug/git-bug/cache"
 8-	"github.com/git-bug/git-bug/repository"
 9-	"github.com/git-bug/git-bug/termui"
10-	"github.com/spf13/cobra"
11-)
12-
13-// runTerm launches the git-bug terminal UI
14-// It first invalidates the cache to ensure git-bug rebuilds it,
15-// then opens the repository, creates a cache, and runs the termui.
16-func runTerm(repoPath string) error {
17-	// Invalidate cache so git-bug is forced to rebuild it
18-	invalidateGitBugCache(repoPath)
19-
20-	// Open the repository
21-	repo, err := repository.OpenGoGitRepo(repoPath, "", nil)
22-	if err != nil {
23-		return fmt.Errorf("failed to open repository: %w", err)
24-	}
25-	defer repo.Close()
26-
27-	// Create the cache (will rebuild since we invalidated)
28-	// Using NewRepoCacheNoEvents to simplify - it waits for all build events
29-	cacheInstance, err := cache.NewRepoCacheNoEvents(repo)
30-	if err != nil {
31-		return fmt.Errorf("failed to create cache: %w", err)
32-	}
33-	defer cacheInstance.Close()
34-
35-	// Run the terminal UI
36-	if err := termui.Run(cacheInstance); err != nil {
37-		return fmt.Errorf("termui error: %w", err)
38-	}
39-
40-	return nil
41-}
42-
43-var termCmd = &cobra.Command{
44-	Use:     "term",
45-	Aliases: []string{"termui", "ui"},
46-	Short:   "Launch the terminal UI to browse and edit bugs",
47-	Long: `Launch the git-bug terminal UI to browse and edit bugs interactively.
48-
49-This command starts the full-featured terminal interface for managing git-bug
50-issues. It provides a two-pane view with a bug list and detailed bug view,
51-allowing you to browse, create, edit, and comment on bugs.
52-
53-Before starting the UI, the cache is invalidated to ensure git-bug rebuilds
54-it with the latest data. This keeps the 'bug' command and 'git-bug' binary
55-in sync.
56-
57-Examples:
58-  bug term                    # Launch terminal UI in current directory
59-  bug termui                  # Same as above (alias)
60-  bug ui                      # Same as above (alias)
61-  bug term --repo /path/to/repo  # Launch UI for specific repository`,
62-	RunE: func(cmd *cobra.Command, args []string) error {
63-		return runTerm(repoPath)
64-	},
65-}
M config.go
+1, -1
1@@ -218,7 +218,7 @@ func (s *SummaryPageData) Active() string { return "summary" }
2 func (s *TreePageData) Active() string    { return "code" }
3 func (s *FilePageData) Active() string    { return "code" }
4 func (s *LogPageData) Active() string     { return "commits" }
5-func (s *CommitPageData) Active() string  { return "commits" }
6+func (s *CommitPageData) Active() string  { return "commit" }
7 func (s *RefPageData) Active() string     { return "refs" }
8 
9 func Bail(err error) {
A flake.lock
+61, -0
 1@@ -0,0 +1,61 @@
 2+{
 3+  "nodes": {
 4+    "flake-utils": {
 5+      "inputs": {
 6+        "systems": "systems"
 7+      },
 8+      "locked": {
 9+        "lastModified": 1731533236,
10+        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
11+        "owner": "numtide",
12+        "repo": "flake-utils",
13+        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
14+        "type": "github"
15+      },
16+      "original": {
17+        "owner": "numtide",
18+        "repo": "flake-utils",
19+        "type": "github"
20+      }
21+    },
22+    "nixpkgs": {
23+      "locked": {
24+        "lastModified": 1775710090,
25+        "narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
26+        "owner": "NixOS",
27+        "repo": "nixpkgs",
28+        "rev": "4c1018dae018162ec878d42fec712642d214fdfa",
29+        "type": "github"
30+      },
31+      "original": {
32+        "owner": "NixOS",
33+        "ref": "nixos-unstable",
34+        "repo": "nixpkgs",
35+        "type": "github"
36+      }
37+    },
38+    "root": {
39+      "inputs": {
40+        "flake-utils": "flake-utils",
41+        "nixpkgs": "nixpkgs"
42+      }
43+    },
44+    "systems": {
45+      "locked": {
46+        "lastModified": 1681028828,
47+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
48+        "owner": "nix-systems",
49+        "repo": "default",
50+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
51+        "type": "github"
52+      },
53+      "original": {
54+        "owner": "nix-systems",
55+        "repo": "default",
56+        "type": "github"
57+      }
58+    }
59+  },
60+  "root": "root",
61+  "version": 7
62+}
A flake.nix
+46, -0
 1@@ -0,0 +1,46 @@
 2+{
 3+  description = "pgit - static site generator for git repositories";
 4+
 5+  inputs = {
 6+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
 7+    flake-utils.url = "github:numtide/flake-utils";
 8+  };
 9+
10+  outputs = { self, nixpkgs, flake-utils }:
11+    flake-utils.lib.eachDefaultSystem (system:
12+      let
13+        pkgs = nixpkgs.legacyPackages.${system};
14+
15+        # Read the version from go.mod or use a default
16+        version = "0.1.0";
17+      in
18+      {
19+        packages.default = pkgs.buildGoModule {
20+          pname = "pgit";
21+          inherit version;
22+          src = ./.;
23+
24+          # This hash will need to be updated based on go.sum
25+          # Run 'nix build' and it will fail with the expected hash
26+          vendorHash = "sha256-95H+k3LHaB6WjnLpwjviCopwfO9MKbyiVKB5HWyNZgE=";
27+
28+          subPackages = [ "cmd/pgit" ];
29+
30+          meta = with pkgs.lib; {
31+            description = "Static site generator for git repositories";
32+            homepage = "https://code.kilimanjaro.io/pgit";
33+            license = licenses.gpl3Only;
34+            mainProgram = "pgit";
35+          };
36+        };
37+
38+        devShells.default = pkgs.mkShell {
39+          buildInputs = with pkgs; [
40+            go
41+            git
42+            gopls
43+            golangci-lint
44+          ];
45+        };
46+      });
47+}
M go.mod
+0, -19
 1@@ -4,7 +4,6 @@ go 1.25.0
 2 
 3 require (
 4 	github.com/alecthomas/chroma/v2 v2.13.0
 5-	github.com/charmbracelet/lipgloss v1.1.0
 6 	github.com/dustin/go-humanize v1.0.1
 7 	github.com/git-bug/git-bug v0.10.1
 8 	github.com/gogs/git-module v1.6.0
 9@@ -17,12 +16,9 @@ require (
10 	dario.cat/mergo v1.0.2 // indirect
11 	github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
12 	github.com/99designs/keyring v1.2.2 // indirect
13-	github.com/MichaelMure/go-term-text v0.3.1 // indirect
14 	github.com/Microsoft/go-winio v0.6.2 // indirect
15 	github.com/ProtonMail/go-crypto v1.4.1 // indirect
16 	github.com/RoaringBitmap/roaring v1.9.4 // indirect
17-	github.com/awesome-gocui/gocui v1.1.0 // indirect
18-	github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
19 	github.com/bits-and-blooms/bitset v1.24.4 // indirect
20 	github.com/blevesearch/bleve v1.0.14 // indirect
21 	github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
22@@ -34,13 +30,6 @@ require (
23 	github.com/blevesearch/zap/v13 v13.0.6 // indirect
24 	github.com/blevesearch/zap/v14 v14.0.5 // indirect
25 	github.com/blevesearch/zap/v15 v15.0.3 // indirect
26-	github.com/charmbracelet/colorprofile v0.4.1 // indirect
27-	github.com/charmbracelet/x/ansi v0.11.6 // indirect
28-	github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
29-	github.com/charmbracelet/x/term v0.2.2 // indirect
30-	github.com/clipperhouse/displaywidth v0.9.0 // indirect
31-	github.com/clipperhouse/stringish v0.1.1 // indirect
32-	github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
33 	github.com/cloudflare/circl v1.6.3 // indirect
34 	github.com/couchbase/vellum v1.0.2 // indirect
35 	github.com/cyphar/filepath-securejoin v0.6.1 // indirect
36@@ -50,8 +39,6 @@ require (
37 	github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
38 	github.com/emirpasic/gods v1.18.1 // indirect
39 	github.com/fatih/color v1.19.0 // indirect
40-	github.com/gdamore/encoding v1.0.1 // indirect
41-	github.com/gdamore/tcell/v2 v2.7.4 // indirect
42 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
43 	github.com/go-git/go-billy/v5 v5.8.0 // indirect
44 	github.com/go-git/go-git/v5 v5.17.2 // indirect
45@@ -60,23 +47,18 @@ require (
46 	github.com/golang/protobuf v1.5.4 // indirect
47 	github.com/golang/snappy v1.0.0 // indirect
48 	github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
49-	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
50 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
51 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
52 	github.com/kevinburke/ssh_config v1.6.0 // indirect
53 	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
54-	github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
55 	github.com/mattn/go-colorable v0.1.14 // indirect
56 	github.com/mattn/go-isatty v0.0.20 // indirect
57-	github.com/mattn/go-runewidth v0.0.19 // indirect
58 	github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 // indirect
59 	github.com/mschoch/smat v0.2.0 // indirect
60 	github.com/mtibben/percent v0.2.1 // indirect
61-	github.com/muesli/termenv v0.16.0 // indirect
62 	github.com/pjbgf/sha1cd v0.5.0 // indirect
63 	github.com/pkg/errors v0.9.1 // indirect
64 	github.com/pmezard/go-difflib v1.0.0 // indirect
65-	github.com/rivo/uniseg v0.4.7 // indirect
66 	github.com/sergi/go-diff v1.4.0 // indirect
67 	github.com/skeema/knownhosts v1.3.2 // indirect
68 	github.com/spf13/pflag v1.0.9 // indirect
69@@ -85,7 +67,6 @@ require (
70 	github.com/tdewolff/parse/v2 v2.8.11 // indirect
71 	github.com/willf/bitset v1.1.11 // indirect
72 	github.com/xanzy/ssh-agent v0.3.3 // indirect
73-	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
74 	go.etcd.io/bbolt v1.4.3 // indirect
75 	golang.org/x/crypto v0.49.0 // indirect
76 	golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
M go.sum
+0, -77
  1@@ -5,8 +5,6 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN
  2 github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
  3 github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
  4 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
  5-github.com/MichaelMure/go-term-text v0.3.1 h1:Kw9kZanyZWiCHOYu9v/8pWEgDQ6UVN9/ix2Vd2zzWf0=
  6-github.com/MichaelMure/go-term-text v0.3.1/go.mod h1:QgVjAEDUnRMlzpS6ky5CGblux7ebeiLnuy9dAaFZu8o=
  7 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
  8 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
  9 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
 10@@ -26,10 +24,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuW
 11 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 12 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
 13 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 14-github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII=
 15-github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg=
 16-github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
 17-github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
 18 github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 19 github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
 20 github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
 21@@ -58,22 +52,6 @@ github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67n
 22 github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY=
 23 github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
 24 github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
 25-github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
 26-github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
 27-github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
 28-github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
 29-github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
 30-github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
 31-github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
 32-github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
 33-github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
 34-github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
 35-github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
 36-github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
 37-github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
 38-github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
 39-github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
 40-github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
 41 github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
 42 github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
 43 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 44@@ -112,12 +90,6 @@ github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+ne
 45 github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
 46 github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
 47 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 48-github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
 49-github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
 50-github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
 51-github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU=
 52-github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
 53-github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
 54 github.com/git-bug/git-bug v0.10.1 h1:mjJK/wrfKWUze5EkVpJj+3N3LzSfeujqqH1qq8x4aco=
 55 github.com/git-bug/git-bug v0.10.1/go.mod h1:43BRtb/Nr6QEJlNLkLY/64vyopxA0y5nvjSNsktnan8=
 56 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 57@@ -153,8 +125,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
 58 github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 59 github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
 60 github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
 61-github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 62-github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
 63 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 64 github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
 65 github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
 66@@ -180,20 +150,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 67 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 68 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 69 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 70-github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 71-github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 72-github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
 73-github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
 74 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 75 github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
 76 github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 77 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 78 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 79-github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 80-github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
 81-github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 82-github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
 83-github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
 84 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 h1:Pijfgr7ZuvX7QIQiEwLdRVr3RoMG+i0SbBO1Qu+7yVk=
 85 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
 86 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 87@@ -203,8 +164,6 @@ github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
 88 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
 89 github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
 90 github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
 91-github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
 92-github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 93 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 94 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 95 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 96@@ -221,11 +180,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 97 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 98 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 99 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
100-github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
101-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
102-github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
103-github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
104-github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
105 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
106 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
107 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
108@@ -273,43 +227,28 @@ github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
109 github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
110 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
111 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
112-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
113-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
114 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
115-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
116 go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
117 go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
118 go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
119 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
120 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
121-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
122-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
123 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
124 golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
125 golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
126 golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
127 golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
128-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
129-golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
130 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
131-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
132-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
133 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
134-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
135-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
136 golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
137 golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
138 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
139-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
140 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
141-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
142-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
143 golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
144 golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
145 golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
146 golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
147 golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
148-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
149 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
151 golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
152@@ -317,34 +256,18 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
153 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
154 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
155 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
156-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
157 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
160 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
162 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
163 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
164 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
165-golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
166-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
167-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
168-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
169 golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
170 golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
171 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
172-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
173 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
174-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
175-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
176-golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
177 golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
178 golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
179 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
180-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
181-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
182-golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
183-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
184 google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
185 google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
186 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
M html/issue_detail.page.tmpl
+5, -2
 1@@ -36,8 +36,11 @@
 2     <div class="issue-comment">
 3       <div class="issue-comment__header">
 4         <span class="issue-comment__author">{{.Author}}</span>
 5-        <span class="issue-comment__date">
 6-          <span class="human-time" data-time="{{.CreatedAtISO}}">{{.CreatedAtDisp}}</span>
 7+        <span class="issue-comment__meta">
 8+          <span class="issue-comment__id">{{.ID}}</span>
 9+          <span class="issue-comment__date">
10+            <span class="human-time" data-time="{{.CreatedAtISO}}">{{.CreatedAtDisp}}</span>
11+          </span>
12         </span>
13       </div>
14       <div class="issue-comment__body markdown">{{.Body}}</div>
M issues.go
+3, -1
 1@@ -50,6 +50,7 @@ type IssueData struct {
 2 }
 3 
 4 type CommentData struct {
 5+	ID            string
 6 	Author        string
 7 	CreatedAt     string
 8 	CreatedAtISO  string
 9@@ -75,7 +76,7 @@ type IssueDetailPageData struct {
10 }
11 
12 func (i *IssuesListPageData) Active() string  { return "issues" }
13-func (i *IssueDetailPageData) Active() string { return "issues" }
14+func (i *IssueDetailPageData) Active() string { return "issue" }
15 
16 func (c *Config) loadIssues() ([]*IssueData, error) {
17 	c.Logger.Info("loading issues from git-bug", "repoPath", c.RepoPath)
18@@ -113,6 +114,7 @@ func (c *Config) loadIssues() ([]*IssueData, error) {
19 			}
20 			createdAtTime, _ := time.Parse("Mon Jan 2 15:04:05 2006 -0700", comment.FormatTime())
21 			comments = append(comments, CommentData{
22+				ID:            GetShortID(comment.CombinedId().String()),
23 				Author:        comment.Author.Name(),
24 				CreatedAt:     comment.FormatTime(),
25 				CreatedAtISO:  createdAtTime.UTC().Format(time.RFC3339),
M static/pgit.css
+17, -0
 1@@ -1410,6 +1410,23 @@ sup {
 2   font-size: 0.8rem;
 3 }
 4 
 5+.issue-comment__meta {
 6+  display: flex;
 7+  align-items: center;
 8+  gap: 0.5rem;
 9+}
10+
11+.issue-comment__id {
12+  color: var(--grey-light);
13+  font-size: 0.8rem;
14+  font-family: monospace;
15+}
16+
17+.issue-comment__id::after {
18+  content: " ยท ";
19+  margin-left: 0.25rem;
20+}
21+
22 .issue-comment__body {
23   line-height: var(--line-height);
24 }