huskies: merge 1199 story Orphaned build-dir GC: reclaim dead worktree targets without touching warm caches

This commit is contained in:
Huskies Agent
2026-07-17 20:57:21 +00:00
parent 2bd41d980c
commit 5b340e7b20
18 changed files with 565 additions and 5 deletions
+37
View File
@@ -0,0 +1,37 @@
//! MCP tool for the on-demand build-directory GC pass (story 1199).
use serde_json::json;
use crate::http::context::AppContext;
/// MCP tool handler for `gc` — runs a build-directory GC pass on demand and
/// returns bytes reclaimed, duration, and which directories were reclaimed
/// or skipped (raced/errored).
pub(crate) async fn tool_gc(ctx: &AppContext) -> Result<String, String> {
let project_root = ctx.services.agents.get_project_root(&ctx.state)?;
let report = crate::service::gc::io::run_gc_pass(&project_root).await;
serde_json::to_string_pretty(&json!({
"bytes_reclaimed": report.bytes_reclaimed,
"reclaimed": report.reclaimed,
"skipped": report.skipped,
"duration_secs": report.duration.as_secs_f64(),
}))
.map_err(|e| format!("Serialization error: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::test_helpers::test_ctx;
#[tokio::test]
async fn tool_gc_returns_zero_bytes_for_empty_project() {
let tmp = tempfile::tempdir().unwrap();
let ctx = test_ctx(tmp.path());
let result = tool_gc(&ctx).await.expect("tool_gc must not fail");
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["bytes_reclaimed"].as_u64(), Some(0));
assert!(parsed["reclaimed"].as_array().unwrap().is_empty());
}
}
+2
View File
@@ -1,9 +1,11 @@
//! MCP agent tools — start, stop, wait, list, and inspect agents via MCP.
mod gc;
mod inspection;
mod lifecycle;
mod worktree;
pub(crate) use gc::tool_gc;
pub(crate) use inspection::{
tool_get_agent_config, tool_get_agent_output, tool_get_agent_remaining_turns_and_budget,
};
+1
View File
@@ -43,6 +43,7 @@ pub async fn dispatch_tool_call(
"list_worktrees" => agent_tools::tool_list_worktrees(ctx),
"remove_worktree" => agent_tools::tool_remove_worktree(&args, ctx).await,
"cleanup_worktrees" => agent_tools::tool_cleanup_worktrees(&args, ctx).await,
"gc" => agent_tools::tool_gc(ctx).await,
// Editor tools
"get_editor_command" => agent_tools::tool_get_editor_command(&args, ctx),
// Lifecycle tools
@@ -180,6 +180,14 @@ pub(super) fn agent_tools() -> Vec<Value> {
}
}
}),
json!({
"name": "gc",
"description": "Run a build-directory GC pass on demand (story 1199): reclaims orphaned worktree target/ dirs and a stale merge_workspace, never touching the main project's or any live worktree's target/. Returns bytes reclaimed, duration, and which directories were reclaimed or skipped.",
"inputSchema": {
"type": "object",
"properties": {}
}
}),
json!({
"name": "get_editor_command",
"description": "Get the open-in-editor command for a worktree. Returns a ready-to-paste shell command like 'zed /path/to/worktree'. Requires the editor preference to be configured via PUT /api/settings/editor.",
+2 -1
View File
@@ -116,7 +116,8 @@ mod tests {
assert!(names.contains(&"convert_item_type"));
assert!(names.contains(&"edit"));
assert!(names.contains(&"write"));
assert_eq!(tools.len(), 84);
assert!(names.contains(&"gc"));
assert_eq!(tools.len(), 85);
}
#[test]