Skip to content

Commit f3e83f7

Browse files
marcusclaude
andcommitted
feat: Improve agent DX based on error pattern analysis
Analyzed 40 agent errors across td and sidecar projects to identify systematic CLI friction points. Changes: - td log: detect issue ID in either arg position (not just first) - task/epic create: add --parent/--epic/--depends-on/--blocks/--minor flags - reject: add --comment/-c/--message/--note/--notes aliases - approve: add -c shorthand for --comment - review: add "finish" as command alias - handoff: accept 0 args, infer focused issue with actionable error - issueIDPattern: match both 6 and 8 char hex IDs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 05313ff commit f3e83f7

6 files changed

Lines changed: 71 additions & 20 deletions

File tree

cmd/epic.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ func init() {
9393
epicCreateCmd.Flags().StringP("priority", "p", "", "Priority (P0, P1, P2, P3, P4)")
9494
epicCreateCmd.Flags().StringP("description", "d", "", "Description text")
9595
epicCreateCmd.Flags().String("labels", "", "Comma-separated labels")
96+
epicCreateCmd.Flags().String("parent", "", "Parent issue ID")
97+
epicCreateCmd.Flags().String("epic", "", "Parent issue ID (alias for --parent)")
98+
epicCreateCmd.Flags().String("depends-on", "", "Issues this depends on")
99+
epicCreateCmd.Flags().String("blocks", "", "Issues this blocks")
96100
// Hidden type flag - set programmatically to "epic"
97101
epicCreateCmd.Flags().StringP("type", "t", "", "")
98102
epicCreateCmd.Flags().MarkHidden("type")

cmd/handoff.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"strings"
99

10+
"github.com/marcus/td/internal/config"
1011
"github.com/marcus/td/internal/db"
1112
"github.com/marcus/td/internal/git"
1213
"github.com/marcus/td/internal/input"
@@ -36,14 +37,42 @@ Or use flags with values, stdin (-), or file (@path):
3637
--done @done.txt Items from file (one per line)
3738
echo "item" | td handoff ID --done - Items from stdin`,
3839
GroupID: "workflow",
39-
Args: cobra.RangeArgs(1, 2),
40+
Args: cobra.RangeArgs(0, 2),
4041
RunE: func(cmd *cobra.Command, args []string) error {
41-
// Validate issue ID (catch empty strings)
42-
if err := ValidateIssueID(args[0], "handoff <issue-id> [message]"); err != nil {
42+
baseDir := getBaseDir()
43+
44+
// Resolve issue ID: from args or focused issue
45+
var issueArg string
46+
var messageArg string
47+
switch len(args) {
48+
case 2:
49+
issueArg = args[0]
50+
messageArg = args[1]
51+
case 1:
52+
// Check if the single arg is an issue ID or a message
53+
if issueIDPattern.MatchString(args[0]) {
54+
issueArg = args[0]
55+
} else {
56+
// Treat as message, infer issue from focus
57+
messageArg = args[0]
58+
}
59+
}
60+
61+
if issueArg == "" {
62+
// Infer from focused issue
63+
focusedID, err := config.GetFocus(baseDir)
64+
if err != nil || focusedID == "" {
65+
output.Error("no issue specified and no focused issue")
66+
fmt.Fprintln(os.Stderr, " Use: td handoff <id> [message] OR td start <id> first")
67+
return fmt.Errorf("no issue specified")
68+
}
69+
issueArg = focusedID
70+
}
71+
72+
if err := ValidateIssueID(issueArg, "handoff <issue-id> [message]"); err != nil {
4373
output.Error("%v", err)
4474
return err
4575
}
46-
baseDir := getBaseDir()
4776

4877
database, err := db.Open(baseDir)
4978
if err != nil {
@@ -58,7 +87,7 @@ Or use flags with values, stdin (-), or file (@path):
5887
return err
5988
}
6089

61-
issueID := args[0]
90+
issueID := issueArg
6291
issue, err := database.GetIssue(issueID)
6392
if err != nil {
6493
output.Error("%v", err)
@@ -92,9 +121,9 @@ Or use flags with values, stdin (-), or file (@path):
92121
handoff.Done = append(handoff.Done, message)
93122
}
94123

95-
// Handle positional message argument: td handoff <id> "message"
96-
if len(args) > 1 && args[1] != "" {
97-
handoff.Done = append(handoff.Done, args[1])
124+
// Handle positional message argument
125+
if messageArg != "" {
126+
handoff.Done = append(handoff.Done, messageArg)
98127
}
99128

100129
// Check if stdin has data (YAML format) - only if not already used by flag expansion

cmd/handoff_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,9 +404,9 @@ func TestHandoffPositionalMessage(t *testing.T) {
404404
t.Errorf("Expected 2 args to be valid: %v", err)
405405
}
406406

407-
// Test with 0 args (should fail)
408-
if err := args(handoffCmd, []string{}); err == nil {
409-
t.Error("Expected 0 args to fail")
407+
// Test with 0 args (should be valid - infers from focused issue)
408+
if err := args(handoffCmd, []string{}); err != nil {
409+
t.Errorf("Expected 0 args to be valid (infer from focus): %v", err)
410410
}
411411

412412
// Test with 3 args (should fail)

cmd/log.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import (
1717
"github.com/spf13/cobra"
1818
)
1919

20-
// issueIDPattern matches valid issue IDs like "td-a1b2c3d4"
21-
var issueIDPattern = regexp.MustCompile(`^td-[0-9a-f]{8}$`)
20+
// issueIDPattern matches valid issue IDs like "td-a1b2c3" or "td-a1b2c3d4"
21+
var issueIDPattern = regexp.MustCompile(`^td-[0-9a-f]{6,8}$`)
2222

2323
var logCmd = &cobra.Command{
2424
Use: "log [issue-id] <message>",
@@ -60,9 +60,18 @@ Supports stdin input for multi-line messages or piped input:
6060
var message string
6161

6262
if len(args) == 2 {
63-
// Two args: first is issue ID, second is message
64-
issueID = args[0]
65-
message = args[1]
63+
// Two args: detect which is issue ID regardless of order
64+
if issueIDPattern.MatchString(args[0]) {
65+
issueID = args[0]
66+
message = args[1]
67+
} else if issueIDPattern.MatchString(args[1]) {
68+
issueID = args[1]
69+
message = args[0]
70+
} else {
71+
// Neither matches ID pattern; treat as original order (first=ID, second=message)
72+
issueID = args[0]
73+
message = args[1]
74+
}
6675
} else if len(args) == 1 {
6776
// One arg: check if it's an issue ID or message
6877
// Issue IDs match pattern "td-[8 hex chars]", otherwise it's a message

cmd/review.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func submitIssueForReview(database *db.DB, issue *models.Issue, sess *session.Se
104104

105105
var reviewCmd = &cobra.Command{
106106
Use: "review [issue-id...]",
107-
Aliases: []string{"submit"},
107+
Aliases: []string{"submit", "finish"},
108108
Short: "Submit one or more issues for review",
109109
Long: `Submits the issue(s) for review. If no handoff exists, a minimal one is
110110
auto-created (consider using 'td handoff' for better documentation).
@@ -511,8 +511,8 @@ Supports bulk operations:
511511
continue
512512
}
513513

514-
// Log
515-
reason, _ := cmd.Flags().GetString("reason")
514+
// Log (supports --reason, --message, --comment, --note, --notes)
515+
reason := approvalReason(cmd)
516516
logMsg := "Rejected"
517517
if reason != "" {
518518
logMsg = "Rejected: " + reason
@@ -776,12 +776,16 @@ func init() {
776776
reviewCmd.Flags().Bool("minor", false, "Mark as minor task (allows self-review)")
777777
approveCmd.Flags().StringP("reason", "m", "", "Reason for approval")
778778
approveCmd.Flags().String("message", "", "Reason for approval (alias for --reason)")
779-
approveCmd.Flags().String("comment", "", "Reason for approval (alias for --message)")
779+
approveCmd.Flags().StringP("comment", "c", "", "Reason for approval (alias for --message)")
780780
approveCmd.Flags().String("note", "", "Reason for approval (alias for --reason)")
781781
approveCmd.Flags().String("notes", "", "Reason for approval (alias for --reason)")
782782
approveCmd.Flags().Bool("json", false, "JSON output")
783783
approveCmd.Flags().Bool("all", false, "Approve all reviewable issues")
784784
rejectCmd.Flags().StringP("reason", "m", "", "Reason for rejection")
785+
rejectCmd.Flags().StringP("comment", "c", "", "Reason for rejection (alias for --reason)")
786+
rejectCmd.Flags().String("message", "", "Reason for rejection (alias for --reason)")
787+
rejectCmd.Flags().String("note", "", "Reason for rejection (alias for --reason)")
788+
rejectCmd.Flags().String("notes", "", "Reason for rejection (alias for --reason)")
785789
rejectCmd.Flags().Bool("json", false, "JSON output")
786790
closeCmd.Flags().StringP("reason", "m", "", "Reason for closing")
787791
closeCmd.Flags().String("comment", "", "Reason for closing (alias for --reason)")

cmd/task.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ func init() {
9393
taskCreateCmd.Flags().StringP("priority", "p", "", "Priority (P0, P1, P2, P3, P4)")
9494
taskCreateCmd.Flags().StringP("description", "d", "", "Description text")
9595
taskCreateCmd.Flags().String("labels", "", "Comma-separated labels")
96+
taskCreateCmd.Flags().String("parent", "", "Parent issue ID")
97+
taskCreateCmd.Flags().String("epic", "", "Parent issue ID (alias for --parent)")
98+
taskCreateCmd.Flags().String("depends-on", "", "Issues this depends on")
99+
taskCreateCmd.Flags().String("blocks", "", "Issues this blocks")
100+
taskCreateCmd.Flags().Bool("minor", false, "Mark as minor task (allows self-review)")
96101
// Hidden type flag - set programmatically to "task"
97102
taskCreateCmd.Flags().StringP("type", "t", "", "")
98103
taskCreateCmd.Flags().MarkHidden("type")

0 commit comments

Comments
 (0)