Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 19 additions & 1 deletion crates/agentflare-jobs/src/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ impl Supervisor {
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
// This supervisor is driven by the daemon's own background discovery
// tick (`spawn_supervisor_discovery`, every `SUPERVISOR_DISCOVERY_INTERVAL`)
// as well as interactive job requests — no console-attached parent is
// guaranteed. Without this flag every dispatched job auto-allocates a
// console window on Windows, flashing briefly even with no active
// Claude Code session, since the daemon runs unattended in the background.
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}

let mut child = cmd.spawn()?;

Expand Down Expand Up @@ -215,11 +227,17 @@ fn kill_graceful(child: &mut std::process::Child, kill_after: Duration) {
}
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let pid = child.id().to_string();
let _ = Command::new("taskkill").args(["/T", "/PID", &pid]).status();
let _ = Command::new("taskkill")
.args(["/T", "/PID", &pid])
.creation_flags(CREATE_NO_WINDOW)
.status();
std::thread::sleep(kill_after);
let _ = Command::new("taskkill")
.args(["/T", "/F", "/PID", &pid])
.creation_flags(CREATE_NO_WINDOW)
.status();
}
#[cfg(not(any(unix, windows)))]
Expand Down
31 changes: 23 additions & 8 deletions crates/flare-git-core/src/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,28 @@ pub(crate) fn git_binary() -> PathBuf {
.clone()
}

/// This crate's git spawns run inside the agentflare daemon far more than
/// any other one -- `resolve_project()` calls `run_in`-backed `repo_toplevel`
/// on essentially every project/item MCP call. The daemon itself is
/// console-less, so without this flag every one of those spawns
/// auto-allocates a console window on Windows, flashing briefly.
#[cfg(windows)]
fn no_console_window(cmd: &mut Command) {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
#[cfg(not(windows))]
fn no_console_window(_cmd: &mut Command) {}

/// Runs `git` in `repo_root`; `Ok(stdout)` trimmed on success, `Err(stderr)`
/// trimmed on a non-zero exit, or a process-spawn error message (git
/// missing, etc) if it couldn't even run.
pub fn run_in(repo_root: &Path, args: &[&str]) -> Result<String, String> {
let out = Command::new(git_binary())
.args(args)
.current_dir(repo_root)
let mut cmd = Command::new(git_binary());
cmd.args(args).current_dir(repo_root);
no_console_window(&mut cmd);
let out = cmd
.output()
.map_err(|e| format!("git not available: {e}"))?;
if !out.status.success() {
Expand Down Expand Up @@ -132,11 +147,11 @@ pub fn run_in_ok(repo_root: &Path, args: &[&str]) -> bool {
/// the rest of this module's helpers return.
pub fn diff(repo_root: &Path, base: &str, head: &str) -> Result<String, String> {
let range = format!("{base}...{head}");
let out = Command::new(git_binary())
.args(["diff", "--unified=3", &range])
.current_dir(repo_root)
.output()
.map_err(|e| format!("git diff failed: {e}"))?;
let mut cmd = Command::new(git_binary());
cmd.args(["diff", "--unified=3", &range])
.current_dir(repo_root);
no_console_window(&mut cmd);
let out = cmd.output().map_err(|e| format!("git diff failed: {e}"))?;
if !out.status.success() {
return Err(format!(
"git diff {range}: {}",
Expand Down
15 changes: 12 additions & 3 deletions crates/flare-git-shim/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,19 @@ struct ScopeCheckResult {
/// the escape hatch for a broken/missing `agentflare` binary, same as any
/// other misclassification.
fn scope_check_deny_reason(subcommand: &str) -> Option<String> {
let output = match std::process::Command::new("agentflare")
.args(["git", "scope-check", "--subcommand", subcommand])
.output()
let mut cmd = std::process::Command::new("agentflare");
cmd.args(["git", "scope-check", "--subcommand", subcommand]);
// Runs on every git invocation through this shim; without this flag,
// whenever the invoking parent has no inherited console (e.g. spawned by
// an IDE/editor extension), Windows auto-allocates one for this child —
// visible as a brief flashing terminal window.
#[cfg(windows)]
{
use std::os::windows::process::CommandExt as _;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
return Some(format!(
Expand Down
9 changes: 9 additions & 0 deletions crates/gateway-registry/src/mcp_stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ impl McpStdioBackend {
// timeout actually terminates the hung child, not just our
// side of the connection.
cmd.kill_on_drop(true);
// This process (the agentflare daemon) is itself console-less;
// any console-subsystem backend spawned without this flag gets
// a console window auto-allocated by Windows, which flashes
// briefly on screen on every circuit-breaker/timeout respawn.
#[cfg(windows)]
{
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
}),
)
.map_err(|e| {
Expand Down
8 changes: 7 additions & 1 deletion src/ipc/process.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use std::time::Duration;

#[cfg(windows)]
use std::os::windows::process::CommandExt;

pub fn is_alive(pid: u32) -> bool {
#[cfg(windows)]
{
let status = std::process::Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
.output();
match status {
Ok(o) => {
Expand All @@ -26,7 +30,6 @@ pub fn is_alive(pid: u32) -> bool {
pub fn spawn_detached(binary: &str, args: &[&str]) -> Result<u32, String> {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
let mut cmd = std::process::Command::new(binary);
cmd.args(args);
cmd.creation_flags(
Expand Down Expand Up @@ -54,6 +57,7 @@ pub fn terminate_gracefully(pid: u32) -> Result<(), String> {
{
let status = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string()])
.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
.status()
.map_err(|e| format!("taskkill: {e}"))?;
if status.success() {
Expand Down Expand Up @@ -81,6 +85,7 @@ pub fn force_kill(pid: u32) -> Result<(), String> {
{
let status = std::process::Command::new("taskkill")
.args(["/F", "/PID", &pid.to_string()])
.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
.status()
.map_err(|e| format!("taskkill /F: {e}"))?;
if status.success() {
Expand Down Expand Up @@ -121,6 +126,7 @@ fn list_pids_raw(binary_name: &str) -> String {
"CSV",
"/NH",
])
.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW)
.output();
#[cfg(not(windows))]
let output = std::process::Command::new("pgrep")
Expand Down
26 changes: 23 additions & 3 deletions src/mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -541,12 +541,32 @@ impl AgentflareMcp {
/// across multiple linked projects depending on which subdirectory a
/// tool was invoked from. Falls back to raw cwd only when nothing is
/// found anywhere above it.
///
/// Memoized per-cwd: every project/item MCP call runs this, and in the
/// long-running daemon that meant a fresh `git rev-parse` subprocess on
/// essentially every call. Keyed by cwd rather than a single cached
/// value because `cli::work::execute_work` calls `set_current_dir` to
/// switch into an item's worktree for in-process work-item execution —
/// a single-value cache would freeze on whatever cwd resolved first and
/// silently break resolution for every worktree after that. A distinct
/// cwd is always a fresh lookup; a repeated cwd is a cache hit.
pub(crate) fn repo_root() -> std::path::PathBuf {
let cwd = std::env::current_dir().unwrap_or_default();
if let Some(root) = flare_git_core::branch::repo_toplevel(&cwd) {
return root;
static CACHE: std::sync::OnceLock<
std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, std::path::PathBuf>>,
> = std::sync::OnceLock::new();
let cache = CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()));
if let Ok(guard) = cache.lock()
&& let Some(root) = guard.get(&cwd)
{
return root.clone();
}
Self::find_root_from(&cwd, &crate::paths::home())
let root = flare_git_core::branch::repo_toplevel(&cwd)
.unwrap_or_else(|| Self::find_root_from(&cwd, &crate::paths::home()));
if let Ok(mut guard) = cache.lock() {
guard.insert(cwd, root.clone());
}
root
}

/// `repo_root()`, but honoring `worktree_repo_root_override` — used only
Expand Down
Loading