Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
156ee0b
docs(01): create phase 1 audit & triage plans
Jan 12, 2026
290af48
docs(01-02): create testing protocol for all 28 tools
Jan 14, 2026
2b1ff81
refactor(02-01): create session submodule structure and extract plan_…
Jan 14, 2026
b3ebb3d
refactor(02-02): extract provider logic into providers.rs submodule
Jan 14, 2026
896179d
refactor(02-03): create commands.rs with all handle_* methods
Jan 14, 2026
87551ad
refactor(02-03): update session/mod.rs to delegate to commands
Jan 14, 2026
f5fbc23
refactor(02-04): extract UI helpers to session/ui.rs
Jan 14, 2026
bd11a3f
feat(03-02): create common error utilities module
Jan 15, 2026
ee7c900
refactor(03-02): update high-priority tools with error utilities
Jan 15, 2026
833dcfa
feat(03-02): add error module to tools with documentation
Jan 15, 2026
1288fd7
feat(03-03): create response formatting utilities
Jan 15, 2026
6b21deb
feat(03-03): update core tools with response formatting
Jan 15, 2026
6135036
refactor(03-03): document response patterns in mod.rs
Jan 15, 2026
b48c816
feat(04-01): expand shell command allowlist with categories
Jan 15, 2026
5b71ee3
feat(04-01): improve shell tool definition and rejection messages
Jan 15, 2026
558c5ec
test(04-01): add shell tool allowlist tests
Jan 15, 2026
f77d649
feat(04-02): improve file_ops tool definitions
Jan 15, 2026
cb9aed0
feat(04-02): improve file_ops path validation error messages
Jan 15, 2026
b601f68
feat(04-02): add file_ops edge case handling
Jan 15, 2026
7cfb4f5
feat(04-03): improve analyze tool definition
Jan 15, 2026
c400d08
feat(04-03): add analyze tool edge case handling
Jan 15, 2026
0df119a
feat(05-01): improve hadolint tool with error patterns and tests
Jan 15, 2026
10ec3ce
feat(05-02): improve helmlint tool with error patterns and tests
Jan 15, 2026
d76cf1e
feat(05-03): improve kubelint tool with error patterns and tests
Jan 15, 2026
614eba8
feat(05-04): improve dclint tool with error patterns and tests
Jan 15, 2026
5328904
feat(06-01): improve k8s_optimize tool with error patterns
Jan 15, 2026
3b6d1c7
feat(06-02): improve prometheus_connect tool with error patterns
Jan 15, 2026
c979343
feat(06-03): improve k8s_costs tool with error patterns
Jan 15, 2026
7289bfd
fix(07-02): preserve context during history truncation
Jan 15, 2026
0417c75
feat(07-03): session persistence with full context restore
Jan 15, 2026
4e7878f
test(08-02): add tests for input.rs and autocomplete.rs
Jan 15, 2026
6055e5e
test(09-02): add tests to untested tool files
Jan 15, 2026
457a570
chore: clean up ignored files
Jan 15, 2026
0bcd1c2
small fixes
Jan 15, 2026
04b7d7b
fix(ci): bump MSRV to 1.87 and ignore transitive security advisories
Jan 15, 2026
da56df9
fix(ci): bump MSRV to 1.88 for AWS SDK compatibility
Jan 15, 2026
c734b3c
fix(fomatting): missing formatting
Jan 15, 2026
9618e10
feat: matrix ui upgrade for better view and visibility
Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
# MSRV 1.88 - AWS SDK requires Rust 1.88
rust: ["1.88"]

steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -79,5 +80,8 @@ jobs:
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
# Only fail on actual vulnerabilities, not unmaintained warnings
ignore: RUSTSEC-2020-0163,RUSTSEC-2024-0320,RUSTSEC-2025-0057,RUSTSEC-2025-0074,RUSTSEC-2025-0075,RUSTSEC-2025-0080,RUSTSEC-2025-0081,RUSTSEC-2025-0098,RUSTSEC-2025-0104,RUSTSEC-2025-0134
# Ignore advisories in transitive dependencies we cannot control:
# - gix-date (RUSTSEC-2025-0140): via rustsec crate, awaiting upstream fix
# - bincode (RUSTSEC-2025-0141): via syntect, marked "complete" by maintainer
# - Other transitive deps from rustsec, aws-sdk, kube, etc.
ignore: RUSTSEC-2020-0163,RUSTSEC-2024-0320,RUSTSEC-2025-0057,RUSTSEC-2025-0074,RUSTSEC-2025-0075,RUSTSEC-2025-0080,RUSTSEC-2025-0081,RUSTSEC-2025-0098,RUSTSEC-2025-0104,RUSTSEC-2025-0134,RUSTSEC-2025-0140,RUSTSEC-2025-0141
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# will have compiled files and executables
debug/
target/
test-results/
tmp/

node_modules/
*.vsix
Expand All @@ -14,6 +16,9 @@ node_modules/
.qoder/*
.qoder/**/*

# Planning documents (local only, not shared)
.planning/

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Ignore docs except specific tracked files
Expand Down
8 changes: 1 addition & 7 deletions .rustfmt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,4 @@ remove_nested_parens = true
merge_derives = true
use_try_shorthand = true
use_field_init_shorthand = true
force_explicit_abi = true
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
where_single_line = false
imports_layout = "Vertical"
imports_granularity = "Crate"
force_explicit_abi = true
7 changes: 4 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
name = "syncable-cli"
version = "0.26.1"
edition = "2024"
rust-version = "1.88" # MSRV - AWS SDK requires 1.88
authors = ["Syncable Team"]
description = "A Rust-based CLI that analyzes code repositories and generates Infrastructure as Code configurations"
license = "GPL-3.0"
repository = "https://github.com/syncable-dev/syncable-cli"
keywords = [
"cli",
"ai",
"devops",
"iac",
"docker",
"kubernetes",
"terraform",
"devops",
]
categories = ["command-line-utilities", "development-tools"]
readme = "README.md"
Expand Down
2 changes: 1 addition & 1 deletion src/agent/compact/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ impl ContextSummary {
}

/// A summary frame ready to be inserted into context
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SummaryFrame {
/// The rendered summary text
pub content: String,
Expand Down
209 changes: 208 additions & 1 deletion src/agent/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub struct ToolCallRecord {
}

/// Conversation history manager with forge-style compaction support
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationHistory {
/// Full conversation turns
turns: Vec<ConversationTurn>,
Expand Down Expand Up @@ -235,6 +235,29 @@ impl ConversationHistory {
self.context_summary = ContextSummary::new();
}

/// Clear turns but preserve the summary frame (for sync with truncated raw_chat_history)
///
/// Use this instead of clear() when raw_chat_history is truncated but we want to
/// preserve the accumulated context from prior compaction.
pub fn clear_turns_preserve_context(&mut self) {
// First compact any remaining turns into the summary
if self.turns.len() > 1 {
let _ = self.compact();
}

// Now clear turns but keep summary_frame and context_summary
self.turns.clear();

// Recalculate tokens (just summary frame now)
self.total_tokens = self
.summary_frame
.as_ref()
.map(|f| f.token_count)
.unwrap_or(0);

// User turn count stays as-is for statistics
}

/// Perform forge-style compaction with smart eviction
/// Returns the summary that was created (for logging/display)
pub fn compact(&mut self) -> Option<String> {
Expand Down Expand Up @@ -482,6 +505,16 @@ impl ConversationHistory {
.iter()
.map(|s| s.as_str())
}

/// Serialize to JSON for session persistence
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}

/// Deserialize from JSON (for session restore)
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}

/// Helper to truncate text with ellipsis
Expand Down Expand Up @@ -622,4 +655,178 @@ mod tests {
let reason = history.compaction_reason();
assert!(reason.is_some());
}

#[test]
fn test_clear_turns_preserve_context() {
// Create history with aggressive compaction to trigger summary
let mut history = ConversationHistory::with_config(CompactConfig {
retention_window: 2,
eviction_window: 0.6,
thresholds: CompactThresholds {
token_threshold: Some(200),
turn_threshold: Some(3),
message_threshold: Some(5),
on_turn_end: None,
},
});

// Add turns to trigger compaction
for i in 0..6 {
history.add_turn(
format!("Question {} with extra text", i),
format!("Answer {} with more detail", i),
vec![],
);
}

// Trigger compaction to build summary
if history.needs_compaction() {
let _ = history.compact();
}

// Verify we have a summary frame now
let had_summary_before = history.summary_frame.is_some();

// Now clear turns while preserving context
history.clear_turns_preserve_context();

// Verify turns are cleared but summary is preserved
assert_eq!(history.turn_count(), 0, "Turns should be cleared");
assert!(
history.summary_frame.is_some() == had_summary_before,
"Summary frame should be preserved"
);

// Token count should only include summary frame
if history.summary_frame.is_some() {
assert!(history.token_count() > 0, "Should have tokens from summary");
}

// to_messages should still work and include summary
let messages = history.to_messages();
if history.summary_frame.is_some() {
assert!(
!messages.is_empty(),
"Should still have summary in messages"
);
}
}

#[test]
fn test_clear_vs_clear_preserve_context() {
let mut history = ConversationHistory::new();

// Add some turns
for i in 0..5 {
history.add_turn(format!("Q{}", i), format!("A{}", i), vec![]);
}

// Force compaction
let _ = history.compact();
let had_summary = history.summary_frame.is_some();

// Test clear_turns_preserve_context
let mut history_preserve = history.clone();
history_preserve.clear_turns_preserve_context();

// Test regular clear
let mut history_clear = history.clone();
history_clear.clear();

// Verify difference
if had_summary {
assert!(
history_preserve.summary_frame.is_some(),
"preserve should keep summary"
);
assert!(
history_clear.summary_frame.is_none(),
"clear removes summary"
);
}

// Both should have no turns
assert_eq!(history_preserve.turn_count(), 0);
assert_eq!(history_clear.turn_count(), 0);
}

#[test]
fn test_history_serialization() {
let mut history = ConversationHistory::new();

// Add some turns
history.add_turn(
"What is this project?".to_string(),
"This is a Rust CLI tool.".to_string(),
vec![ToolCallRecord {
tool_name: "analyze".to_string(),
args_summary: "path: .".to_string(),
result_summary: "Found Rust project".to_string(),
tool_id: Some("tool_1".to_string()),
droppable: false,
}],
);

// Serialize
let json = history.to_json().expect("Should serialize");
assert!(!json.is_empty());

// Deserialize
let restored = ConversationHistory::from_json(&json).expect("Should deserialize");
assert_eq!(restored.turn_count(), 1);
assert_eq!(restored.user_turn_count(), 1);

// Verify tool call preserved
let messages = restored.to_messages();
assert!(!messages.is_empty());
}

#[test]
fn test_history_serialization_with_compaction() {
// Create history with compaction triggered
let mut history = ConversationHistory::with_config(CompactConfig {
retention_window: 2,
eviction_window: 0.6,
thresholds: CompactThresholds {
token_threshold: Some(200),
turn_threshold: Some(3),
message_threshold: Some(5),
on_turn_end: None,
},
});

// Add many turns to trigger compaction
for i in 0..6 {
history.add_turn(
format!("Question {} with some text", i),
format!("Answer {} with more detail", i),
vec![],
);
}

// Trigger compaction
if history.needs_compaction() {
let _ = history.compact();
}

let had_summary = history.summary_frame.is_some();

// Serialize with summary
let json = history.to_json().expect("Should serialize");

// Deserialize and verify summary preserved
let restored = ConversationHistory::from_json(&json).expect("Should deserialize");
assert_eq!(
restored.summary_frame.is_some(),
had_summary,
"Summary frame should be preserved"
);

// to_messages should include summary
let messages = restored.to_messages();
if had_summary {
// Summary adds 2 messages (user + assistant acknowledgment)
assert!(messages.len() >= 2, "Should have summary messages");
}
}
}
Loading