diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 99b51ca34..afdef2005 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -4523,7 +4523,6 @@ components: - run_id - settings - graph - - labels - checkpoints_disabled properties: run_id: diff --git a/lib/crates/fabro-cli/tests/it/cmd/attach.rs b/lib/crates/fabro-cli/tests/it/cmd/attach.rs index 328efb39a..4edb7c0ee 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/attach.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/attach.rs @@ -159,7 +159,6 @@ fn attach_replays_completed_detached_run() { exit_code: 0 ----- stdout ----- ----- stderr ----- - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ Start [TIME] ✓ Run Tests [TIME] @@ -266,7 +265,6 @@ fn attach_before_completion_streams_to_finished_state() { exit_code: 0 ----- stdout ----- ----- stderr ----- - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ start [DURATION] ✓ wait [DURATION] @@ -690,22 +688,6 @@ fn attach_json_errors_without_prompting_for_human_input() { "run_id": "[ULID]", "ts": "[TIMESTAMP]" }, - { - "actor": { - "display": "system:worker", - "id": "worker", - "kind": "system" - }, - "event": "run.notice", - "id": "[EVENT_ID]", - "properties": { - "code": "dirty_worktree", - "level": "warn", - "message": "Uncommitted changes will not be included in the worktree." - }, - "run_id": "[ULID]", - "ts": "[TIMESTAMP]" - }, { "actor": { "display": "system:worker", diff --git a/lib/crates/fabro-cli/tests/it/cmd/dump.rs b/lib/crates/fabro-cli/tests/it/cmd/dump.rs index 2c99b692b..9224ab842 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/dump.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/dump.rs @@ -270,9 +270,9 @@ fn dump_exports_completed_run_snapshot() { "); assert_snapshot!(dump_file_summary(&output_dir), @" - checkpoints/0014.json - checkpoints/0018.json - checkpoints/0022.json + checkpoints/0013.json + checkpoints/0017.json + checkpoints/0021.json events.jsonl graph.fabro run.json diff --git a/lib/crates/fabro-cli/tests/it/cmd/logs.rs b/lib/crates/fabro-cli/tests/it/cmd/logs.rs index 141dd2dc1..2ded011d2 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/logs.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/logs.rs @@ -188,7 +188,6 @@ fn logs_pretty_formats_small_run() { success: true exit_code: 0 ----- stdout ----- - [CLOCK] Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] [CLOCK] Sandbox: local [DURATION] [CLOCK] ▶ Simple [ULID] Run tests and report results diff --git a/lib/crates/fabro-cli/tests/it/cmd/run.rs b/lib/crates/fabro-cli/tests/it/cmd/run.rs index 6755943dc..dec66554a 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/run.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/run.rs @@ -674,7 +674,6 @@ fn dry_run_simple() { Goal: Run tests and report results Run: [ULID] - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ Start [TIME] ✓ Run Tests [TIME] diff --git a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs b/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs index ae7022563..a44ef957d 100644 --- a/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs +++ b/lib/crates/fabro-cli/tests/it/workflow/dry_run_examples.rs @@ -20,7 +20,6 @@ fn dry_run_branching() { warning [node: implement]: Node 'implement' has goal_gate=true but no retry_target or fallback_retry_target (goal_gate_has_retry) Run: [ULID] - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ Start [TIME] ✓ Plan [TIME] @@ -56,7 +55,6 @@ fn dry_run_conditions() { Goal: Test condition evaluation with OR and parentheses Run: [ULID] - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ start [TIME] ✓ Decide [TIME] @@ -90,7 +88,6 @@ fn dry_run_parallel() { Goal: Test parallel and fan-in execution Run: [ULID] - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ start [TIME] ✓ Fork Work [TIME] @@ -125,7 +122,6 @@ fn dry_run_styled() { Goal: Build a styled pipeline Run: [ULID] - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ start [TIME] ✓ Plan [TIME] @@ -160,7 +156,6 @@ fn dry_run_legacy_tool() { Goal: Verify backwards compatibility with old tool naming Run: [ULID] - Warning: Uncommitted changes will not be included in the worktree. [dirty_worktree] Sandbox: local (ready in [TIME]) ✓ Start [TIME] ✓ Echo [TIME] diff --git a/lib/crates/fabro-workflow/src/pipeline/initialize.rs b/lib/crates/fabro-workflow/src/pipeline/initialize.rs index 6bcf89564..ef8cbc923 100644 --- a/lib/crates/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflow/src/pipeline/initialize.rs @@ -45,9 +45,9 @@ struct WorktreePlan { async fn resolve_worktree_base_sha( sandbox: &dyn Sandbox, plan: &WorktreePlan, -) -> Result { +) -> Result, Error> { if let Some(base_sha) = plan.base_sha.as_ref() { - return Ok(base_sha.clone()); + return Ok(Some(base_sha.clone())); } let result = sandbox @@ -67,6 +67,9 @@ async fn resolve_worktree_base_sha( } else { output }; + if is_not_git_repository(output) { + return Ok(None); + } return Err(Error::engine(format!( "git rev-parse HEAD failed (exit {}): {}", result.exit_code, output @@ -77,7 +80,11 @@ async fn resolve_worktree_base_sha( if base_sha.is_empty() { return Err(Error::engine("git rev-parse HEAD returned no commit sha")); } - Ok(base_sha.to_string()) + Ok(Some(base_sha.to_string())) +} + +fn is_not_git_repository(output: &str) -> bool { + output.contains("not a git repository") || output.contains("ambiguous argument 'HEAD'") } async fn run_hooks( @@ -131,17 +138,19 @@ fn resolve_worktree_plan(options: &mut InitOptions) -> Option { } } - let git_is_clean = options + let local_dirty = options .run_options .pre_run_git .as_ref() - .is_some_and(|git| matches!(git.local_dirty, fabro_types::DirtyStatus::Clean)); + .map(|git| git.local_dirty); + let git_is_clean = + local_dirty.is_some_and(|status| matches!(status, fabro_types::DirtyStatus::Clean)); let strategy = options .sandbox .workdir_strategy(worktree_mode, git_is_clean, options.checkpoint.is_some()); - if !git_is_clean { + if matches!(local_dirty, Some(fabro_types::DirtyStatus::Dirty)) { let env_name = match strategy { WorkdirStrategy::LocalWorktree => Some("worktree"), WorkdirStrategy::Cloud => Some("remote sandbox"), @@ -491,33 +500,36 @@ pub async fn initialize( .build(Some(Arc::clone(&sandbox_event_callback))) .await .map_err(|e| Error::engine(e.to_string()))?; - metadata_runtime - .ensure_git_available(&*inner) - .await - .map_err(|err| Error::engine(format!("sandbox git unavailable: {err}")))?; - let base_sha = resolve_worktree_base_sha(&*inner, plan).await?; - options.run_options.display_base_sha = Some(base_sha.clone()); - options.run_options.git = Some(GitCheckpointOptions { - base_sha: Some(base_sha.clone()), - run_branch: Some(plan.branch_name.clone()), - meta_branch: Some(metadata_branch_name(&options.run_id.to_string())), - }); - let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions { - branch_name: plan.branch_name.clone(), - base_sha, - worktree_path: plan.worktree_path.to_string_lossy().into_owned(), - skip_branch_creation: plan.skip_branch_creation, - setup_intent: Some(git_setup_intent(&options.run_options)), - }); - worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback()); - match worktree.initialize().await { - Ok(()) => { - worktree_created = true; - Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree))) - } - Err(e) => { - return Err(Error::engine(format!("Git worktree setup failed: {e}"))); + if let Some(base_sha) = resolve_worktree_base_sha(&*inner, plan).await? { + metadata_runtime + .ensure_git_available(&*inner) + .await + .map_err(|err| Error::engine(format!("sandbox git unavailable: {err}")))?; + options.run_options.display_base_sha = Some(base_sha.clone()); + options.run_options.git = Some(GitCheckpointOptions { + base_sha: Some(base_sha.clone()), + run_branch: Some(plan.branch_name.clone()), + meta_branch: Some(metadata_branch_name(&options.run_id.to_string())), + }); + let mut worktree = WorktreeSandbox::new(inner, WorktreeOptions { + branch_name: plan.branch_name.clone(), + base_sha, + worktree_path: plan.worktree_path.to_string_lossy().into_owned(), + skip_branch_creation: plan.skip_branch_creation, + setup_intent: Some(git_setup_intent(&options.run_options)), + }); + worktree.set_event_callback(Arc::clone(&options.emitter).worktree_callback()); + match worktree.initialize().await { + Ok(()) => { + worktree_created = true; + Arc::new(ReadBeforeWriteSandbox::new(Arc::new(worktree))) + } + Err(e) => { + return Err(Error::engine(format!("Git worktree setup failed: {e}"))); + } } + } else { + Arc::new(ReadBeforeWriteSandbox::new(inner)) } } else { Arc::new(ReadBeforeWriteSandbox::new( diff --git a/lib/packages/fabro-api-client/src/models/run-spec.ts b/lib/packages/fabro-api-client/src/models/run-spec.ts index 4e0a26789..da5d7c704 100644 --- a/lib/packages/fabro-api-client/src/models/run-spec.ts +++ b/lib/packages/fabro-api-client/src/models/run-spec.ts @@ -34,7 +34,7 @@ export interface RunSpec { 'source_directory'?: string | null; 'repo_origin_url'?: string | null; 'base_branch'?: string | null; - 'labels': { [key: string]: string; }; + 'labels'?: { [key: string]: string; }; 'provenance'?: { [key: string]: any; } | null; 'manifest_blob'?: string | null; 'definition_blob'?: string | null;