feat(sling): add --no-merge flag to skip merge queue (#939)

When contributing to upstream repos or wanting human review before merge,
the --no-merge flag keeps polecat work on a feature branch instead of
auto-merging to main.

Changes:
- Add --no-merge flag to gt sling command
- Store no_merge field in bead's AttachmentFields
- Modify gt done to skip merge queue when no_merge is set
- Push branch and mail dispatcher "READY_FOR_REVIEW" instead
- Add tests for field parsing and sling flag storage

Closes: gt-p7b8

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
David Van Couvering
2026-01-25 18:08:34 -08:00
committed by GitHub
parent a86c7d954f
commit 92ccacffd9
6 changed files with 278 additions and 0 deletions

View File

@@ -903,6 +903,80 @@ func TestAttachmentFieldsRoundTrip(t *testing.T) {
}
}
// TestNoMergeField tests the no_merge field in AttachmentFields.
// The no_merge flag tells gt done to skip the merge queue and keep work on a feature branch.
func TestNoMergeField(t *testing.T) {
t.Run("parse no_merge true", func(t *testing.T) {
issue := &Issue{Description: "no_merge: true\ndispatched_by: mayor"}
fields := ParseAttachmentFields(issue)
if fields == nil {
t.Fatal("ParseAttachmentFields() = nil")
}
if !fields.NoMerge {
t.Error("NoMerge should be true")
}
if fields.DispatchedBy != "mayor" {
t.Errorf("DispatchedBy = %q, want 'mayor'", fields.DispatchedBy)
}
})
t.Run("parse no_merge false", func(t *testing.T) {
issue := &Issue{Description: "no_merge: false\ndispatched_by: crew"}
fields := ParseAttachmentFields(issue)
if fields == nil {
t.Fatal("ParseAttachmentFields() = nil")
}
if fields.NoMerge {
t.Error("NoMerge should be false")
}
})
t.Run("parse no-merge alternate format", func(t *testing.T) {
issue := &Issue{Description: "no-merge: true"}
fields := ParseAttachmentFields(issue)
if fields == nil {
t.Fatal("ParseAttachmentFields() = nil")
}
if !fields.NoMerge {
t.Error("NoMerge should be true with hyphen format")
}
})
t.Run("format no_merge", func(t *testing.T) {
fields := &AttachmentFields{
NoMerge: true,
DispatchedBy: "mayor",
}
got := FormatAttachmentFields(fields)
if !strings.Contains(got, "no_merge: true") {
t.Errorf("FormatAttachmentFields() missing no_merge, got:\n%s", got)
}
if !strings.Contains(got, "dispatched_by: mayor") {
t.Errorf("FormatAttachmentFields() missing dispatched_by, got:\n%s", got)
}
})
t.Run("round-trip with no_merge", func(t *testing.T) {
original := &AttachmentFields{
AttachedMolecule: "mol-test",
AttachedAt: "2026-01-24T12:00:00Z",
DispatchedBy: "gastown/crew/max",
NoMerge: true,
}
formatted := FormatAttachmentFields(original)
issue := &Issue{Description: formatted}
parsed := ParseAttachmentFields(issue)
if parsed == nil {
t.Fatal("round-trip parse returned nil")
}
if *parsed != *original {
t.Errorf("round-trip mismatch:\ngot %+v\nwant %+v", parsed, original)
}
})
}
// TestResolveBeadsDir tests the redirect following logic.
func TestResolveBeadsDir(t *testing.T) {
// Create temp directory structure

View File

@@ -21,6 +21,7 @@ type AttachmentFields struct {
AttachedAt string // ISO 8601 timestamp when attached
AttachedArgs string // Natural language args passed via gt sling --args (no-tmux mode)
DispatchedBy string // Agent ID that dispatched this work (for completion notification)
NoMerge bool // If true, gt done skips merge queue (for upstream PRs/human review)
}
// ParseAttachmentFields extracts attachment fields from an issue's description.
@@ -65,6 +66,9 @@ func ParseAttachmentFields(issue *Issue) *AttachmentFields {
case "dispatched_by", "dispatched-by", "dispatchedby":
fields.DispatchedBy = value
hasFields = true
case "no_merge", "no-merge", "nomerge":
fields.NoMerge = strings.ToLower(value) == "true"
hasFields = true
}
}
@@ -95,6 +99,9 @@ func FormatAttachmentFields(fields *AttachmentFields) string {
if fields.DispatchedBy != "" {
lines = append(lines, "dispatched_by: "+fields.DispatchedBy)
}
if fields.NoMerge {
lines = append(lines, "no_merge: true")
}
return strings.Join(lines, "\n")
}
@@ -117,6 +124,9 @@ func SetAttachmentFields(issue *Issue, fields *AttachmentFields) string {
"dispatched_by": true,
"dispatched-by": true,
"dispatchedby": true,
"no_merge": true,
"no-merge": true,
"nomerge": true,
}
// Collect non-attachment lines from existing description