Skip to content
This repository was archived by the owner on Apr 11, 2026. It is now read-only.

Commit 00e8773

Browse files
z23ccclaude
andcommitted
feat(flowctl): add compress filters for show/guard/cat and strip more compact fields
Wire show, guard, and cat commands through the TOML compress pipeline (pretty_output) so they benefit from automatic token optimization in non-TTY mode (Claude Code). Add three new TOML filter definitions with inline tests. Expand COMPACT_STRIP to remove 8 additional JSON fields that LLM consumers rarely need (branch_name, review statuses, defaults). - show: strips slug prefixes, spec paths, shortens status labels - guard: short-circuits to "✓ guard passed" on all-pass, limits failures to 30 lines - cat: truncates long specs to head 30 + tail 5 lines - compact JSON: strips branch_name, plan/completion review status/timestamps, defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d536c22 commit 00e8773

7 files changed

Lines changed: 272 additions & 21 deletions

File tree

flowctl/crates/flowctl-cli/src/commands/admin/guard.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::sync::OnceLock;
1111
use regex::Regex;
1212
use serde_json::json;
1313

14-
use crate::output::{error_exit, json_output};
14+
use crate::output::{error_exit, json_output, pretty_output};
1515

1616
use flowctl_core::types::CONFIG_FILE;
1717

@@ -409,6 +409,7 @@ pub fn cmd_guard(json_mode: bool, layer: String) {
409409
let mut results: Vec<serde_json::Value> = Vec::new();
410410
let mut all_passed = true;
411411
let mut pass_count: usize = 0;
412+
let mut pretty_buf = String::new();
412413

413414
for (layer_name, cmd_type, cmd) in &commands {
414415
let output = Command::new("sh")
@@ -447,14 +448,14 @@ pub fn cmd_guard(json_mode: bool, layer: String) {
447448

448449
if !json_mode {
449450
let icon = if passed { "\u{2713}" } else { "\u{2717}" };
450-
println!(
451-
"{} [{}] {}: {}",
451+
pretty_buf.push_str(&format!(
452+
"{} [{}] {}: {}\n",
452453
icon, layer_name, cmd_type, filtered.summary
453-
);
454+
));
454455
// Show errors inline for failed guards
455456
for err in &filtered.errors {
456457
for err_line in err.lines().take(3) {
457-
println!(" {}", err_line);
458+
pretty_buf.push_str(&format!(" {}\n", err_line));
458459
}
459460
}
460461
}
@@ -465,7 +466,8 @@ pub fn cmd_guard(json_mode: bool, layer: String) {
465466
} else {
466467
let total = commands.len();
467468
let suffix = if all_passed { "" } else { " \u{2014} FAILED" };
468-
println!("\n{}/{} guards passed{}", pass_count, total, suffix);
469+
pretty_buf.push_str(&format!("\n{}/{} guards passed{}", pass_count, total, suffix));
470+
pretty_output("guard", &pretty_buf);
469471
}
470472

471473
if !all_passed {

flowctl/crates/flowctl-cli/src/commands/query.rs

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -198,19 +198,21 @@ pub fn cmd_show(json: bool, id: String) {
198198
result["tasks"] = json!(task_summaries);
199199
json_output(result);
200200
} else {
201-
println!("Epic: {}", epic.id);
202-
println!("Title: {}", epic.title);
203-
println!("Status: {}", epic.status);
204-
println!("Spec: .flow/specs/{}.md", epic.id);
205-
println!("\nTasks ({}):", tasks.len());
201+
let mut buf = String::new();
202+
buf.push_str(&format!("Epic: {}\n", epic.id));
203+
buf.push_str(&format!("Title: {}\n", epic.title));
204+
buf.push_str(&format!("Status: {}\n", epic.status));
205+
buf.push_str(&format!("Spec: .flow/specs/{}.md\n", epic.id));
206+
buf.push_str(&format!("\nTasks ({}):\n", tasks.len()));
206207
for t in &tasks {
207208
let deps = if t.depends_on.is_empty() {
208209
String::new()
209210
} else {
210211
format!(" (deps: {})", t.depends_on.join(", "))
211212
};
212-
println!(" [{}] {}: {}{}", t.status, t.id, t.title, deps);
213+
buf.push_str(&format!(" [{}] {}: {}{}\n", t.status, t.id, t.title, deps));
213214
}
215+
pretty_output("show", &buf);
214216
}
215217
} else if is_task_id(&id) {
216218
let task = match get_task(&flow_dir, &id) {
@@ -223,20 +225,22 @@ pub fn cmd_show(json: bool, id: String) {
223225
if json {
224226
json_output(task_to_json(&task));
225227
} else {
226-
println!("Task: {}", task.id);
227-
println!("Epic: {}", task.epic);
228-
println!("Title: {}", task.title);
229-
println!("Status: {}", task.status);
228+
let mut buf = String::new();
229+
buf.push_str(&format!("Task: {}\n", task.id));
230+
buf.push_str(&format!("Epic: {}\n", task.epic));
231+
buf.push_str(&format!("Title: {}\n", task.title));
232+
buf.push_str(&format!("Status: {}\n", task.status));
230233
if task.domain != flowctl_core::types::Domain::General {
231-
println!("Domain: {}", task.domain);
234+
buf.push_str(&format!("Domain: {}\n", task.domain));
232235
}
233236
let deps_str = if task.depends_on.is_empty() {
234237
"none".to_string()
235238
} else {
236239
task.depends_on.join(", ")
237240
};
238-
println!("Depends on: {}", deps_str);
239-
println!("Spec: .flow/tasks/{}.md", task.id);
241+
buf.push_str(&format!("Depends on: {}\n", deps_str));
242+
buf.push_str(&format!("Spec: .flow/tasks/{}.md\n", task.id));
243+
pretty_output("show", &buf);
240244
}
241245
} else {
242246
error_exit(&format!(
@@ -483,7 +487,7 @@ pub fn cmd_cat(id: String) {
483487
// Epic spec: still read from specs/ directory
484488
let spec_path = flow_dir.join(SPECS_DIR).join(format!("{}.md", id));
485489
match fs::read_to_string(&spec_path) {
486-
Ok(content) => print!("{}", content),
490+
Ok(content) => pretty_output("cat", &content),
487491
Err(_) => {
488492
error_exit(&format!("Spec not found: {}", spec_path.display()));
489493
}
@@ -495,7 +499,7 @@ pub fn cmd_cat(id: String) {
495499
if body.is_empty() {
496500
error_exit(&format!("Task spec not found: {}", id));
497501
}
498-
print!("{}", body);
502+
pretty_output("cat", &body);
499503
}
500504
Err(_) => {
501505
error_exit(&format!("Task not found: {}", id));

flowctl/crates/flowctl-cli/src/output.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ const COMPACT_STRIP: &[&str] = &[
1717
"spec_path",
1818
"file_path",
1919
"schema_version",
20+
"branch_name",
21+
"plan_review_status",
22+
"plan_reviewed_at",
23+
"completion_review_status",
24+
"completion_reviewed_at",
25+
"default_impl",
26+
"default_review",
27+
"default_sync",
2028
];
2129

2230
/// Shared output options (flattened into every subcommand via clap).

flowctl/crates/flowctl-core/src/compress.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ const FILES_TOML: &str = include_str!("filters/files.toml");
3838
const READY_TOML: &str = include_str!("filters/ready.toml");
3939
const HOOK_PRECOMPACT_TOML: &str = include_str!("filters/hook_precompact.toml");
4040
const HOOK_SUBAGENT_TOML: &str = include_str!("filters/hook_subagent.toml");
41+
const SHOW_TOML: &str = include_str!("filters/show.toml");
42+
const GUARD_TOML: &str = include_str!("filters/guard.toml");
43+
const CAT_TOML: &str = include_str!("filters/cat.toml");
4144

4245
// ---------------------------------------------------------------------------
4346
// Deserialization types (TOML schema)
@@ -269,6 +272,9 @@ const BUILTIN_SOURCES: &[(&str, &str)] = &[
269272
("ready", READY_TOML),
270273
("hook_precompact", HOOK_PRECOMPACT_TOML),
271274
("hook_subagent", HOOK_SUBAGENT_TOML),
275+
("show", SHOW_TOML),
276+
("guard", GUARD_TOML),
277+
("cat", CAT_TOML),
272278
];
273279

274280
fn load_registry() -> Vec<CompiledFilter> {
@@ -698,6 +704,9 @@ on_empty = "all clean"
698704
"ready",
699705
"hook_precompact",
700706
"hook_subagent",
707+
"show",
708+
"guard",
709+
"cat",
701710
] {
702711
assert!(
703712
reg.iter().any(|f| f.name == *expected),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
schema_version = 1
2+
3+
[filters.cat]
4+
description = "Compact flowctl cat output — truncate long specs to head+tail"
5+
match_command = "^flowctl\\s+cat"
6+
head_lines = 30
7+
tail_lines = 5
8+
max_lines = 50
9+
truncate_lines_at = 120
10+
11+
[[tests.cat]]
12+
name = "short spec passes through"
13+
input = """
14+
# Epic: Token Optimization
15+
16+
## Goal
17+
Reduce token waste in flowctl output.
18+
19+
## Tasks
20+
1. Add show filter
21+
2. Add guard filter
22+
3. Add cat filter
23+
"""
24+
expected = """
25+
# Epic: Token Optimization
26+
27+
## Goal
28+
Reduce token waste in flowctl output.
29+
30+
## Tasks
31+
1. Add show filter
32+
2. Add guard filter
33+
3. Add cat filter
34+
"""
35+
36+
[[tests.cat]]
37+
name = "long spec truncated"
38+
input = """
39+
line 1
40+
line 2
41+
line 3
42+
line 4
43+
line 5
44+
line 6
45+
line 7
46+
line 8
47+
line 9
48+
line 10
49+
line 11
50+
line 12
51+
line 13
52+
line 14
53+
line 15
54+
line 16
55+
line 17
56+
line 18
57+
line 19
58+
line 20
59+
line 21
60+
line 22
61+
line 23
62+
line 24
63+
line 25
64+
line 26
65+
line 27
66+
line 28
67+
line 29
68+
line 30
69+
line 31
70+
line 32
71+
line 33
72+
line 34
73+
line 35
74+
line 36
75+
line 37
76+
line 38
77+
line 39
78+
line 40
79+
"""
80+
expected = """
81+
line 1
82+
line 2
83+
line 3
84+
line 4
85+
line 5
86+
line 6
87+
line 7
88+
line 8
89+
line 9
90+
line 10
91+
line 11
92+
line 12
93+
line 13
94+
line 14
95+
line 15
96+
line 16
97+
line 17
98+
line 18
99+
line 19
100+
line 20
101+
line 21
102+
line 22
103+
line 23
104+
line 24
105+
line 25
106+
line 26
107+
line 27
108+
line 28
109+
line 29
110+
line 30
111+
... (5 lines omitted)
112+
line 36
113+
line 37
114+
line 38
115+
line 39
116+
line 40
117+
"""
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
schema_version = 1
2+
3+
[filters.guard]
4+
description = "Compact flowctl guard output — short-circuit on all-pass, truncate failures"
5+
match_command = "^flowctl\\s+guard"
6+
strip_ansi = true
7+
match_output = [
8+
# All guards passed — collapse to one-liner (skip if any ✗ failure present)
9+
{ pattern = "guards passed$", message = "✓ guard passed", unless = "" },
10+
]
11+
truncate_lines_at = 100
12+
max_lines = 30
13+
14+
[[tests.guard]]
15+
name = "all passed short-circuits"
16+
input = """
17+
✓ [backend] test: 255 passed, 2 ignored (11 suites, 4.28s)
18+
✓ [backend] lint: no issues
19+
✓ [backend] typecheck: no issues
20+
21+
3/3 guards passed
22+
"""
23+
expected = "✓ guard passed"
24+
25+
[[tests.guard]]
26+
name = "failure preserved"
27+
input = """
28+
✓ [backend] test: 255 passed, 2 ignored (11 suites, 4.28s)
29+
✗ [backend] lint: 3 issues
30+
error: unused variable `x`
31+
error: missing docs
32+
error: dead code
33+
34+
2/3 guards passed — FAILED
35+
"""
36+
expected = """
37+
✓ [backend] test: 255 passed, 2 ignored (11 suites, 4.28s)
38+
✗ [backend] lint: 3 issues
39+
error: unused variable `x`
40+
error: missing docs
41+
error: dead code
42+
43+
2/3 guards passed — FAILED
44+
"""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
schema_version = 1
2+
3+
[filters.show]
4+
description = "Compact flowctl show output — strip slug noise, shorten status, truncate"
5+
match_command = "^flowctl\\s+show"
6+
strip_lines_matching = [
7+
"^\\s*$",
8+
]
9+
replace = [
10+
# Strip epic slug prefix from dep lists and task IDs
11+
{ pattern = "\\bfn-\\d+(?:-[a-zA-Z0-9-]+)?(\\.\\d+)", replacement = "$1" },
12+
# Compact status labels
13+
{ pattern = "Status: in_progress", replacement = "Status: wip" },
14+
{ pattern = "Status: blocked", replacement = "Status: blk" },
15+
{ pattern = "Status: skipped", replacement = "Status: skip" },
16+
# Compact task lines inside epic show
17+
{ pattern = "^(\\s*)\\[in_progress\\]", replacement = "${1}[wip]" },
18+
{ pattern = "^(\\s*)\\[skipped\\]", replacement = "${1}[skip]" },
19+
{ pattern = "^(\\s*)\\[blocked\\]", replacement = "${1}[blk]" },
20+
# Strip "Spec: .flow/..." lines (LLM already knows spec paths)
21+
{ pattern = "^Spec: .flow/.*$", replacement = "" },
22+
]
23+
truncate_lines_at = 80
24+
max_lines = 40
25+
26+
[[tests.show]]
27+
name = "epic show compaction"
28+
input = """
29+
Epic: fn-21-improve-flowctl-code-quality
30+
Title: Improve flowctl code quality
31+
Status: open
32+
Spec: .flow/specs/fn-21-improve-flowctl-code-quality.md
33+
34+
Tasks (3):
35+
[done] fn-21-improve-flowctl-code-quality.1: Remove dead code [backend]
36+
[in_progress] fn-21-improve-flowctl-code-quality.2: Add clippy config [backend]
37+
[todo] fn-21-improve-flowctl-code-quality.3: Add pre-commit hook [backend]
38+
"""
39+
expected = """
40+
Epic: fn-21-improve-flowctl-code-quality
41+
Title: Improve flowctl code quality
42+
Status: open
43+
Tasks (3):
44+
[done] .1: Remove dead code [backend]
45+
[wip] .2: Add clippy config [backend]
46+
[todo] .3: Add pre-commit hook [backend]
47+
"""
48+
49+
[[tests.show]]
50+
name = "task show compaction"
51+
input = """
52+
Task: fn-21-improve-flowctl-code-quality.1
53+
Epic: fn-21-improve-flowctl-code-quality
54+
Title: Remove dead code in diagnostics.rs
55+
Status: done
56+
Domain: backend
57+
Depends on: none
58+
Spec: .flow/tasks/fn-21-improve-flowctl-code-quality.1.md
59+
"""
60+
expected = """
61+
Task: .1
62+
Epic: fn-21-improve-flowctl-code-quality
63+
Title: Remove dead code in diagnostics.rs
64+
Status: done
65+
Domain: backend
66+
Depends on: none
67+
"""

0 commit comments

Comments
 (0)