Skip to content

Commit 060fdf2

Browse files
committed
[project-#175] fix: exclude gobby and git from gsqz
1 parent 9912229 commit 060fdf2

8 files changed

Lines changed: 162 additions & 124 deletions

File tree

crates/gsqz/README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ AI coding assistants run shell commands and dump the full output into the contex
2525

2626
## The Fix
2727

28-
gsqz wraps your shell commands and compresses their output using pattern-matched pipelines. It knows how to summarize `git status`, collapse test output, group lint errors by rule, and truncate walls of text — all configured in plain YAML.
28+
gsqz wraps your shell commands and compresses their output using pattern-matched pipelines. It knows how to collapse test output, group lint errors by rule, summarize builds, and truncate walls of text — all configured in plain YAML.
2929

3030
```text
3131
$ gsqz -- cargo test
@@ -91,7 +91,6 @@ gsqz is installed automatically as part of the [Gobby](https://github.com/GobbyA
9191

9292
```bash
9393
# Compress command output (backward-compatible form)
94-
gsqz -- git status
9594
gsqz -- cargo test
9695
gsqz -- npm run lint
9796

@@ -167,9 +166,6 @@ pipelines:
167166
168167
| Pipeline | Matches | What it does |
169168
|----------|---------|-------------|
170-
| `git-status` | `git status` | Groups by status code (Modified, Added, Untracked...) |
171-
| `git-diff` | `git diff` | Per-file section truncation |
172-
| `git-log` | `git log` | Head + tail with omission marker |
173169
| `pytest` | `pytest`, `uv run pytest` | Extracts failures + summary |
174170
| `cargo-test` | `cargo test` | Extracts failures + summary |
175171
| `generic-test` | `npm test`, `go test`, etc. | Failure grouping |
@@ -179,6 +175,8 @@ pipelines:
179175

180176
...and 10+ more. Run `gsqz --dump-config` to see the full list.
181177

178+
Gobby-owned CLIs (`gobby`, `gobby-cli`, `gcode`, `ghook`, `gloc`, `gsqz`) and `git` are always surfaced raw with no compression header and no savings report.
179+
182180
### Step reference
183181

184182
| Step | Parameters | Description |

crates/gsqz/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: gsqz
33
description: >
44
Output compressor for LLM token optimization. Wraps shell commands and compresses
55
verbose output by 70-95% using pattern-matched pipelines. Use when running tests,
6-
builds, git operations, linting, or any command that produces more output than an
6+
builds, linting, or any command that produces more output than an
77
LLM needs. Triggers on: compress output, reduce tokens, verbose output, test output,
88
build output, token savings, save context, shrink output.
99
---
@@ -18,7 +18,6 @@ Prefix any command with `gsqz --`:
1818

1919
```bash
2020
gsqz -- cargo test
21-
gsqz -- git status
2221
gsqz -- npm run lint
2322
```
2423

@@ -30,7 +29,6 @@ gsqz auto-detects the command and applies the right compression:
3029

3130
| Category | Commands |
3231
|---|---|
33-
| Git | status, diff, log, push/pull/fetch/clone, add/commit/stash |
3432
| Tests | pytest, cargo test, npm test, vitest, jest, mocha, go test |
3533
| Linters | ruff, mypy, pylint, eslint, tsc, biome, golangci-lint, staticcheck |
3634
| Build | cargo build, go build, next build, webpack, make |
@@ -40,6 +38,8 @@ gsqz auto-detects the command and applies the right compression:
4038
| GitHub | gh pr/issue list/view |
4139
| Fallback | Any unmatched command: keeps first 20 + last 20 lines |
4240

41+
Gobby-owned CLIs (`gobby`, `gobby-cli`, `gcode`, `ghook`, `gloc`, `gsqz`) and `git` are built-in exclusions. gsqz surfaces them raw.
42+
4343
## When NOT to Use
4444

4545
- **Interactive commands** — gsqz captures all output, breaking interactivity

crates/gsqz/config.yaml

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -15,60 +15,6 @@ settings:
1515
on_empty: 'No output after compression.'
1616

1717
pipelines:
18-
# === Git ===
19-
git-status:
20-
match: '\bgit\s+status\b'
21-
steps:
22-
- filter_lines:
23-
patterns:
24-
- '^\s*$'
25-
- '^On branch '
26-
- '^Your branch is '
27-
- '^ \(use "git '
28-
- '^Changes not staged'
29-
- '^Changes to be committed'
30-
- '^Untracked files:'
31-
- '^no changes added'
32-
- group_lines:
33-
mode: git_status
34-
35-
git-diff:
36-
match: '\bgit\s+diff\b'
37-
steps:
38-
- filter_lines:
39-
patterns:
40-
- '^index [0-9a-f]'
41-
- group_lines:
42-
mode: git_diff
43-
44-
git-log:
45-
match: '\bgit\s+log\b'
46-
steps:
47-
- filter_lines:
48-
patterns:
49-
- '^Merge:'
50-
- '^\s*$'
51-
52-
git-transfer:
53-
match: '\bgit\s+(?:push|pull|fetch|clone)\b'
54-
steps:
55-
- filter_lines:
56-
patterns:
57-
- '^remote: Counting'
58-
- '^remote: Compressing'
59-
- '^remote: Total'
60-
- '^Receiving objects:'
61-
- '^Resolving deltas:'
62-
- '^Unpacking objects:'
63-
- '^remote: Enumerating'
64-
65-
git-mutation:
66-
match: '\bgit\s+(?:add|commit|stash|tag|branch)\b'
67-
steps:
68-
- filter_lines:
69-
patterns:
70-
- '^\s*$'
71-
7218
# === Test runners ===
7319
pytest:
7420
match: '\b(?:pytest|py\.test)\b'
@@ -274,4 +220,6 @@ fallback:
274220
head: 20
275221
tail: 20
276222

223+
# These are additive. Gobby-owned CLIs and git are always excluded in code:
224+
# gobby, gobby-cli, gcode, ghook, gloc, gsqz, git.
277225
excluded_commands: []

crates/gsqz/src/compressor.rs

Lines changed: 99 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use regex::Regex;
22

33
use crate::command_split;
4-
use crate::config::{Config, Step};
4+
use crate::config::{BUILTIN_EXCLUDED_COMMANDS, Config, Step};
55
use crate::primitives::{dedup, filter, group, match_output, prose, replace, truncate};
66

77
pub struct CompressionResult {
@@ -33,6 +33,20 @@ impl CompressionResult {
3333
}
3434
}
3535

36+
fn first_command_token(segment: &str) -> Option<&str> {
37+
segment
38+
.split_whitespace()
39+
.find(|token| !token.contains('='))
40+
}
41+
42+
fn command_basename(token: &str) -> &str {
43+
token
44+
.trim_matches(|c| c == '\'' || c == '"')
45+
.rsplit(['/', '\\'])
46+
.next()
47+
.unwrap_or(token)
48+
}
49+
3650
struct CompiledPipeline {
3751
name: String,
3852
regex: Regex,
@@ -82,26 +96,36 @@ impl Compressor {
8296
}
8397
}
8498

99+
pub fn command_is_excluded(&self, command: &str) -> bool {
100+
command_split::split_compound(command)
101+
.iter()
102+
.filter_map(|segment| first_command_token(segment))
103+
.map(command_basename)
104+
.any(|name| BUILTIN_EXCLUDED_COMMANDS.contains(&name))
105+
|| self.excluded.iter().any(|r| r.is_match(command))
106+
}
107+
85108
pub fn compress(&self, command: &str, output: &str) -> CompressionResult {
86109
let original_chars = output.len();
87110

88-
// Skip if too short
89-
if original_chars < self.min_length {
111+
// Built-in and configured exclusions are raw passthrough. Check before
112+
// length so excluded commands are always identified as `excluded`.
113+
if self.command_is_excluded(command) {
90114
return CompressionResult {
91115
compressed: output.to_string(),
92116
original_chars,
93117
compressed_chars: original_chars,
94-
strategy_name: "passthrough".into(),
118+
strategy_name: "excluded".into(),
95119
};
96120
}
97121

98-
// Skip excluded commands
99-
if self.excluded.iter().any(|r| r.is_match(command)) {
122+
// Skip if too short
123+
if original_chars < self.min_length {
100124
return CompressionResult {
101125
compressed: output.to_string(),
102126
original_chars,
103127
compressed_chars: original_chars,
104-
strategy_name: "excluded".into(),
128+
strategy_name: "passthrough".into(),
105129
};
106130
}
107131

@@ -315,7 +339,7 @@ mod tests {
315339
}
316340

317341
#[test]
318-
fn test_git_status_pipeline() {
342+
fn test_git_status_is_excluded() {
319343
let compressor = Compressor::new(&test_config());
320344
let mut lines = Vec::new();
321345
for i in 0..100 {
@@ -326,9 +350,64 @@ mod tests {
326350
}
327351
let output = lines.join("");
328352
let result = compressor.compress("git status", &output);
329-
assert_eq!(result.strategy_name, "git-status");
330-
assert!(result.compressed.contains("Modified"));
331-
assert!(result.compressed.contains("Untracked"));
353+
assert_eq!(result.strategy_name, "excluded");
354+
assert_eq!(result.compressed, output);
355+
assert_eq!(result.compressed_chars, result.original_chars);
356+
assert!(result.is_passthrough());
357+
assert!(!result.compressed.contains("[gsqz:"));
358+
}
359+
360+
#[test]
361+
fn test_builtin_excluded_commands_are_raw_passthrough() {
362+
let compressor = Compressor::new(&test_config());
363+
let output = (0..120)
364+
.map(|i| format!("structured output line {}\n", i))
365+
.collect::<String>();
366+
367+
for command in [
368+
"gobby status",
369+
"gobby-cli status",
370+
"gcode search symbol",
371+
"ghook --diagnose",
372+
"gloc prompt",
373+
"gsqz --stats -- cargo test",
374+
"git diff",
375+
] {
376+
let result = compressor.compress(command, &output);
377+
assert_eq!(result.strategy_name, "excluded", "{command}");
378+
assert_eq!(result.compressed, output, "{command}");
379+
assert_eq!(result.compressed_chars, result.original_chars, "{command}");
380+
assert!(result.is_passthrough(), "{command}");
381+
}
382+
}
383+
384+
#[test]
385+
fn test_builtin_exclusion_runs_before_min_length() {
386+
let compressor = Compressor::new(&test_config());
387+
let result = compressor.compress("gcode search symbol", "ok");
388+
assert_eq!(result.strategy_name, "excluded");
389+
assert_eq!(result.compressed, "ok");
390+
}
391+
392+
#[test]
393+
fn test_builtin_exclusion_matches_compound_segments() {
394+
let compressor = Compressor::new(&test_config());
395+
let output = (0..120)
396+
.map(|i| format!("line {}\n", i))
397+
.collect::<String>();
398+
399+
let result = compressor.compress("cargo test && git status", &output);
400+
assert_eq!(result.strategy_name, "excluded");
401+
402+
let result = compressor.compress("rg git README.md", &output);
403+
assert_ne!(result.strategy_name, "excluded");
404+
}
405+
406+
#[test]
407+
fn test_builtin_exclusion_matches_binary_paths() {
408+
let compressor = Compressor::new(&test_config());
409+
let result = compressor.compress("/Users/josh/.gobby/bin/gcode search", "ok");
410+
assert_eq!(result.strategy_name, "excluded");
332411
}
333412

334413
#[test]
@@ -507,10 +586,10 @@ mod tests {
507586
// Pure passthrough cases — main.rs surfaces output verbatim.
508587
assert!(mk("passthrough").is_passthrough());
509588
assert!(mk("excluded").is_passthrough());
510-
assert!(mk("git-mutation/no-op").is_passthrough());
589+
assert!(mk("pytest/no-op").is_passthrough());
511590
assert!(mk("cargo-test/no-op").is_passthrough());
512591
// Real compression — main.rs prepends the header and reports to daemon.
513-
assert!(!mk("git-status").is_passthrough());
592+
assert!(!mk("pytest").is_passthrough());
514593
assert!(!mk("cargo-test/low-savings").is_passthrough());
515594
assert!(!mk("pytest/on_empty").is_passthrough());
516595
assert!(!mk("fallback").is_passthrough());
@@ -556,10 +635,12 @@ mod tests {
556635
fn test_compound_falls_back_to_earlier_segment() {
557636
let compressor = Compressor::new(&test_config());
558637
// Last segment doesn't match any pipeline, first does
559-
let output = (0..200)
560-
.map(|i| format!(" M src/file_{}.rs\n", i))
561-
.collect::<String>();
562-
let result = compressor.compress("git status && unknown-cmd", &output);
563-
assert_eq!(result.strategy_name, "git-status");
638+
let mut lines: Vec<String> = (0..100)
639+
.map(|i| format!("test test_{} ... ok\n", i))
640+
.collect();
641+
lines.push("test result: ok. 100 passed; 0 failed\n".into());
642+
let output = lines.join("");
643+
let result = compressor.compress("cargo test && unknown-cmd", &output);
644+
assert_eq!(result.strategy_name, "cargo-test");
564645
}
565646
}

crates/gsqz/src/config.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ const DEFAULT_CONFIG: &str = include_str!("../config.yaml");
99
/// The raw compiled-in config YAML, for `--init` to write to disk.
1010
pub const DEFAULT_CONFIG_YAML: &str = DEFAULT_CONFIG;
1111

12+
pub const BUILTIN_EXCLUDED_COMMANDS: &[&str] = &[
13+
"gobby",
14+
"gobby-cli",
15+
"gcode",
16+
"ghook",
17+
"gloc",
18+
"gsqz",
19+
"git",
20+
];
21+
1222
#[derive(Debug, Clone, Deserialize)]
1323
pub struct Config {
1424
#[serde(default)]
@@ -333,6 +343,10 @@ impl Config {
333343
pipeline.steps.len()
334344
));
335345
}
346+
out.push_str(&format!(
347+
"\nbuiltin_excluded_commands: {:?}\n",
348+
BUILTIN_EXCLUDED_COMMANDS
349+
));
336350
if !self.excluded_commands.is_empty() {
337351
out.push_str(&format!(
338352
"\nexcluded_commands: {:?}\n",
@@ -358,17 +372,16 @@ mod tests {
358372
#[test]
359373
fn test_default_config_has_expected_pipelines() {
360374
let config = Config::builtin();
361-
assert!(config.pipelines.contains_key("git-status"));
362375
assert!(config.pipelines.contains_key("pytest"));
363376
assert!(config.pipelines.contains_key("cargo-test"));
364377
}
365378

366379
#[test]
367380
fn test_pipeline_has_match_and_steps() {
368381
let config = Config::builtin();
369-
let git_status = config.pipelines.get("git-status").unwrap();
370-
assert!(!git_status.match_pattern.is_empty());
371-
assert!(!git_status.steps.is_empty());
382+
let pytest = config.pipelines.get("pytest").unwrap();
383+
assert!(!pytest.match_pattern.is_empty());
384+
assert!(!pytest.steps.is_empty());
372385
}
373386

374387
#[test]
@@ -505,6 +518,7 @@ mod tests {
505518
assert!(dump.contains("min_output_length: 1000"));
506519
assert!(dump.contains("max_compressed_lines: 100"));
507520
assert!(dump.contains("pipelines:"));
521+
assert!(dump.contains("builtin_excluded_commands:"));
508522
}
509523

510524
#[test]

0 commit comments

Comments
 (0)