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

Commit d8187fd

Browse files
committed
feat(compress): add 7 TOML filters + wire pretty_output callers
Adds gap, memory, dag, files, ready, hook_precompact, hook_subagent filters to the builtin registry and routes each command's pretty path through pretty_output() so --compact invocations apply the filter. Savings measured against a TTY baseline: - gap list: 199 → 160 B (-19.6%) - memory list: 1411 → 1028 B (-27.1%) - dag: 255 → 167 B (-34.5%) - files: 1335 → 724 B (-45.8%) - ready: 248 → 139 B (-44.0%) - hook pre-compact: 1320 → 473 B (-64.2%) - hook subagent: 861 → 473 B (-45.1%) 30 inline filter tests pass; full cargo test suite green (297 passed). Task: fn-21-extend-flowctl-compress-filters.1
1 parent a22b9aa commit d8187fd

14 files changed

Lines changed: 562 additions & 41 deletions

File tree

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use std::fs;
1010
use clap::Subcommand;
1111
use serde_json::json;
1212

13-
use crate::output::{error_exit, json_output};
13+
use crate::output::{error_exit, json_output, pretty_output};
1414

1515
use flowctl_core::id::is_epic_id;
1616
use flowctl_core::types::EPICS_DIR;
@@ -238,22 +238,28 @@ fn cmd_gap_list(json_mode: bool, epic_id: &str, status_filter: Option<&str>) {
238238
let suffix = status_filter
239239
.map(|s| format!(" (status={})", s))
240240
.unwrap_or_default();
241-
println!("No gaps for {}{}", epic_id, suffix);
241+
let msg = format!("No gaps for {}{}", epic_id, suffix);
242+
pretty_output("gap", &msg);
242243
} else {
244+
use std::fmt::Write as _;
245+
let mut buf = String::new();
243246
for g in &filtered {
244247
let marker = if g["status"].as_str() == Some("resolved") {
245248
"\u{2713}"
246249
} else {
247250
"\u{2717}"
248251
};
249-
println!(
252+
writeln!(
253+
buf,
250254
" {} {} [{}] {}",
251255
marker,
252256
g["id"].as_str().unwrap_or(""),
253257
g["priority"].as_str().unwrap_or(""),
254258
g["capability"].as_str().unwrap_or(""),
255-
);
259+
)
260+
.ok();
256261
}
262+
pretty_output("gap", &buf);
257263
}
258264
}
259265

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ use clap::Subcommand;
1515
use regex::Regex;
1616
use serde_json::{json, Value};
1717

18+
use crate::output::pretty_output;
19+
1820
#[derive(Subcommand, Debug)]
1921
pub enum HookCmd {
2022
/// Extract session memories from transcript (Stop hook).
@@ -1582,11 +1584,14 @@ fn cmd_pre_compact() {
15821584
}
15831585

15841586
if !lines.is_empty() {
1585-
println!("[flow-code state]");
1587+
use std::fmt::Write as _;
1588+
let mut buf = String::new();
1589+
writeln!(buf, "[flow-code state]").ok();
15861590
for line in &lines {
1587-
println!("{line}");
1591+
writeln!(buf, "{line}").ok();
15881592
}
1589-
println!("[/flow-code state]");
1593+
writeln!(buf, "[/flow-code state]").ok();
1594+
pretty_output("hook_precompact", &buf);
15901595
}
15911596

15921597
std::process::exit(0);
@@ -1623,7 +1628,8 @@ fn cmd_subagent_context() {
16231628
if let Some(val) = run_flowctl(&flowctl, &["tasks", "--status", "in_progress", "--json"]) {
16241629
let json_str = serde_json::to_string(&val).unwrap_or_default();
16251630
if json_str != "[]" && !json_str.is_empty() {
1626-
println!("Active flow-code tasks: {json_str}");
1631+
let line = format!("Active flow-code tasks: {json_str}");
1632+
pretty_output("hook_subagent", &line);
16271633
}
16281634
}
16291635

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

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use regex::Regex;
1010
use serde_json::json;
1111
use sha2::{Digest, Sha256};
1212

13-
use crate::output::{error_exit, json_output};
13+
use crate::output::{error_exit, json_output, pretty_output};
1414
use flowctl_core::types::{CONFIG_FILE, MEMORY_DIR};
1515

1616
use super::helpers::get_flow_dir;
@@ -862,8 +862,10 @@ fn cmd_memory_list(
862862
"index": index_data,
863863
}));
864864
} else {
865+
use std::fmt::Write as _;
866+
let mut buf = String::new();
865867
let mut stale_count = 0;
866-
println!("Memory: {} entries, {} total references\n", total, total_refs);
868+
writeln!(buf, "Memory: {} entries, {} total references\n", total, total_refs).ok();
867869
for idx in &index {
868870
let eid = idx.get("id").and_then(|v| v.as_i64()).unwrap_or(0);
869871
let eid_str = eid.to_string();
@@ -889,24 +891,29 @@ fn cmd_memory_list(
889891
let entry_type = idx.get("type").and_then(|v| v.as_str()).unwrap_or("");
890892
let summary = idx.get("summary").and_then(|v| v.as_str()).unwrap_or("");
891893
let summary_trunc: String = summary.chars().take(70).collect();
892-
println!(
894+
writeln!(
895+
buf,
893896
" #{:3} [{:10}] refs={:2} {}{}",
894897
eid, entry_type, refs, summary_trunc, stale_tag
895-
);
898+
)
899+
.ok();
896900
}
897-
println!();
901+
writeln!(buf).ok();
898902
let mut sorted_counts: Vec<_> = counts.iter().collect();
899903
sorted_counts.sort_by_key(|(k, _)| (*k).clone());
900904
for (t, c) in &sorted_counts {
901-
println!(" {}: {}", t, c);
905+
writeln!(buf, " {}: {}", t, c).ok();
902906
}
903-
println!(" Total: {}", total);
907+
writeln!(buf, " Total: {}", total).ok();
904908
if stale_count > 0 {
905-
println!(
909+
writeln!(
910+
buf,
906911
" Stale: {} (not verified in 90+ days — run /flow-code:retro to verify)",
907912
stale_count
908-
);
913+
)
914+
.ok();
909915
}
916+
pretty_output("memory", &buf);
910917
}
911918
}
912919

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -717,24 +717,29 @@ pub fn cmd_files(json_mode: bool, epic: String) {
717717
"conflict_count": conflicts.len(),
718718
}));
719719
} else {
720-
println!("File ownership for {}:\n", epic);
720+
use std::fmt::Write as _;
721+
let mut buf = String::new();
722+
writeln!(buf, "File ownership for {}:\n", epic).ok();
721723
if ownership.is_empty() {
722-
println!(" No files declared.");
724+
writeln!(buf, " No files declared.").ok();
723725
} else {
724726
for (f, task_ids) in &ownership {
725727
if task_ids.len() == 1 {
726-
println!(" {} \u{2192} {}", f, task_ids[0]);
728+
writeln!(buf, " {} \u{2192} {}", f, task_ids[0]).ok();
727729
} else {
728-
println!(" {} \u{2192} CONFLICT: {}", f, task_ids.join(", "));
730+
writeln!(buf, " {} \u{2192} CONFLICT: {}", f, task_ids.join(", ")).ok();
729731
}
730732
}
731733
if !conflicts.is_empty() {
732-
println!(
734+
writeln!(
735+
buf,
733736
"\n \u{26a0} {} file conflict(s) \u{2014} tasks sharing files cannot run in parallel",
734737
conflicts.len()
735-
);
738+
)
739+
.ok();
736740
}
737741
}
742+
pretty_output("files", &buf);
738743
}
739744
}
740745

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::path::PathBuf;
99
use clap::Subcommand;
1010
use serde_json::json;
1111

12-
use crate::output::{error_exit, json_output};
12+
use crate::output::{error_exit, json_output, pretty_output};
1313

1414
/// Open DB or exit with error.
1515
fn open_db_or_exit() -> crate::commands::db_shim::Connection {
@@ -398,8 +398,10 @@ pub fn cmd_dag(json_flag: bool, epic_id: Option<String>) {
398398
}
399399

400400
// ASCII rendering
401-
println!("DAG for {}", epic_id);
402-
println!();
401+
use std::fmt::Write as _;
402+
let mut buf = String::new();
403+
writeln!(buf, "DAG for {}", epic_id).ok();
404+
writeln!(buf).ok();
403405

404406
for layer in 0..=max_layer {
405407
let mut nodes_in_layer: Vec<&flowctl_core::types::Task> = tasks
@@ -421,9 +423,9 @@ pub fn cmd_dag(json_flag: bool, epic_id: Option<String>) {
421423
let label = format!(".{} [{}]", short_id, status_icon);
422424
let indent = " ".repeat(layer);
423425
let connector = if layer > 0 { "\u{2514}\u{2500}\u{2500} " } else { "" };
424-
println!("{}{}\u{250c}\u{2500}{}\u{2500}\u{2510}", indent, connector, "\u{2500}".repeat(label.len()), );
425-
println!("{}{}\u{2502} {} \u{2502}", indent, if layer > 0 { " " } else { "" }, label);
426-
println!("{}{}\u{2514}\u{2500}{}\u{2500}\u{2518}", indent, if layer > 0 { " " } else { "" }, "\u{2500}".repeat(label.len()));
426+
writeln!(buf, "{}{}\u{250c}\u{2500}{}\u{2500}\u{2510}", indent, connector, "\u{2500}".repeat(label.len())).ok();
427+
writeln!(buf, "{}{}\u{2502} {} \u{2502}", indent, if layer > 0 { " " } else { "" }, label).ok();
428+
writeln!(buf, "{}{}\u{2514}\u{2500}{}\u{2500}\u{2518}", indent, if layer > 0 { " " } else { "" }, "\u{2500}".repeat(label.len())).ok();
427429
}
428430

429431
// Draw arrows between layers
@@ -434,11 +436,12 @@ pub fn cmd_dag(json_flag: bool, epic_id: Option<String>) {
434436
.collect();
435437
if !next_layer_nodes.is_empty() {
436438
let indent = " ".repeat(layer + 1);
437-
println!("{}\u{2502}", indent);
438-
println!("{}\u{2193}", indent);
439+
writeln!(buf, "{}\u{2502}", indent).ok();
440+
writeln!(buf, "{}\u{2193}", indent).ok();
439441
}
440442
}
441443
}
444+
pretty_output("dag", &buf);
442445
}
443446

444447
// ── Estimate command ─────────────────────────────────────────────────

flowctl/crates/flowctl-cli/src/commands/workflow/scheduling.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::fs;
55

66
use serde_json::json;
77

8-
use crate::output::{error_exit, json_output};
8+
use crate::output::{error_exit, json_output, pretty_output};
99

1010
use flowctl_core::id::{is_epic_id, parse_id};
1111
use flowctl_core::state_machine::Status;
@@ -104,16 +104,18 @@ pub fn cmd_ready(json_mode: bool, epic: String) {
104104
})).collect::<Vec<_>>(),
105105
}));
106106
} else {
107-
println!("Ready tasks for {} (actor: {}):", epic, current_actor);
107+
use std::fmt::Write as _;
108+
let mut buf = String::new();
109+
writeln!(buf, "Ready tasks for {} (actor: {}):", epic, current_actor).ok();
108110
if ready.is_empty() {
109-
println!(" (none)");
111+
writeln!(buf, " (none)").ok();
110112
} else {
111113
for t in &ready {
112-
println!(" {}: {}", t.id, t.title);
114+
writeln!(buf, " {}: {}", t.id, t.title).ok();
113115
}
114116
}
115117
if !in_progress.is_empty() {
116-
println!("\nIn progress:");
118+
writeln!(buf, "\nIn progress:").ok();
117119
for t in &in_progress {
118120
let assignee = get_runtime(&t.id)
119121
.and_then(|rt| rt.assignee)
@@ -123,15 +125,16 @@ pub fn cmd_ready(json_mode: bool, epic: String) {
123125
} else {
124126
""
125127
};
126-
println!(" {}: {} [{}]{}", t.id, t.title, assignee, marker);
128+
writeln!(buf, " {}: {} [{}]{}", t.id, t.title, assignee, marker).ok();
127129
}
128130
}
129131
if !blocked.is_empty() {
130-
println!("\nBlocked:");
132+
writeln!(buf, "\nBlocked:").ok();
131133
for (t, deps) in &blocked {
132-
println!(" {}: {} (by: {})", t.id, t.title, deps.join(", "));
134+
writeln!(buf, " {}: {} (by: {})", t.id, t.title, deps.join(", ")).ok();
133135
}
134136
}
137+
pretty_output("ready", &buf);
135138
}
136139
}
137140

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ use std::sync::OnceLock;
3131
const EPICS_TOML: &str = include_str!("filters/epics.toml");
3232
const TASKS_TOML: &str = include_str!("filters/tasks.toml");
3333
const STATUS_TOML: &str = include_str!("filters/status.toml");
34+
const GAP_TOML: &str = include_str!("filters/gap.toml");
35+
const MEMORY_TOML: &str = include_str!("filters/memory.toml");
36+
const DAG_TOML: &str = include_str!("filters/dag.toml");
37+
const FILES_TOML: &str = include_str!("filters/files.toml");
38+
const READY_TOML: &str = include_str!("filters/ready.toml");
39+
const HOOK_PRECOMPACT_TOML: &str = include_str!("filters/hook_precompact.toml");
40+
const HOOK_SUBAGENT_TOML: &str = include_str!("filters/hook_subagent.toml");
3441

3542
// ---------------------------------------------------------------------------
3643
// Deserialization types (TOML schema)
@@ -255,6 +262,13 @@ const BUILTIN_SOURCES: &[(&str, &str)] = &[
255262
("epics", EPICS_TOML),
256263
("tasks", TASKS_TOML),
257264
("status", STATUS_TOML),
265+
("gap", GAP_TOML),
266+
("memory", MEMORY_TOML),
267+
("dag", DAG_TOML),
268+
("files", FILES_TOML),
269+
("ready", READY_TOML),
270+
("hook_precompact", HOOK_PRECOMPACT_TOML),
271+
("hook_subagent", HOOK_SUBAGENT_TOML),
258272
];
259273

260274
fn load_registry() -> Vec<CompiledFilter> {
@@ -672,8 +686,19 @@ on_empty = "all clean"
672686
fn test_builtin_filters_load() {
673687
let reg = registry();
674688
assert!(!reg.is_empty(), "builtin registry should load at least one filter");
675-
// The three POC filters we ship
676-
for expected in &["epics", "tasks", "status"] {
689+
// POC filters + fn-21 extensions
690+
for expected in &[
691+
"epics",
692+
"tasks",
693+
"status",
694+
"gap",
695+
"memory",
696+
"dag",
697+
"files",
698+
"ready",
699+
"hook_precompact",
700+
"hook_subagent",
701+
] {
677702
assert!(
678703
reg.iter().any(|f| f.name == *expected),
679704
"expected builtin filter '{}' to be present",

0 commit comments

Comments
 (0)