feat(story-93): expose server logs to agents via get_server_logs MCP tool

- Add log_buffer module: bounded 1000-line ring buffer with push/get_recent API
- Add slog! macro: drop-in for eprintln! that also captures to ring buffer
- Replace all eprintln! calls across agents, watcher, search, chat, worktree, claude_code with slog!
- Add get_server_logs MCP tool: accepts count (1-500) and optional filter params
- 5 unit tests for log_buffer covering push/retrieve, eviction, filtering, count limits, empty buffer
- 262 tests passing, clippy clean

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-23 20:38:19 +00:00
parent 3d480e7c22
commit 8c6bd4cf74
10 changed files with 243 additions and 66 deletions

View File

@@ -1,3 +1,4 @@
use crate::slog;
use crate::state::SessionState;
use ignore::WalkBuilder;
use serde::Serialize;
@@ -52,7 +53,7 @@ pub async fn search_files_impl(query: String, root: PathBuf) -> Result<Vec<Searc
});
}
}
Err(err) => eprintln!("Error walking dir: {}", err),
Err(err) => slog!("Error walking dir: {}", err),
}
}

View File

@@ -15,6 +15,7 @@
//! via exit-code inspection and silently skips the commit while still broadcasting
//! the event so connected clients stay in sync.
use crate::slog;
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher, recommended_watcher};
use serde::Serialize;
use std::collections::HashMap;
@@ -151,13 +152,13 @@ fn flush_pending(
("remove", item.to_string(), format!("story-kit: remove {item}"))
};
eprintln!("[watcher] flush: {commit_msg}");
slog!("[watcher] flush: {commit_msg}");
match git_add_work_and_commit(git_root, &commit_msg) {
Ok(committed) => {
if committed {
eprintln!("[watcher] committed: {commit_msg}");
slog!("[watcher] committed: {commit_msg}");
} else {
eprintln!("[watcher] skipped (already committed): {commit_msg}");
slog!("[watcher] skipped (already committed): {commit_msg}");
}
let stage = additions.first().map_or("unknown", |(_, s)| s);
let evt = WatcherEvent {
@@ -169,7 +170,7 @@ fn flush_pending(
let _ = event_tx.send(evt);
}
Err(e) => {
eprintln!("[watcher] git error: {e}");
slog!("[watcher] git error: {e}");
}
}
}
@@ -192,17 +193,17 @@ pub fn start_watcher(
}) {
Ok(w) => w,
Err(e) => {
eprintln!("[watcher] failed to create watcher: {e}");
slog!("[watcher] failed to create watcher: {e}");
return;
}
};
if let Err(e) = watcher.watch(&work_dir, RecursiveMode::Recursive) {
eprintln!("[watcher] failed to watch {}: {e}", work_dir.display());
slog!("[watcher] failed to watch {}: {e}", work_dir.display());
return;
}
eprintln!("[watcher] watching {}", work_dir.display());
slog!("[watcher] watching {}", work_dir.display());
const DEBOUNCE: Duration = Duration::from_millis(300);
@@ -237,13 +238,13 @@ pub fn start_watcher(
false
}
Ok(Err(e)) => {
eprintln!("[watcher] notify error: {e}");
slog!("[watcher] notify error: {e}");
false
}
// Debounce window expired — time to flush.
Err(mpsc::RecvTimeoutError::Timeout) => true,
Err(mpsc::RecvTimeoutError::Disconnected) => {
eprintln!("[watcher] channel disconnected, shutting down");
slog!("[watcher] channel disconnected, shutting down");
break;
}
};