From 7c28db61c95b34d014fbb9511f9d19ea9ac864c6 Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:09:07 +1000 Subject: [PATCH 1/9] feat(config): add an --agent flag to job log cmd --- cmd/job/log.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmd/job/log.go b/cmd/job/log.go index 81b1d25d..f2082d72 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -17,6 +17,7 @@ type LogCmd struct { Pipeline string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"p"` BuildNumber string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"b"` NoTimestamps bool `help:"Strip timestamp prefixes from log output" name:"no-timestamps"` + LLMOptimized bool `help:"Format output to be optimal for LLM consumption (strips ANSI, deduplicates loops)" name:"agent"` } func (c *LogCmd) Help() string { @@ -73,6 +74,10 @@ func (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { logContent = stripTimestamps(logContent) } + if c.LLMOptimized { + logContent = formatForLLM(logContent) + } + writer, cleanup := bkIO.Pager(f.NoPager) defer func() { _ = cleanup() }() @@ -85,3 +90,7 @@ var timestampRegex = regexp.MustCompile(`bk;t=\d+\x07`) func stripTimestamps(content string) string { return timestampRegex.ReplaceAllString(content, "") } + +func formatForLLM(content string) string { + return content +} From 5d1c4b4081065ca254629df9fd6f82ad45b353d1 Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:10:43 +1000 Subject: [PATCH 2/9] feat: strip ansi chars --- cmd/job/log.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/job/log.go b/cmd/job/log.go index f2082d72..bb61f80a 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -87,10 +87,13 @@ func (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { var timestampRegex = regexp.MustCompile(`bk;t=\d+\x07`) +var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) + func stripTimestamps(content string) string { return timestampRegex.ReplaceAllString(content, "") } func formatForLLM(content string) string { + content = ansiRegex.ReplaceAllString(content, "") return content } From 918e8796e2a540d80ee04b6ada5d8b7ad18bd683 Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:15:05 +1000 Subject: [PATCH 3/9] feat(logs): deduplicate and add clear headers --- cmd/job/log.go | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/cmd/job/log.go b/cmd/job/log.go index bb61f80a..df4eb0cd 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "regexp" + "strings" "github.com/alecthomas/kong" "github.com/buildkite/cli/v3/internal/cli" @@ -95,5 +96,43 @@ func stripTimestamps(content string) string { func formatForLLM(content string) string { content = ansiRegex.ReplaceAllString(content, "") - return content + + lines := strings.Split(content, "\n") + result := make([]string, 0, len(lines)) + + var prevLine string + hasPrev := false + repeatCount := 0 + + flush := func() { + if repeatCount > 0 { + result = append(result, fmt.Sprintf("[Previous line repeated %d times]", repeatCount)) + repeatCount = 0 + } + } + + for _, line := range lines { + if hasPrev && line == prevLine { + repeatCount++ + continue + } + + flush() + + processed := line + for _, prefix := range []string{"---", "+++", "~~~"} { + if strings.HasPrefix(line, prefix) { + processed = "\n=== PHASE: " + strings.TrimPrefix(line, prefix) + " ===" + break + } + } + + result = append(result, processed) + prevLine = line + hasPrev = true + } + + flush() + + return strings.Join(result, "\n") } From 4c50e15bf122116b32b7d25cd2a1288147d396df Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:18:51 +1000 Subject: [PATCH 4/9] test(logs): implement some testing to ensure fmt --- cmd/job/log.go | 40 +++++++++++++++----- cmd/job/log_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 cmd/job/log_test.go diff --git a/cmd/job/log.go b/cmd/job/log.go index df4eb0cd..0293089a 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -90,12 +90,21 @@ var timestampRegex = regexp.MustCompile(`bk;t=\d+\x07`) var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) +// oscRegex matches OSC (`\x1b]...`) and APC (`\x1b_...`) escape sequences +// terminated by BEL (\x07) or ST (\x1b\\). Buildkite uses APC sequences for its +// inline timestamp metadata (e.g. `\x1b_bk;t=1700000000000\x07`). +var oscRegex = regexp.MustCompile("\x1b[\\]_][^\x07]*(?:\x07|\x1b\\\\)") + func stripTimestamps(content string) string { return timestampRegex.ReplaceAllString(content, "") } func formatForLLM(content string) string { content = ansiRegex.ReplaceAllString(content, "") + content = oscRegex.ReplaceAllString(content, "") + // Strip any bare timestamp markers that weren't wrapped in an APC sequence, + // so deduplication works regardless of the --no-timestamps flag. + content = stripTimestamps(content) lines := strings.Split(content, "\n") result := make([]string, 0, len(lines)) @@ -112,22 +121,21 @@ func formatForLLM(content string) string { } for _, line := range lines { - if hasPrev && line == prevLine { + // Collapse carriage-return redraws (progress bars, spinners): keep only + // the final segment that would actually be visible in a terminal. + if idx := strings.LastIndex(line, "\r"); idx >= 0 { + line = line[idx+1:] + } + + // Deduplicate consecutive identical lines, but never collapse blank lines. + if line != "" && hasPrev && line == prevLine { repeatCount++ continue } flush() - processed := line - for _, prefix := range []string{"---", "+++", "~~~"} { - if strings.HasPrefix(line, prefix) { - processed = "\n=== PHASE: " + strings.TrimPrefix(line, prefix) + " ===" - break - } - } - - result = append(result, processed) + result = append(result, transformHeader(line)) prevLine = line hasPrev = true } @@ -136,3 +144,15 @@ func formatForLLM(content string) string { return strings.Join(result, "\n") } + +// transformHeader rewrites Buildkite log group markers (`---`, `+++`, `~~~`) +// into clear phase boundaries for an LLM. The marker must be a standalone token +// or followed by a space so that separators like `----------` are left intact. +func transformHeader(line string) string { + for _, prefix := range []string{"---", "+++", "~~~"} { + if line == prefix || strings.HasPrefix(line, prefix+" ") { + return "\n=== PHASE: " + strings.TrimPrefix(line, prefix) + " ===" + } + } + return line +} diff --git a/cmd/job/log_test.go b/cmd/job/log_test.go new file mode 100644 index 00000000..197b81c1 --- /dev/null +++ b/cmd/job/log_test.go @@ -0,0 +1,91 @@ +package job + +import "testing" + +func TestFormatForLLM(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want string + }{ + { + name: "empty", + input: "", + want: "", + }, + { + name: "strips ANSI color codes", + input: "\x1b[31mred\x1b[0m text", + want: "red text", + }, + { + name: "strips APC timestamp sequences", + input: "\x1b_bk;t=1700000000000\x07hello", + want: "hello", + }, + { + name: "strips bare timestamp markers", + input: "bk;t=1700000000000\x07hello", + want: "hello", + }, + { + name: "deduplicates consecutive identical lines", + input: "loop\nloop\nloop\ndone", + want: "loop\n[Previous line repeated 2 times]\ndone", + }, + { + name: "deduplicates run at end of input", + input: "start\nloop\nloop\nloop", + want: "start\nloop\n[Previous line repeated 2 times]", + }, + { + name: "does not deduplicate blank lines", + input: "a\n\n\n\nb", + want: "a\n\n\n\nb", + }, + { + name: "does not deduplicate non-adjacent duplicates", + input: "a\nb\na", + want: "a\nb\na", + }, + { + name: "collapses carriage-return redraws", + input: "10%\r50%\r100%", + want: "100%", + }, + { + name: "rewrites group markers into phase headers", + input: "--- Running tests", + want: "\n=== PHASE: Running tests ===", + }, + { + name: "rewrites plus and tilde markers", + input: "+++ Failed\n~~~ Cleanup", + want: "\n=== PHASE: Failed ===\n\n=== PHASE: Cleanup ===", + }, + { + name: "leaves separator-like lines intact", + input: "----------", + want: "----------", + }, + { + name: "standalone marker becomes header", + input: "---", + want: "\n=== PHASE: ===", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := formatForLLM(tt.input) + if got != tt.want { + t.Errorf("formatForLLM(%q) =\n%q\nwant\n%q", tt.input, got, tt.want) + } + }) + } +} From f7e2bb06ba69698039ddff33d8e2f0701ccce18a Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:24:30 +1000 Subject: [PATCH 5/9] chore(fmt): fix carriage returns format --- cmd/job/log.go | 6 ++++-- cmd/job/log_test.go | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/cmd/job/log.go b/cmd/job/log.go index 0293089a..b0db5e9d 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -121,8 +121,10 @@ func formatForLLM(content string) string { } for _, line := range lines { - // Collapse carriage-return redraws (progress bars, spinners): keep only - // the final segment that would actually be visible in a terminal. + // Normalise CRLF line endings, then collapse carriage-return redraws + // (progress bars, spinners) by keeping only the final segment that would + // actually be visible in a terminal. + line = strings.TrimRight(line, "\r") if idx := strings.LastIndex(line, "\r"); idx >= 0 { line = line[idx+1:] } diff --git a/cmd/job/log_test.go b/cmd/job/log_test.go index 197b81c1..244f614a 100644 --- a/cmd/job/log_test.go +++ b/cmd/job/log_test.go @@ -55,6 +55,16 @@ func TestFormatForLLM(t *testing.T) { input: "10%\r50%\r100%", want: "100%", }, + { + name: "trims trailing CR from CRLF line endings", + input: "hello\r\nworld\r\n", + want: "hello\nworld\n", + }, + { + name: "collapses redraws with trailing CRs", + input: "10%\r50%\r100% done\r\r", + want: "100% done", + }, { name: "rewrites group markers into phase headers", input: "--- Running tests", From a3c3ff77fe64b6c68053ad318a43d5e324e2cc8f Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:40:30 +1000 Subject: [PATCH 6/9] refactor(output): extend ansi for --no-timestamps --- cmd/job/log.go | 5 ++++- cmd/job/log_test.go | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/cmd/job/log.go b/cmd/job/log.go index b0db5e9d..e6708802 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -86,7 +86,10 @@ func (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { return nil } -var timestampRegex = regexp.MustCompile(`bk;t=\d+\x07`) +// timestampRegex matches Buildkite's inline timestamp markers, including the +// optional APC introducer (`\x1b_`) so the whole sequence is removed rather than +// leaving a dangling escape byte behind. +var timestampRegex = regexp.MustCompile(`(?:\x1b_)?bk;t=\d+\x07`) var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) diff --git a/cmd/job/log_test.go b/cmd/job/log_test.go index 244f614a..9b57fa0e 100644 --- a/cmd/job/log_test.go +++ b/cmd/job/log_test.go @@ -30,6 +30,11 @@ func TestFormatForLLM(t *testing.T) { input: "bk;t=1700000000000\x07hello", want: "hello", }, + { + name: "handles header after timestamp marker", + input: "\x1b_bk;t=1700000000000\x07~~~ Preparing secrets\r", + want: "\n=== PHASE: Preparing secrets ===", + }, { name: "deduplicates consecutive identical lines", input: "loop\nloop\nloop\ndone", @@ -99,3 +104,12 @@ func TestFormatForLLM(t *testing.T) { }) } } + +func TestStripTimestamps(t *testing.T) { + t.Parallel() + + in := "\x1b_bk;t=1700000000000\x07hello" + if got := stripTimestamps(in); got != "hello" { + t.Errorf("stripTimestamps(%q) = %q, want %q", in, got, "hello") + } +} From 2ed3f430b4991a5ddb89348b60e8df33b6dfabbc Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Fri, 19 Jun 2026 15:51:30 +1000 Subject: [PATCH 7/9] chore(fmt): remove additional whitespace --- cmd/job/log.go | 9 +++++++-- cmd/job/log_test.go | 13 +++++++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/cmd/job/log.go b/cmd/job/log.go index e6708802..140a970d 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -131,6 +131,8 @@ func formatForLLM(content string) string { if idx := strings.LastIndex(line, "\r"); idx >= 0 { line = line[idx+1:] } + // Drop trailing whitespace; it carries no information for an LLM. + line = strings.TrimRight(line, " \t") // Deduplicate consecutive identical lines, but never collapse blank lines. if line != "" && hasPrev && line == prevLine { @@ -147,7 +149,9 @@ func formatForLLM(content string) string { flush() - return strings.Join(result, "\n") + // Trim the single leading newline introduced when the log starts with a + // phase header, so the output doesn't begin with a blank line. + return strings.TrimPrefix(strings.Join(result, "\n"), "\n") } // transformHeader rewrites Buildkite log group markers (`---`, `+++`, `~~~`) @@ -156,7 +160,8 @@ func formatForLLM(content string) string { func transformHeader(line string) string { for _, prefix := range []string{"---", "+++", "~~~"} { if line == prefix || strings.HasPrefix(line, prefix+" ") { - return "\n=== PHASE: " + strings.TrimPrefix(line, prefix) + " ===" + title := strings.TrimSpace(strings.TrimPrefix(line, prefix)) + return "\n=== PHASE: " + title + " ===" } } return line diff --git a/cmd/job/log_test.go b/cmd/job/log_test.go index 9b57fa0e..30eb7058 100644 --- a/cmd/job/log_test.go +++ b/cmd/job/log_test.go @@ -33,7 +33,12 @@ func TestFormatForLLM(t *testing.T) { { name: "handles header after timestamp marker", input: "\x1b_bk;t=1700000000000\x07~~~ Preparing secrets\r", - want: "\n=== PHASE: Preparing secrets ===", + want: "=== PHASE: Preparing secrets ===", + }, + { + name: "trims trailing whitespace", + input: "hello \nworld\t", + want: "hello\nworld", }, { name: "deduplicates consecutive identical lines", @@ -73,12 +78,12 @@ func TestFormatForLLM(t *testing.T) { { name: "rewrites group markers into phase headers", input: "--- Running tests", - want: "\n=== PHASE: Running tests ===", + want: "=== PHASE: Running tests ===", }, { name: "rewrites plus and tilde markers", input: "+++ Failed\n~~~ Cleanup", - want: "\n=== PHASE: Failed ===\n\n=== PHASE: Cleanup ===", + want: "=== PHASE: Failed ===\n\n=== PHASE: Cleanup ===", }, { name: "leaves separator-like lines intact", @@ -88,7 +93,7 @@ func TestFormatForLLM(t *testing.T) { { name: "standalone marker becomes header", input: "---", - want: "\n=== PHASE: ===", + want: "=== PHASE: ===", }, } From e5f98394b0095a74e6f5ede72a675490245fe3f0 Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Thu, 25 Jun 2026 12:04:20 +1000 Subject: [PATCH 8/9] chore(logic): use package for llm output --- cmd/job/log.go | 89 ++++++------------------------------ cmd/job/log_test.go | 108 -------------------------------------------- go.mod | 1 + go.sum | 2 + 4 files changed, 18 insertions(+), 182 deletions(-) diff --git a/cmd/job/log.go b/cmd/job/log.go index 140a970d..f21cc771 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -4,13 +4,13 @@ import ( "context" "fmt" "regexp" - "strings" "github.com/alecthomas/kong" "github.com/buildkite/cli/v3/internal/cli" bkIO "github.com/buildkite/cli/v3/internal/io" "github.com/buildkite/cli/v3/pkg/cmd/factory" "github.com/buildkite/cli/v3/pkg/cmd/validation" + "github.com/mcncl/terminal-to-llm/digest" ) type LogCmd struct { @@ -19,6 +19,9 @@ type LogCmd struct { BuildNumber string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"b"` NoTimestamps bool `help:"Strip timestamp prefixes from log output" name:"no-timestamps"` LLMOptimized bool `help:"Format output to be optimal for LLM consumption (strips ANSI, deduplicates loops)" name:"agent"` + Format string `help:"Output rendering for --agent: plain or markdown" name:"format" enum:"plain,markdown" default:"plain"` + MaxTokens int `help:"Hard ceiling on the estimated token count of --agent output (0 = unlimited)" name:"max-tokens"` + NoWindow bool `help:"Disable failure-focused windowing in --agent output (keep all lines)" name:"no-window"` } func (c *LogCmd) Help() string { @@ -29,6 +32,12 @@ Examples: # Strip timestamp prefixes from output $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 --no-timestamps + + # Format for LLM consumption + $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 --agent + + # Format for LLM as markdown, capped at 2000 tokens, keeping all lines + $ bk job log 0190046e-e199-453b-a302-a21a4d649d31 --agent --format markdown --max-tokens 2000 --no-window ` } @@ -76,7 +85,11 @@ func (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { } if c.LLMOptimized { - logContent = formatForLLM(logContent) + opt := digest.Default() + opt.Format = digest.ParseFormat(c.Format) + opt.MaxTokens = c.MaxTokens + opt.Window = !c.NoWindow + logContent = digest.Process([]byte(logContent), opt) } writer, cleanup := bkIO.Pager(f.NoPager) @@ -91,78 +104,6 @@ func (c *LogCmd) Run(kongCtx *kong.Context, globals cli.GlobalFlags) error { // leaving a dangling escape byte behind. var timestampRegex = regexp.MustCompile(`(?:\x1b_)?bk;t=\d+\x07`) -var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) - -// oscRegex matches OSC (`\x1b]...`) and APC (`\x1b_...`) escape sequences -// terminated by BEL (\x07) or ST (\x1b\\). Buildkite uses APC sequences for its -// inline timestamp metadata (e.g. `\x1b_bk;t=1700000000000\x07`). -var oscRegex = regexp.MustCompile("\x1b[\\]_][^\x07]*(?:\x07|\x1b\\\\)") - func stripTimestamps(content string) string { return timestampRegex.ReplaceAllString(content, "") } - -func formatForLLM(content string) string { - content = ansiRegex.ReplaceAllString(content, "") - content = oscRegex.ReplaceAllString(content, "") - // Strip any bare timestamp markers that weren't wrapped in an APC sequence, - // so deduplication works regardless of the --no-timestamps flag. - content = stripTimestamps(content) - - lines := strings.Split(content, "\n") - result := make([]string, 0, len(lines)) - - var prevLine string - hasPrev := false - repeatCount := 0 - - flush := func() { - if repeatCount > 0 { - result = append(result, fmt.Sprintf("[Previous line repeated %d times]", repeatCount)) - repeatCount = 0 - } - } - - for _, line := range lines { - // Normalise CRLF line endings, then collapse carriage-return redraws - // (progress bars, spinners) by keeping only the final segment that would - // actually be visible in a terminal. - line = strings.TrimRight(line, "\r") - if idx := strings.LastIndex(line, "\r"); idx >= 0 { - line = line[idx+1:] - } - // Drop trailing whitespace; it carries no information for an LLM. - line = strings.TrimRight(line, " \t") - - // Deduplicate consecutive identical lines, but never collapse blank lines. - if line != "" && hasPrev && line == prevLine { - repeatCount++ - continue - } - - flush() - - result = append(result, transformHeader(line)) - prevLine = line - hasPrev = true - } - - flush() - - // Trim the single leading newline introduced when the log starts with a - // phase header, so the output doesn't begin with a blank line. - return strings.TrimPrefix(strings.Join(result, "\n"), "\n") -} - -// transformHeader rewrites Buildkite log group markers (`---`, `+++`, `~~~`) -// into clear phase boundaries for an LLM. The marker must be a standalone token -// or followed by a space so that separators like `----------` are left intact. -func transformHeader(line string) string { - for _, prefix := range []string{"---", "+++", "~~~"} { - if line == prefix || strings.HasPrefix(line, prefix+" ") { - title := strings.TrimSpace(strings.TrimPrefix(line, prefix)) - return "\n=== PHASE: " + title + " ===" - } - } - return line -} diff --git a/cmd/job/log_test.go b/cmd/job/log_test.go index 30eb7058..d320ae96 100644 --- a/cmd/job/log_test.go +++ b/cmd/job/log_test.go @@ -2,114 +2,6 @@ package job import "testing" -func TestFormatForLLM(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input string - want string - }{ - { - name: "empty", - input: "", - want: "", - }, - { - name: "strips ANSI color codes", - input: "\x1b[31mred\x1b[0m text", - want: "red text", - }, - { - name: "strips APC timestamp sequences", - input: "\x1b_bk;t=1700000000000\x07hello", - want: "hello", - }, - { - name: "strips bare timestamp markers", - input: "bk;t=1700000000000\x07hello", - want: "hello", - }, - { - name: "handles header after timestamp marker", - input: "\x1b_bk;t=1700000000000\x07~~~ Preparing secrets\r", - want: "=== PHASE: Preparing secrets ===", - }, - { - name: "trims trailing whitespace", - input: "hello \nworld\t", - want: "hello\nworld", - }, - { - name: "deduplicates consecutive identical lines", - input: "loop\nloop\nloop\ndone", - want: "loop\n[Previous line repeated 2 times]\ndone", - }, - { - name: "deduplicates run at end of input", - input: "start\nloop\nloop\nloop", - want: "start\nloop\n[Previous line repeated 2 times]", - }, - { - name: "does not deduplicate blank lines", - input: "a\n\n\n\nb", - want: "a\n\n\n\nb", - }, - { - name: "does not deduplicate non-adjacent duplicates", - input: "a\nb\na", - want: "a\nb\na", - }, - { - name: "collapses carriage-return redraws", - input: "10%\r50%\r100%", - want: "100%", - }, - { - name: "trims trailing CR from CRLF line endings", - input: "hello\r\nworld\r\n", - want: "hello\nworld\n", - }, - { - name: "collapses redraws with trailing CRs", - input: "10%\r50%\r100% done\r\r", - want: "100% done", - }, - { - name: "rewrites group markers into phase headers", - input: "--- Running tests", - want: "=== PHASE: Running tests ===", - }, - { - name: "rewrites plus and tilde markers", - input: "+++ Failed\n~~~ Cleanup", - want: "=== PHASE: Failed ===\n\n=== PHASE: Cleanup ===", - }, - { - name: "leaves separator-like lines intact", - input: "----------", - want: "----------", - }, - { - name: "standalone marker becomes header", - input: "---", - want: "=== PHASE: ===", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got := formatForLLM(tt.input) - if got != tt.want { - t.Errorf("formatForLLM(%q) =\n%q\nwant\n%q", tt.input, got, tt.want) - } - }) - } -} - func TestStripTimestamps(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index 053a99f6..3d2df9fc 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/go-git/go-git/v5 v5.19.1 github.com/goccy/go-yaml v1.19.2 github.com/google/uuid v1.6.0 + github.com/mcncl/terminal-to-llm v0.0.0-20260625015351-3819cf9f5f9b github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/posthog/posthog-go v1.16.1 github.com/vektah/gqlparser/v2 v2.5.35 diff --git a/go.sum b/go.sum index 56054d51..8d5963e5 100644 --- a/go.sum +++ b/go.sum @@ -126,6 +126,8 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mcncl/terminal-to-llm v0.0.0-20260625015351-3819cf9f5f9b h1:EY/R1QdVIzyKIYx+EPk/I3jq7W2HTHeMOFGA6Rr1Eps= +github.com/mcncl/terminal-to-llm v0.0.0-20260625015351-3819cf9f5f9b/go.mod h1:+zIpLjV+/ByEl/BRo9rxZdpZAxxQAG0yZg4kxW+hOmM= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= From 6f1166c907e6364fc72ceb4b31805d19ec2a87bc Mon Sep 17 00:00:00 2001 From: Ben McNicholl Date: Thu, 25 Jun 2026 12:18:49 +1000 Subject: [PATCH 9/9] chore(options): add an `llm` alias for `--agent` --- cmd/job/log.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/job/log.go b/cmd/job/log.go index f21cc771..d0e2df57 100644 --- a/cmd/job/log.go +++ b/cmd/job/log.go @@ -18,7 +18,7 @@ type LogCmd struct { Pipeline string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"p"` BuildNumber string `help:"Deprecated; ignored because job UUIDs no longer require pipeline or build context" short:"b"` NoTimestamps bool `help:"Strip timestamp prefixes from log output" name:"no-timestamps"` - LLMOptimized bool `help:"Format output to be optimal for LLM consumption (strips ANSI, deduplicates loops)" name:"agent"` + LLMOptimized bool `help:"Format output to be optimal for LLM consumption (strips ANSI, deduplicates loops)" name:"agent" aliases:"llm"` Format string `help:"Output rendering for --agent: plain or markdown" name:"format" enum:"plain,markdown" default:"plain"` MaxTokens int `help:"Hard ceiling on the estimated token count of --agent output (0 = unlimited)" name:"max-tokens"` NoWindow bool `help:"Disable failure-focused windowing in --agent output (keep all lines)" name:"no-window"`