diff --git a/Cargo.lock b/Cargo.lock
index 08dcc63cb..aaa52bd5f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -845,9 +845,9 @@ dependencies = [
[[package]]
name = "cc"
-version = "1.2.47"
+version = "1.2.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
+checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -950,7 +950,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "codex-app-server-protocol"
version = "0.63.0"
-source = "git+https://github.com/namastexlabs/codex?branch=main#885fa3a11c439c82a577a02c2bccb81464aa11d4"
+source = "git+https://github.com/namastexlabs/codex?branch=main#27dd137e257b3e2b1857ebb02113178e064722c7"
dependencies = [
"anyhow",
"clap",
@@ -968,7 +968,7 @@ dependencies = [
[[package]]
name = "codex-git"
version = "0.63.0"
-source = "git+https://github.com/namastexlabs/codex?branch=main#885fa3a11c439c82a577a02c2bccb81464aa11d4"
+source = "git+https://github.com/namastexlabs/codex?branch=main#27dd137e257b3e2b1857ebb02113178e064722c7"
dependencies = [
"once_cell",
"regex",
@@ -983,7 +983,7 @@ dependencies = [
[[package]]
name = "codex-protocol"
version = "0.63.0"
-source = "git+https://github.com/namastexlabs/codex?branch=main#885fa3a11c439c82a577a02c2bccb81464aa11d4"
+source = "git+https://github.com/namastexlabs/codex?branch=main#27dd137e257b3e2b1857ebb02113178e064722c7"
dependencies = [
"base64 0.22.1",
"codex-git",
@@ -1008,7 +1008,7 @@ dependencies = [
[[package]]
name = "codex-utils-cache"
version = "0.63.0"
-source = "git+https://github.com/namastexlabs/codex?branch=main#885fa3a11c439c82a577a02c2bccb81464aa11d4"
+source = "git+https://github.com/namastexlabs/codex?branch=main#27dd137e257b3e2b1857ebb02113178e064722c7"
dependencies = [
"lru",
"sha1",
@@ -1018,7 +1018,7 @@ dependencies = [
[[package]]
name = "codex-utils-image"
version = "0.63.0"
-source = "git+https://github.com/namastexlabs/codex?branch=main#885fa3a11c439c82a577a02c2bccb81464aa11d4"
+source = "git+https://github.com/namastexlabs/codex?branch=main#27dd137e257b3e2b1857ebb02113178e064722c7"
dependencies = [
"base64 0.22.1",
"codex-utils-cache",
@@ -2830,9 +2830,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.82"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
+checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -3163,7 +3163,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "mcp-types"
version = "0.63.0"
-source = "git+https://github.com/namastexlabs/codex?branch=main#885fa3a11c439c82a577a02c2bccb81464aa11d4"
+source = "git+https://github.com/namastexlabs/codex?branch=main#27dd137e257b3e2b1857ebb02113178e064722c7"
dependencies = [
"schemars 1.1.0",
"serde",
@@ -3275,9 +3275,9 @@ dependencies = [
[[package]]
name = "moxcms"
-version = "0.7.9"
+version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6"
+checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608"
dependencies = [
"num-traits",
"pxfm",
@@ -4103,9 +4103,9 @@ dependencies = [
[[package]]
name = "pxfm"
-version = "0.1.25"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
+checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b"
dependencies = [
"num-traits",
]
@@ -4487,9 +4487,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
-version = "1.13.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
+checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
dependencies = [
"zeroize",
]
@@ -4888,9 +4888,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.16.0"
+version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1"
+checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -4907,9 +4907,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.16.0"
+version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b"
+checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
dependencies = [
"darling",
"proc-macro2",
@@ -5962,9 +5962,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tracing"
-version = "0.1.41"
+version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
dependencies = [
"log",
"pin-project-lite",
@@ -6006,9 +6006,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
-version = "0.3.20"
+version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
+checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
dependencies = [
"matchers",
"nu-ansi-term",
@@ -6365,9 +6365,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
+checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
dependencies = [
"cfg-if",
"once_cell",
@@ -6378,9 +6378,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.55"
+version = "0.4.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
+checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c"
dependencies = [
"cfg-if",
"js-sys",
@@ -6391,9 +6391,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
+checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -6401,9 +6401,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
+checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -6414,18 +6414,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.105"
+version = "0.2.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
+checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
-version = "0.3.82"
+version = "0.3.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
+checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac"
dependencies = [
"js-sys",
"wasm-bindgen",
diff --git a/forge-app/Cargo.toml b/forge-app/Cargo.toml
index 213702706..4009838b9 100644
--- a/forge-app/Cargo.toml
+++ b/forge-app/Cargo.toml
@@ -12,6 +12,10 @@ path = "src/main.rs"
name = "generate-forge-types"
path = "src/bin/generate_forge_types.rs"
+[[bin]]
+name = "forge-cleanup"
+path = "src/bin/cleanup.rs"
+
[lib]
name = "forge_app"
crate-type = ["cdylib", "rlib"]
diff --git a/forge-app/build.rs b/forge-app/build.rs
index 3c82cc4c1..66bc0b623 100644
--- a/forge-app/build.rs
+++ b/forge-app/build.rs
@@ -1,6 +1,23 @@
+use std::{fs, path::Path};
+
fn main() {
+ // macOS framework linking
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") {
println!("cargo:rustc-link-lib=framework=AppKit");
println!("cargo:rustc-link-lib=framework=Foundation");
}
+
+ // Create frontend/dist directory if it doesn't exist
+ let dist_path = Path::new("../frontend/dist");
+ if !dist_path.exists() {
+ println!("cargo:warning=Creating dummy frontend/dist directory for compilation");
+ fs::create_dir_all(dist_path).unwrap();
+
+ // Create a dummy index.html
+ let dummy_html = r#"
+
Build frontend first
+Please build the frontend
"#;
+
+ fs::write(dist_path.join("index.html"), dummy_html).unwrap();
+ }
}
diff --git a/forge-app/openapi.yaml b/forge-app/openapi.yaml
index 0eb3b055e..ea775bba4 100644
--- a/forge-app/openapi.yaml
+++ b/forge-app/openapi.yaml
@@ -344,6 +344,12 @@ paths:
base_branch:
type: string
example: main
+ github_issue_id:
+ type: integer
+ format: int64
+ nullable: true
+ description: GitHub issue ID linking this task to its originating issue (enforces "No Wish Without Issue" rule)
+ example: 123
required: [task, executor_profile_id, base_branch]
responses:
'200':
@@ -899,6 +905,11 @@ components:
type: string
format: uuid
nullable: true
+ github_issue_id:
+ type: integer
+ format: int64
+ nullable: true
+ description: GitHub issue ID linking this task to its originating issue (enforces "No Wish Without Issue" rule)
created_at:
type: string
format: date-time
@@ -926,6 +937,12 @@ components:
type: string
format: uuid
nullable: true
+ github_issue_id:
+ type: integer
+ format: int64
+ nullable: true
+ description: GitHub issue ID linking this task to its originating issue (enforces "No Wish Without Issue" rule)
+ example: 123
required: [project_id, title]
UpdateTask:
diff --git a/forge-app/src/bin/cleanup.rs b/forge-app/src/bin/cleanup.rs
new file mode 100644
index 000000000..6b5e3dc3a
--- /dev/null
+++ b/forge-app/src/bin/cleanup.rs
@@ -0,0 +1,348 @@
+//! Forge Cleanup - Cleanup stale worktrees
+//!
+//! Scans the worktree temp directory, compares with active task_attempts
+//! in the database, and identifies/removes orphaned worktrees.
+
+use std::collections::HashSet;
+use std::path::PathBuf;
+
+use anyhow::{Context, Result};
+use sqlx::{Row, SqlitePool};
+
+/// Get the worktree base directory
+fn get_worktree_base_dir() -> PathBuf {
+ utils::path::get_automagik_forge_temp_dir().join("worktrees")
+}
+
+/// Get database URL
+///
+/// TODO: This logic is duplicated from the db crate (db::DBService::get_database_url).
+/// Consider exposing it from a shared utility crate to avoid maintenance issues.
+fn get_database_url() -> String {
+ if let Ok(db_url) = std::env::var("DATABASE_URL") {
+ if db_url.starts_with("sqlite://") {
+ let path_part = db_url.strip_prefix("sqlite://").unwrap();
+ if PathBuf::from(path_part).is_absolute() {
+ db_url
+ } else {
+ let abs_path = std::env::current_dir()
+ .unwrap_or_else(|_| PathBuf::from("."))
+ .join(path_part);
+ format!("sqlite://{}", abs_path.to_string_lossy())
+ }
+ } else {
+ db_url
+ }
+ } else {
+ format!(
+ "sqlite://{}",
+ utils::assets::asset_dir().join("db.sqlite").to_string_lossy()
+ )
+ }
+}
+
+/// Worktree info with size
+#[derive(Debug)]
+struct WorktreeInfo {
+ path: PathBuf,
+ name: String,
+ size_bytes: u64,
+}
+
+impl WorktreeInfo {
+ /// Format a byte size as human-readable string
+ fn format_size(size_bytes: u64) -> String {
+ const KB: u64 = 1024;
+ const MB: u64 = 1024 * KB;
+ const GB: u64 = 1024 * MB;
+
+ if size_bytes >= GB {
+ format!("{:.2} GB", size_bytes as f64 / GB as f64)
+ } else if size_bytes >= MB {
+ format!("{:.2} MB", size_bytes as f64 / MB as f64)
+ } else if size_bytes >= KB {
+ format!("{:.2} KB", size_bytes as f64 / KB as f64)
+ } else {
+ format!("{} bytes", size_bytes)
+ }
+ }
+
+ /// Get human-readable size for this worktree
+ fn size_human(&self) -> String {
+ Self::format_size(self.size_bytes)
+ }
+}
+
+/// Calculate directory size recursively
+fn dir_size(path: &PathBuf) -> u64 {
+ let mut size = 0u64;
+ if let Ok(entries) = std::fs::read_dir(path) {
+ for entry_result in entries {
+ match entry_result {
+ Ok(entry) => {
+ let entry_path = entry.path();
+ if entry_path.is_dir() {
+ size += dir_size(&entry_path);
+ } else if let Ok(metadata) = entry_path.metadata() {
+ size += metadata.len();
+ }
+ }
+ Err(e) => {
+ eprintln!(
+ "Warning: Failed to read entry in {}: {}",
+ path.display(),
+ e
+ );
+ }
+ }
+ }
+ }
+ size
+}
+
+/// Scan worktree directory for all worktrees
+fn scan_worktree_directory(base_dir: &PathBuf) -> Result> {
+ let mut worktrees = Vec::new();
+
+ if !base_dir.exists() {
+ return Ok(worktrees);
+ }
+
+ let entries = std::fs::read_dir(base_dir)
+ .with_context(|| format!("Failed to read worktree directory: {}", base_dir.display()))?;
+
+ for entry_result in entries {
+ match entry_result {
+ Ok(entry) => {
+ let path = entry.path();
+ if path.is_dir() {
+ let name = path
+ .file_name()
+ .map(|n| n.to_string_lossy().into_owned())
+ .unwrap_or_else(|| "unknown".to_string());
+ let size_bytes = dir_size(&path);
+ worktrees.push(WorktreeInfo {
+ path,
+ name,
+ size_bytes,
+ });
+ }
+ }
+ Err(e) => {
+ eprintln!(
+ "Warning: Failed to read worktree directory entry in {}: {}",
+ base_dir.display(),
+ e
+ );
+ }
+ }
+ }
+
+ Ok(worktrees)
+}
+
+/// Get active worktree paths from database (where worktree_deleted = false)
+async fn get_active_worktree_paths(pool: &SqlitePool) -> Result> {
+ let records = sqlx::query(
+ "SELECT container_ref FROM task_attempts WHERE worktree_deleted = FALSE AND container_ref IS NOT NULL",
+ )
+ .fetch_all(pool)
+ .await
+ .context("Failed to query task_attempts")?;
+
+ Ok(records
+ .into_iter()
+ .filter_map(|r| r.get::