diff --git a/crates/client/src/spawn.rs b/crates/client/src/spawn.rs index 38ba5a16..00012fa6 100644 --- a/crates/client/src/spawn.rs +++ b/crates/client/src/spawn.rs @@ -35,10 +35,14 @@ pub struct SpawnConfig { /// Process-local Ticket role marker supplied only by Ticket role launches. /// This does not alter prompts, manifests, or Ticket claim records. pub ticket_role: Option, - /// Explicit runtime workspace root. The child uses it as process cwd and - /// receives it via `--workspace` so startup does not infer workspace - /// identity from the parent process cwd. + /// Explicit runtime workspace root. The child receives it via + /// `--workspace` so startup does not infer workspace identity from the + /// parent process cwd. pub workspace_root: PathBuf, + /// Optional child process cwd. This is not runtime workspace identity and + /// is not passed as a CLI argument; the child observes it as its ordinary + /// process current directory. + pub cwd: Option, /// `Some(id)` のとき `--session ` を付与し、当該セッションから /// resume させる。 pub resume_from: Option, @@ -149,7 +153,7 @@ where let mut command = Command::new(config.runtime_command.program()); command .args(config.runtime_command.prefix_args()) - .current_dir(&config.workspace_root) + .current_dir(config.cwd.as_ref().unwrap_or(&config.workspace_root)) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::from(stderr_file)) @@ -335,6 +339,7 @@ mod tests { profile: Some("project:companion".to_string()), ticket_role: None, workspace_root: PathBuf::from("/work/other-project"), + cwd: None, resume_from: None, } } @@ -372,9 +377,10 @@ mod tests { } #[test] - fn runtime_args_pass_ticket_role_marker_when_present() { + fn runtime_args_do_not_include_child_cwd() { let mut config = base_config(); - config.ticket_role = Some("intake".to_string()); + config.ticket_role = Some("orchestrator".to_string()); + config.cwd = Some(PathBuf::from("/work/main/.worktree/orchestration/yoi")); assert_eq!( runtime_args(&config), @@ -386,7 +392,7 @@ mod tests { "--profile", "project:companion", "--ticket-role", - "intake", + "orchestrator", ] ); } diff --git a/crates/client/src/ticket_role.rs b/crates/client/src/ticket_role.rs index 5bf1f74f..4f570031 100644 --- a/crates/client/src/ticket_role.rs +++ b/crates/client/src/ticket_role.rs @@ -78,6 +78,7 @@ impl TicketIntakeHandoff { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TicketRoleLaunchContext { pub workspace_root: PathBuf, + pub cwd: Option, pub original_workspace_root: Option, pub target_workspace_root: Option, pub role: TicketRole, @@ -97,6 +98,7 @@ impl TicketRoleLaunchContext { pub fn new(workspace_root: impl Into, role: TicketRole) -> Self { Self { workspace_root: workspace_root.into(), + cwd: None, original_workspace_root: None, target_workspace_root: None, role, @@ -113,6 +115,11 @@ impl TicketRoleLaunchContext { } } + pub fn with_cwd(mut self, root: impl Into) -> Self { + self.cwd = Some(root.into()); + self + } + pub fn with_original_workspace_root(mut self, root: impl Into) -> Self { self.original_workspace_root = Some(root.into()); self @@ -144,6 +151,7 @@ impl TicketRoleLaunchContext { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TicketRoleLaunchPlan { pub workspace_root: PathBuf, + pub cwd: Option, pub original_workspace_root: PathBuf, pub target_workspace_root: PathBuf, pub implementation_worktree_root: PathBuf, @@ -175,6 +183,7 @@ impl TicketRoleLaunchPlan { profile: Some(self.profile.clone()), ticket_role: Some(self.role.as_str().to_string()), workspace_root: self.workspace_root.clone(), + cwd: self.cwd.clone(), resume_from: None, }) } @@ -285,6 +294,7 @@ pub fn plan_ticket_role_launch_with_config( Ok(TicketRoleLaunchPlan { workspace_root: context.workspace_root, + cwd: context.cwd, original_workspace_root, target_workspace_root, implementation_worktree_root, @@ -678,6 +688,9 @@ fn append_workspace_routing_context(out: &mut String, context: &TicketRoleLaunch "role_workspace_root", &context.workspace_root.display().to_string(), ); + if let Some(cwd) = &context.cwd { + push_bounded_bullet(out, "role_cwd", &cwd.display().to_string()); + } push_bounded_bullet( out, "original_workspace_root", @@ -875,6 +888,7 @@ mod tests { fn test_launch_plan(workspace: &std::path::Path) -> TicketRoleLaunchPlan { TicketRoleLaunchPlan { workspace_root: workspace.to_path_buf(), + cwd: None, original_workspace_root: workspace.to_path_buf(), target_workspace_root: workspace.to_path_buf(), implementation_worktree_root: workspace.join(".worktree"), @@ -1330,6 +1344,7 @@ workflow = "ticket-review-workflow" .spawn_config(PodRuntimeCommand::for_executable("/bin/yoi")) .unwrap(); assert_eq!(spawn_config.workspace_root, temp.path()); + assert_eq!(spawn_config.cwd, None); assert!(text.contains("Workspace routing context:")); assert!(text.contains("role_workspace_root")); diff --git a/crates/pod/src/controller.rs b/crates/pod/src/controller.rs index 8d0d7e53..4146d7f8 100644 --- a/crates/pod/src/controller.rs +++ b/crates/pod/src/controller.rs @@ -508,7 +508,7 @@ where // Pod-immutable snapshots taken before the mutable worker borrow // below so the worker borrow doesn't conflict with reads on `pod`. let scope_handle = pod.scope().clone(); - let pwd = pod.pwd().to_path_buf(); + let cwd = pod.cwd().to_path_buf(); let workspace_root = pod.workspace_root().to_path_buf(); let task_feature = pod.task_feature(); let session_id_for_usage = pod.segment_id().to_string(); @@ -526,7 +526,7 @@ where // ScopedFs (builtin tools, fs_view, compact worker) reads from it, // and any future scope mutation (SpawnPod-style revoke, future // GrantScope) propagates through it. - let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), pwd.clone()); + let fs = tools::ScopedFs::with_shared_scope(scope_handle.clone(), cwd.clone()); let tracker = tools::Tracker::new(); // Same ScopedFs also powers the IPC `ListCompletions` query — keep // a clone for the FS view we attach below, since the tools consume @@ -607,7 +607,7 @@ where spawner_socket, runtime_base.clone(), workspace_root.clone(), - pwd.clone(), + cwd.clone(), spawned_registry.clone(), self_parent_socket, spawner_manifest, @@ -618,7 +618,7 @@ where worker.register_tool(read_pod_output_tool(spawned_registry.clone())); worker.register_tool(stop_pod_tool(spawned_registry.clone())); let discovery = - PodDiscovery::new(pod_store, spawner_name, runtime_base, pwd, spawned_registry); + PodDiscovery::new(pod_store, spawner_name, runtime_base, cwd, spawned_registry); worker.register_tool(list_pods_tool(discovery.clone())); worker.register_tool(restore_pod_tool(discovery.clone())); worker.register_tool(send_to_peer_pod_tool(discovery)); @@ -664,7 +664,7 @@ async fn controller_loop( pod.store().clone(), spawner_name.clone(), discovery_runtime_base, - pod.pwd().to_path_buf(), + pod.cwd().to_path_buf(), spawned_registry.clone(), ); let mut pending: Option = None; @@ -1292,7 +1292,7 @@ where .collect(); protocol::Greeting { pod_name: manifest.pod.name.clone(), - cwd: pod.pwd().display().to_string(), + cwd: pod.cwd().display().to_string(), provider: provider_name, model: model_id, scope_summary: pod.scope_snapshot().summary(), diff --git a/crates/pod/src/entrypoint.rs b/crates/pod/src/entrypoint.rs index 6ec5e86f..1e7a0c8c 100644 --- a/crates/pod/src/entrypoint.rs +++ b/crates/pod/src/entrypoint.rs @@ -29,12 +29,6 @@ struct Cli { #[arg(long, value_name = "PATH")] workspace: Option, - /// Internal spawned child process/tool working directory. This is separate - /// from `--workspace`; adopted Pods use `--workspace` for runtime context - /// and this path for tool defaults. - #[arg(long, value_name = "PATH", requires = "adopt", hide = true)] - tool_cwd: Option, - /// Manifest TOML to use directly as a one-file compatibility/debug input. /// This bypasses profile discovery but still applies builtin defaults and /// the same required-field validation boundary. @@ -107,17 +101,6 @@ fn runtime_workspace_root(cli: &Cli) -> Result { } } -fn runtime_tool_cwd(cli: &Cli, workspace_root: &Path) -> Result { - let raw = cli.tool_cwd.as_deref().unwrap_or(workspace_root); - let path = if raw.is_absolute() { - raw.to_path_buf() - } else { - workspace_root.join(raw) - }; - std::fs::canonicalize(&path) - .map_err(|e| format!("failed to resolve tool cwd {}: {e}", path.display())) -} - fn runtime_pod_name(cli: &Cli, workspace_root: &Path) -> String { cli.pod .as_deref() @@ -300,6 +283,13 @@ fn exit_code_from_i32(code: i32) -> ExitCode { } async fn run_cli_inner(cli: Cli) -> ExitCode { + let cwd = match std::env::current_dir() { + Ok(path) => path, + Err(e) => { + eprintln!("error: failed to resolve current directory: {e}"); + return ExitCode::FAILURE; + } + }; let workspace_root = match runtime_workspace_root(&cli) { Ok(root) => root, Err(e) => { @@ -314,15 +304,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode { return ExitCode::FAILURE; } }; - - if let Err(e) = std::env::set_current_dir(&workspace_root) { - eprintln!( - "error: failed to enter runtime workspace {}: {e}", - workspace_root.display() - ); - return ExitCode::FAILURE; - } - // Initialize persistent store. `paths::sessions_dir()` only // returns None when none of YOI_HOME / YOI_DATA_DIR / // HOME is set — surface that as a hard error to match the @@ -372,33 +353,17 @@ async fn run_cli_inner(cli: Cli) -> ExitCode { return ExitCode::FAILURE; } }; - let tool_cwd = match runtime_tool_cwd(&cli, &workspace_root) { - Ok(path) => path, - Err(e) => { - eprintln!("error: {e}"); - return ExitCode::FAILURE; - } - }; match Pod::from_manifest_spawned_with_context( manifest, store, loader, callback, workspace_root.clone(), - tool_cwd.clone(), + cwd.clone(), ) .await { - Ok(p) => { - if let Err(e) = std::env::set_current_dir(&tool_cwd) { - eprintln!( - "error: failed to enter tool cwd {}: {e}", - tool_cwd.display() - ); - return ExitCode::FAILURE; - } - p - } + Ok(p) => p, Err(e) => { eprintln!("error: failed to create spawned pod: {e}"); return ExitCode::FAILURE; @@ -418,12 +383,14 @@ async fn run_cli_inner(cli: Cli) -> ExitCode { return ExitCode::FAILURE; } }; - match Pod::restore_from_manifest( + match Pod::restore_from_manifest_with_context( source_session_id, source_segment_id, manifest, store, loader, + workspace_root.clone(), + cwd.clone(), ) .await { @@ -437,7 +404,16 @@ async fn run_cli_inner(cli: Cli) -> ExitCode { manifest.pod.name = pod_name.to_string(); match store.read_by_name(pod_name) { Ok(Some(_)) => { - match Pod::restore_from_pod_metadata(pod_name, manifest, store, loader).await { + match Pod::restore_from_pod_metadata_with_context( + pod_name, + manifest, + store, + loader, + workspace_root.clone(), + cwd.clone(), + ) + .await + { Ok(p) => p, Err(e) => { eprintln!("error: failed to restore pod {pod_name}: {e}"); @@ -449,20 +425,38 @@ async fn run_cli_inner(cli: Cli) -> ExitCode { eprintln!("error: pod state missing for {pod_name}"); return ExitCode::FAILURE; } - Ok(None) => match Pod::from_manifest(manifest, store, loader).await { - Ok(p) => p, - Err(e) => { - eprintln!("error: failed to create pod {pod_name}: {e}"); - return ExitCode::FAILURE; + Ok(None) => { + match Pod::from_manifest_with_context( + manifest, + store, + loader, + workspace_root.clone(), + cwd.clone(), + ) + .await + { + Ok(p) => p, + Err(e) => { + eprintln!("error: failed to create pod {pod_name}: {e}"); + return ExitCode::FAILURE; + } } - }, + } Err(e) => { eprintln!("error: failed to read pod state for {pod_name}: {e}"); return ExitCode::FAILURE; } } } else { - match Pod::from_manifest(manifest, store, loader).await { + match Pod::from_manifest_with_context( + manifest, + store, + loader, + workspace_root.clone(), + cwd.clone(), + ) + .await + { Ok(p) => p, Err(e) => { eprintln!("error: failed to create pod: {e}"); @@ -478,7 +472,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode { pod.set_runtime_ticket_role(Some(role)); } let pod_name = pod.manifest().pod.name.clone(); - // Spawn the controller (starts socket server) let runtime_base = match paths::runtime_dir() { Some(d) => d, diff --git a/crates/pod/src/fs_view.rs b/crates/pod/src/fs_view.rs index 07219565..583f3fc5 100644 --- a/crates/pod/src/fs_view.rs +++ b/crates/pod/src/fs_view.rs @@ -45,7 +45,7 @@ pub struct PodFsView { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FileCandidate { /// 入力 prefix と整合する形のパス(prefix が absolute なら absolute、 - /// relative なら pwd 相対)。 + /// relative なら cwd 相対)。 pub path: String, pub is_dir: bool, } @@ -114,7 +114,7 @@ impl PodFsView { /// `path` を ScopedFs 経由で解決し、submit 時の `Segment::FileRef` /// attachment 用 system message を返す。 /// - /// - `path` は relative なら pwd 相対、absolute なら absolute として解釈 + /// - `path` は relative なら cwd 相対、absolute なら absolute として解釈 /// - 通常ディレクトリは浅い entry listing として `[Dir: ]\n` に展開する /// - ディレクトリ listing は hidden / gitignore を特別扱いせず、scope 上 readable な /// 直下 entry だけを最大 `DIR_FILE_REF_ENTRY_LIMIT` 件返す @@ -126,7 +126,7 @@ impl PodFsView { let abs = if p.is_absolute() { p.to_path_buf() } else { - self.fs.pwd().join(p) + self.fs.cwd().join(p) }; // 通常ディレクトリだけを FileRef listing として扱う。symlink を含むパスは @@ -163,16 +163,16 @@ impl PodFsView { /// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。 /// - /// - `prefix` が空 or `pwd` 相対のときは pwd 直下を見る + /// - `prefix` が空 or `cwd` 相対のときは cwd 直下を見る /// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙 /// - 末尾が名前部分のときは、その名前を starts_with でフィルタ /// - scope 上 readable なエントリのみ返す /// - ディレクトリ → ファイル の順、各グループ内は名前昇順 /// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない) pub fn list_file_completions(&self, prefix: &str) -> Vec { - let pwd = self.fs.pwd(); + let cwd = self.fs.cwd(); let scope = self.fs.scope(); - let (dir, name_prefix, is_absolute) = split_prefix(prefix, pwd); + let (dir, name_prefix, is_absolute) = split_prefix(prefix, cwd); let read_dir = match std::fs::read_dir(&dir) { Ok(rd) => rd, @@ -194,7 +194,7 @@ impl PodFsView { let display = if is_absolute { path.display().to_string() } else { - path.strip_prefix(pwd) + path.strip_prefix(cwd) .map(|p| p.display().to_string()) .unwrap_or_else(|_| path.display().to_string()) }; @@ -343,7 +343,7 @@ fn format_range(offset: Option, limit: Option) -> String { } } -fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) { +fn split_prefix(prefix: &str, cwd: &Path) -> (PathBuf, String, bool) { let is_absolute = Path::new(prefix).is_absolute(); let p = Path::new(prefix); let (parent, name) = if prefix.is_empty() || prefix.ends_with('/') { @@ -359,9 +359,9 @@ fn split_prefix(prefix: &str, pwd: &Path) -> (PathBuf, String, bool) { let dir = if is_absolute { parent } else if parent.as_os_str().is_empty() { - pwd.to_path_buf() + cwd.to_path_buf() } else { - pwd.join(parent) + cwd.join(parent) }; (dir, name, is_absolute) } diff --git a/crates/pod/src/pod.rs b/crates/pod/src/pod.rs index 7f91cf40..c0809d65 100644 --- a/crates/pod/src/pod.rs +++ b/crates/pod/src/pod.rs @@ -232,7 +232,7 @@ pub struct Pod { /// wrapper over `segment_state.segment_id()`. segment_state: Arc, /// Absolute tool/process working directory of the Pod. - pwd: PathBuf, + cwd: PathBuf, /// Absolute runtime workspace root used for project records, workflow, /// memory, Ticket config, Profile context, and spawned-child inheritance. workspace_root: PathBuf, @@ -423,7 +423,7 @@ impl Pod { store: self.store.clone(), pod_metadata_writer: None, segment_state: self.segment_state.clone(), - pwd: self.pwd.clone(), + cwd: self.cwd.clone(), workspace_root: self.workspace_root.clone(), scope: self.scope.clone(), delegation_scope: self.delegation_scope.clone(), @@ -577,7 +577,7 @@ impl Pod { impl Pod { /// Create a new Pod from a pre-built Worker and store. /// - /// Callers must pre-resolve `pwd` (absolute) and build a [`Scope`] + /// Callers must pre-resolve `cwd` (absolute) and build a [`Scope`] /// — typically via [`Scope::from_config`] when coming from a /// manifest, or [`Scope::writable`] in tests. /// @@ -589,7 +589,7 @@ impl Pod { manifest: PodManifest, worker: Worker, store: St, - pwd: PathBuf, + cwd: PathBuf, scope: Scope, ) -> Result { // Segment creation is deferred to `ensure_segment_head` at first @@ -606,8 +606,8 @@ impl Pod { store, pod_metadata_writer: None, segment_state: SegmentState::new(session_id, segment_id, 0), - workspace_root: pwd.clone(), - pwd, + workspace_root: cwd.clone(), + cwd, scope: SharedScope::new(scope), delegation_scope, hook_builder: HookRegistryBuilder::new(), @@ -718,11 +718,11 @@ impl Pod { } /// The Pod's tool/process working directory. - pub fn pwd(&self) -> &Path { - &self.pwd + pub fn cwd(&self) -> &Path { + &self.cwd } - /// The Pod's runtime workspace root. This stays separate from `pwd` for + /// The Pod's runtime workspace root. This stays separate from `cwd` for /// spawned children whose SpawnPod `cwd` only changes tool defaults. pub fn workspace_root(&self) -> &Path { &self.workspace_root @@ -1325,7 +1325,7 @@ impl Pod { let scope_snapshot = self.scope.snapshot(); let ctx = SystemPromptContext { now: chrono::Utc::now(), - cwd: &self.pwd, + cwd: &self.cwd, language: worker_language, scope: &scope_snapshot, tool_names, @@ -1562,7 +1562,7 @@ impl Pod { fn resolve_file_refs(&self, segments: &[Segment]) -> Vec { let view = crate::fs_view::PodFsView::new(tools::ScopedFs::with_shared_scope( self.scope.clone(), - self.pwd.clone(), + self.cwd.clone(), )); let mut out = Vec::new(); for seg in segments { @@ -2461,11 +2461,11 @@ impl Pod { auto_read_budget, ))); - // Build an independent compact worker. Scope and pwd are shared + // Build an independent compact worker. Scope and cwd are shared // with the main Pod (reads go through the same policy) but the // Tracker is fresh — compact-time reads must not pollute the // main session's recency list, which feeds `default_refs` above. - let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.pwd.clone()); + let scoped_fs = tools::ScopedFs::with_shared_scope(self.scope.clone(), self.cwd.clone()); let summary_tracker = tools::Tracker::new(); let summary_client: Box = self.build_compactor_client()?; let summary_system_prompt = self @@ -3708,7 +3708,7 @@ where /// process's `std::env::current_dir()` — callers that want a /// different cwd must `cd` before constructing the Pod (e.g. the /// `SpawnPod` tool sets `Command::current_dir` on the child). The - /// captured pwd is canonicalised and validated against + /// captured cwd is canonicalised and validated against /// `manifest.scope`. /// /// `loader` is installed into the system-prompt template @@ -3720,7 +3720,25 @@ where store: St, loader: PromptLoader, ) -> Result { - let mut common = prepare_pod_common(&manifest, &loader, /* parse_template */ true)?; + let cwd = current_cwd()?; + Self::from_manifest_with_context(manifest, store, loader, cwd.clone(), cwd).await + } + + pub async fn from_manifest_with_context( + manifest: PodManifest, + store: St, + loader: PromptLoader, + workspace_root: PathBuf, + cwd: PathBuf, + ) -> Result { + let mut common = prepare_pod_common_with_context( + &manifest, + &loader, + /* parse_template */ true, + workspace_root, + cwd, + manifest.scope.clone(), + )?; let skill_shadows = std::mem::take(&mut common.skill_shadows); // Segment creation is deferred to the first run (see @@ -3757,7 +3775,7 @@ where store, pod_metadata_writer, segment_state: SegmentState::new(session_id, segment_id, 0), - pwd: common.pwd, + cwd: common.cwd, workspace_root: common.workspace_root, scope: SharedScope::new(common.scope), delegation_scope: common.delegation_scope, @@ -3814,14 +3832,14 @@ where loader: PromptLoader, callback_socket: PathBuf, ) -> Result { - let pwd = current_pwd()?; + let cwd = current_cwd()?; Self::from_manifest_spawned_with_context( manifest, store, loader, callback_socket, - pwd.clone(), - pwd, + cwd.clone(), + cwd, ) .await } @@ -3832,14 +3850,14 @@ where loader: PromptLoader, callback_socket: PathBuf, workspace_root: PathBuf, - tool_cwd: PathBuf, + cwd: PathBuf, ) -> Result { let mut common = prepare_pod_common_with_context( &manifest, &loader, /* parse_template */ true, workspace_root, - tool_cwd, + cwd, manifest.scope.clone(), )?; let skill_shadows = std::mem::take(&mut common.skill_shadows); @@ -3865,7 +3883,7 @@ where store, pod_metadata_writer, segment_state: SegmentState::new(session_id, segment_id, 0), - pwd: common.pwd, + cwd: common.cwd, workspace_root: common.workspace_root, scope: SharedScope::new(common.scope), delegation_scope: common.delegation_scope, @@ -3917,6 +3935,26 @@ where manifest: PodManifest, store: St, loader: PromptLoader, + ) -> Result { + let cwd = current_cwd()?; + Self::restore_from_pod_metadata_with_context( + pod_name, + manifest, + store, + loader, + cwd.clone(), + cwd, + ) + .await + } + + pub async fn restore_from_pod_metadata_with_context( + pod_name: &str, + manifest: PodManifest, + store: St, + loader: PromptLoader, + workspace_root: PathBuf, + cwd: PathBuf, ) -> Result { let metadata = store @@ -3936,15 +3974,36 @@ where session_id: active.session_id, })?; let manifest = match metadata.resolved_manifest_snapshot { - Some(snapshot) => serde_json::from_value(snapshot).map_err(|source| { - PodError::PodMetadataManifestSnapshot { - pod_name: pod_name.to_string(), - source, + Some(snapshot) => { + let mut restored: PodManifest = + serde_json::from_value(snapshot).map_err(|source| { + PodError::PodMetadataManifestSnapshot { + pod_name: pod_name.to_string(), + source, + } + })?; + if !manifest.scope.allow.is_empty() || !manifest.scope.deny.is_empty() { + restored.scope = manifest.scope; } - })?, + if !manifest.delegation_scope.allow.is_empty() + || !manifest.delegation_scope.deny.is_empty() + { + restored.delegation_scope = manifest.delegation_scope; + } + restored + } None => manifest, }; - Self::restore_from_manifest(active.session_id, segment_id, manifest, store, loader).await + Self::restore_from_manifest_with_context( + active.session_id, + segment_id, + manifest, + store, + loader, + workspace_root, + cwd, + ) + .await } /// Restore a Pod from an existing session log. @@ -3970,6 +4029,28 @@ where manifest: PodManifest, store: St, loader: PromptLoader, + ) -> Result { + let cwd = current_cwd()?; + Self::restore_from_manifest_with_context( + session_id, + segment_id, + manifest, + store, + loader, + cwd.clone(), + cwd, + ) + .await + } + + pub async fn restore_from_manifest_with_context( + session_id: SessionId, + segment_id: SegmentId, + manifest: PodManifest, + store: St, + loader: PromptLoader, + workspace_root: PathBuf, + cwd: PathBuf, ) -> Result { // Read raw entries once so we can both reconstruct state and // seed the broadcast sink's mirror with the same prefix that @@ -3982,10 +4063,12 @@ where let mirror_entries: Vec = raw_entries.clone(); let scope_config = effective_restore_scope_config(&store, &manifest)?; - let mut common = prepare_pod_common_with_scope( + let mut common = prepare_pod_common_with_context( &manifest, &loader, /* parse_template */ false, + workspace_root, + cwd, scope_config, )?; let skill_shadows = std::mem::take(&mut common.skill_shadows); @@ -4045,7 +4128,7 @@ where store, pod_metadata_writer, segment_state: SegmentState::new(session_id, segment_id, state.entries_count), - pwd: common.pwd, + cwd: common.cwd, workspace_root: common.workspace_root, scope: SharedScope::new(common.scope), delegation_scope: common.delegation_scope, @@ -4584,12 +4667,12 @@ pub enum PodError { #[error(transparent)] Scope(ScopeError), - #[error("pwd is not readable under the configured scope: {}", .pwd.display())] - PwdOutsideScope { pwd: PathBuf }, + #[error("cwd is not readable under the configured scope: {}", .cwd.display())] + CwdOutsideScope { cwd: PathBuf }, - #[error("failed to resolve pwd {}: {source}", .pwd.display())] - InvalidPwd { - pwd: PathBuf, + #[error("failed to resolve cwd {}: {source}", .cwd.display())] + InvalidCwd { + cwd: PathBuf, #[source] source: std::io::Error, }, @@ -4671,12 +4754,12 @@ pub enum PodError { } /// Bundle of resources that every high-level Pod constructor needs: -/// tool pwd, runtime workspace root, scope, an LLM client, the prompt catalog, +/// cwd, runtime workspace root, scope, an LLM client, the prompt catalog, /// and (optionally) a parsed system-prompt template. Built once by -/// [`prepare_pod_common`] from the resolved manifest and then split into Pod +/// [`prepare_pod_common_with_context`] from the resolved manifest and then split into Pod /// fields. struct PodCommon { - pwd: PathBuf, + cwd: PathBuf, workspace_root: PathBuf, scope: Scope, delegation_scope: DelegationScope, @@ -4752,58 +4835,42 @@ fn delegated_write_rule_to_deny(rule: PodSpawnedScopeRule) -> Option (rule.permission == Permission::Write).then_some(rule) } -/// Resolve pwd / scope / LLM client / prompt catalog from a validated -/// manifest. Used by `from_manifest`, `from_manifest_spawned`, -/// and `restore_from_manifest` so they share one definition of "what -/// pieces fall out of a manifest". +/// Build the runtime pieces that are derivable directly from the resolved +/// manifest. Used by new, spawned, and restored Pods so they share one +/// definition of "what pieces fall out of a manifest". /// -/// `parse_template` controls whether the manifest's instruction is -/// parsed as a system-prompt template. New Pods always parse so the -/// template is rendered at first turn; restored Pods skip parsing -/// because the saved session log replays a previously-rendered -/// `system_prompt` verbatim. -fn prepare_pod_common( - manifest: &PodManifest, - loader: &PromptLoader, - parse_template: bool, -) -> Result { - let pwd = current_pwd()?; - let workspace_root = pwd.clone(); - let scope = build_scope_with_memory(manifest, &workspace_root)?; - prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope) -} - -fn prepare_pod_common_with_scope( - manifest: &PodManifest, - loader: &PromptLoader, - parse_template: bool, - scope_config: ScopeConfig, -) -> Result { - let pwd = current_pwd()?; - let workspace_root = pwd.clone(); - let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?; - prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope) -} - +/// `parse_template` controls whether the manifest's instruction is parsed as a +/// system-prompt template. New Pods always parse so the template is rendered at +/// first turn; restored Pods skip parsing because the saved session log replays +/// a previously-rendered `system_prompt` verbatim. fn prepare_pod_common_with_context( manifest: &PodManifest, loader: &PromptLoader, parse_template: bool, workspace_root: PathBuf, - pwd: PathBuf, + cwd: PathBuf, scope_config: ScopeConfig, ) -> Result { let workspace_root = - std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidPwd { - pwd: workspace_root.clone(), + std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidCwd { + cwd: workspace_root.clone(), source, })?; - let pwd = std::fs::canonicalize(&pwd).map_err(|source| PodError::InvalidPwd { - pwd: pwd.clone(), + let cwd = std::fs::canonicalize(&cwd).map_err(|source| PodError::InvalidCwd { + cwd: cwd.clone(), source, })?; + let mut scope_config = scope_config; + if let Some(mem) = manifest.memory.as_ref() { + let layout = memory::WorkspaceLayout::resolve(mem, &workspace_root); + scope_config.deny.extend(memory::deny_write_rules(&layout)); + scope_config + .deny + .extend(workflow_crate::deny_write_rules(&layout)); + } + scope_config.allow.extend(skill_dir_read_rules(manifest)); let scope = Scope::from_config(&scope_config).map_err(PodError::Scope)?; - prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, pwd, scope) + prepare_pod_common_from_scope(manifest, loader, parse_template, workspace_root, cwd, scope) } fn prepare_pod_common_from_scope( @@ -4811,16 +4878,16 @@ fn prepare_pod_common_from_scope( loader: &PromptLoader, parse_template: bool, workspace_root: PathBuf, - pwd: PathBuf, + cwd: PathBuf, scope: Scope, ) -> Result { if !scope.is_readable(&workspace_root) { - return Err(PodError::PwdOutsideScope { - pwd: workspace_root, + return Err(PodError::CwdOutsideScope { + cwd: workspace_root, }); } - if !scope.is_readable(&pwd) { - return Err(PodError::PwdOutsideScope { pwd }); + if !scope.is_readable(&cwd) { + return Err(PodError::CwdOutsideScope { cwd }); } let delegation_scope = DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?; @@ -4847,7 +4914,7 @@ fn prepare_pod_common_from_scope( }; Ok(PodCommon { - pwd, + cwd, workspace_root, scope, delegation_scope, @@ -4900,28 +4967,6 @@ where } } -/// Build the Pod's runtime [`Scope`] from the manifest, layering the -/// memory subsystem's deny-write rules on top when `[memory]` is -/// present, and read-allow rules for any external Agent Skills -/// directories ingested. The deny rules cap generic CRUD tools so they -/// cannot touch `/memory/` or `/knowledge/` while -/// the memory tools (registered separately) bypass `ScopedFs` and write -/// through `std::fs` directly. Skill directories are added at -/// `Permission::Read` so the agent can `Read` `scripts/` / `references/` -/// / `assets/` referenced by the Workflow body. -fn build_scope_with_memory(manifest: &PodManifest, pwd: &Path) -> Result { - let mut scope_config = manifest.scope.clone(); - if let Some(mem) = manifest.memory.as_ref() { - let layout = memory::WorkspaceLayout::resolve(mem, pwd); - scope_config.deny.extend(memory::deny_write_rules(&layout)); - scope_config - .deny - .extend(workflow_crate::deny_write_rules(&layout)); - } - scope_config.allow.extend(skill_dir_read_rules(manifest)); - Scope::from_config(&scope_config).map_err(PodError::Scope) -} - /// Allow-rules granting `Read` access to every skill directory the Pod /// will ingest from the manifest's `[skills] directories`. Returned /// rules are recursive so the entire skill bundle (`SKILL.md` + @@ -4941,17 +4986,17 @@ fn skill_dir_read_rules(manifest: &PodManifest) -> Vec { .collect() } -/// Snapshot the process's current working directory as the Pod's pwd, +/// Snapshot the process's current working directory as the Pod's cwd, /// canonicalising symlinks and any `.`/`..` components. The Pod keeps /// this value for its lifetime; changes to the process-wide cwd after /// construction do not affect scope checks or the system prompt. -fn current_pwd() -> Result { - let cwd = std::env::current_dir().map_err(|source| PodError::InvalidPwd { - pwd: PathBuf::from("."), +fn current_cwd() -> Result { + let cwd = std::env::current_dir().map_err(|source| PodError::InvalidCwd { + cwd: PathBuf::from("."), source, })?; cwd.canonicalize() - .map_err(|source| PodError::InvalidPwd { pwd: cwd, source }) + .map_err(|source| PodError::InvalidCwd { cwd: cwd, source }) } #[cfg(test)] @@ -4962,18 +5007,18 @@ mod spawned_context_tests { fn spawn_pod_context_keeps_workspace_root_separate_from_tool_pwd() { let tmp = tempfile::tempdir().unwrap(); let workspace_root = tmp.path().join("workspace-root"); - let tool_cwd = tmp.path().join("child-worktree"); + let cwd = tmp.path().join("child-worktree"); std::fs::create_dir_all(&workspace_root).unwrap(); - std::fs::create_dir_all(&tool_cwd).unwrap(); + std::fs::create_dir_all(&cwd).unwrap(); - let mut manifest = minimal_manifest_for_context_test(&workspace_root, &tool_cwd); + let mut manifest = minimal_manifest_for_context_test(&workspace_root, &cwd); manifest.memory = Some(manifest::MemoryConfig::default()); let common = prepare_pod_common_with_context( &manifest, &PromptLoader::builtins_only(), false, workspace_root.clone(), - tool_cwd.clone(), + cwd.clone(), manifest.scope.clone(), ) .unwrap(); @@ -4982,14 +5027,14 @@ mod spawned_context_tests { common.workspace_root, workspace_root.canonicalize().unwrap() ); - assert_eq!(common.pwd, tool_cwd.canonicalize().unwrap()); + assert_eq!(common.cwd, cwd.canonicalize().unwrap()); assert_eq!( common.memory_layout.as_ref().unwrap().root(), workspace_root.canonicalize().unwrap() ); } - fn minimal_manifest_for_context_test(workspace_root: &Path, tool_cwd: &Path) -> PodManifest { + fn minimal_manifest_for_context_test(workspace_root: &Path, cwd: &Path) -> PodManifest { let toml_str = format!( r#" [pod] @@ -5010,7 +5055,7 @@ target = "{}" permission = "write" "#, workspace_root.display(), - tool_cwd.display() + cwd.display() ); let mut manifest = PodManifest::from_toml(&toml_str).unwrap(); manifest.model.auth = Some(manifest::AuthRef::None); @@ -5226,10 +5271,10 @@ mod build_summary_prompt_tests { let dir = tempfile::tempdir().unwrap(); let manifest = minimal_manifest_with_skills(vec![]); let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); - let pwd = dir.path().join("workspace"); - std::fs::create_dir_all(&pwd).unwrap(); - let scope = Scope::writable(&pwd).unwrap(); - let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope) + let cwd = dir.path().join("workspace"); + std::fs::create_dir_all(&cwd).unwrap(); + let scope = Scope::writable(&cwd).unwrap(); + let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope) .await .unwrap(); pod.ensure_segment_head().unwrap(); @@ -5371,10 +5416,10 @@ mod build_summary_prompt_tests { let dir = tempfile::tempdir().unwrap(); let manifest = minimal_manifest_with_skills(vec![]); let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); - let pwd = dir.path().join("workspace"); - std::fs::create_dir_all(&pwd).unwrap(); - let scope = Scope::writable(&pwd).unwrap(); - let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope) + let cwd = dir.path().join("workspace"); + std::fs::create_dir_all(&cwd).unwrap(); + let scope = Scope::writable(&cwd).unwrap(); + let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope) .await .unwrap(); @@ -5474,24 +5519,24 @@ mod build_summary_prompt_tests { ) -> String { let dir = tempfile::tempdir().unwrap(); let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); - let pwd = dir.path().join("workspace"); - std::fs::create_dir_all(&pwd).unwrap(); + let cwd = dir.path().join("workspace"); + std::fs::create_dir_all(&cwd).unwrap(); if let Some(doc) = summary_doc { - std::fs::create_dir_all(pwd.join(".yoi/memory")).unwrap(); - std::fs::write(pwd.join(".yoi/memory/summary.md"), doc).unwrap(); + std::fs::create_dir_all(cwd.join(".yoi/memory")).unwrap(); + std::fs::write(cwd.join(".yoi/memory/summary.md"), doc).unwrap(); } if include_knowledge { - std::fs::create_dir_all(pwd.join(".yoi/knowledge")).unwrap(); + std::fs::create_dir_all(cwd.join(".yoi/knowledge")).unwrap(); std::fs::write( - pwd.join(".yoi/knowledge/resident-policy.md"), + cwd.join(".yoi/knowledge/resident-policy.md"), knowledge_doc("knowledge resident desc"), ) .unwrap(); } if include_workflow { - std::fs::create_dir_all(pwd.join(".yoi/workflow")).unwrap(); + std::fs::create_dir_all(cwd.join(".yoi/workflow")).unwrap(); std::fs::write( - pwd.join(".yoi/workflow/resident-flow.md"), + cwd.join(".yoi/workflow/resident-flow.md"), workflow_doc("workflow resident desc"), ) .unwrap(); @@ -5499,15 +5544,15 @@ mod build_summary_prompt_tests { let mut manifest = minimal_manifest_with_skills(vec![]); manifest.memory = memory_config; - let scope = Scope::writable(&pwd).unwrap(); - let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd.clone(), scope) + let scope = Scope::writable(&cwd).unwrap(); + let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd.clone(), scope) .await .unwrap(); pod.memory_layout = pod .manifest .memory .as_ref() - .map(|mem| memory::WorkspaceLayout::resolve(mem, &pwd)); + .map(|mem| memory::WorkspaceLayout::resolve(mem, &cwd)); if let Some(layout) = pod.memory_layout.as_ref() { pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap(); } diff --git a/crates/pod/src/spawn/tool.rs b/crates/pod/src/spawn/tool.rs index 713c9757..bc345438 100644 --- a/crates/pod/src/spawn/tool.rs +++ b/crates/pod/src/spawn/tool.rs @@ -228,8 +228,8 @@ pub struct SpawnPodTool { /// memory context. SpawnPod `cwd` must not affect this value. workspace_root: PathBuf, /// Directory the spawned Pod's tools should use when the LLM did not - /// override it. Defaults to the spawner's tool pwd. - spawner_pwd: PathBuf, + /// override it. Defaults to the spawner's cwd. + spawner_cwd: PathBuf, /// Optional typed runtime command injected by tests. Production resolves /// the runtime command from `std::env::current_exe()` at launch time. runtime_command: Option, @@ -270,7 +270,7 @@ impl SpawnPodTool { callback_socket: PathBuf, runtime_base: PathBuf, workspace_root: PathBuf, - spawner_pwd: PathBuf, + spawner_cwd: PathBuf, registry: Arc, parent_socket: Option, spawner_manifest: PodManifest, @@ -284,7 +284,7 @@ impl SpawnPodTool { callback_socket, runtime_base, workspace_root, - spawner_pwd, + spawner_cwd, runtime_command, registry, parent_socket, @@ -318,7 +318,7 @@ impl Tool for SpawnPodTool { let scope_allow = parse_scope(&input.scope)?; self.validate_delegation_scope(&scope_allow)?; - let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_pwd)?; + let child_cwd = validate_spawn_cwd(input.cwd.as_deref(), &scope_allow, &self.spawner_cwd)?; let spawn_selector = parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| { @@ -481,9 +481,7 @@ impl SpawnPodTool { .arg(spawn_config_json) .arg("--workspace") .arg(&self.workspace_root) - .arg("--tool-cwd") - .arg(child_cwd) - .current_dir(&self.workspace_root) + .current_dir(child_cwd) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::from(stderr_file)) @@ -881,7 +879,7 @@ pub fn spawn_pod_tool( callback_socket: PathBuf, runtime_base: PathBuf, workspace_root: PathBuf, - spawner_pwd: PathBuf, + spawner_cwd: PathBuf, registry: Arc, parent_socket: Option, spawner_manifest: PodManifest, @@ -893,7 +891,7 @@ pub fn spawn_pod_tool( callback_socket, runtime_base, workspace_root, - spawner_pwd, + spawner_cwd, registry, parent_socket, spawner_manifest, @@ -909,7 +907,7 @@ pub fn spawn_pod_tool_with_runtime_command( callback_socket: PathBuf, runtime_base: PathBuf, workspace_root: PathBuf, - spawner_pwd: PathBuf, + spawner_cwd: PathBuf, registry: Arc, parent_socket: Option, spawner_manifest: PodManifest, @@ -922,7 +920,7 @@ pub fn spawn_pod_tool_with_runtime_command( callback_socket, runtime_base, workspace_root, - spawner_pwd, + spawner_cwd, registry, parent_socket, spawner_manifest, @@ -937,7 +935,7 @@ fn spawn_pod_tool_impl( callback_socket: PathBuf, runtime_base: PathBuf, workspace_root: PathBuf, - spawner_pwd: PathBuf, + spawner_cwd: PathBuf, registry: Arc, parent_socket: Option, spawner_manifest: PodManifest, @@ -969,7 +967,7 @@ fn spawn_pod_tool_impl( callback_socket.clone(), runtime_base.clone(), workspace_root.clone(), - spawner_pwd.clone(), + spawner_cwd.clone(), registry.clone(), parent_socket.clone(), spawner_manifest.clone(), diff --git a/crates/pod/tests/spawn_pod_test.rs b/crates/pod/tests/spawn_pod_test.rs index 56a260cb..db8e15b5 100644 --- a/crates/pod/tests/spawn_pod_test.rs +++ b/crates/pod/tests/spawn_pod_test.rs @@ -270,7 +270,7 @@ fn clear_env() { } #[tokio::test] -async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() { +async fn spawn_pod_launches_runtime_in_workspace_and_process_cwd() { let _env = EnvGuard::acquire(); let allow_root = TempDir::new().unwrap(); @@ -315,7 +315,7 @@ async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() { tool.execute(&input, Default::default()).await.unwrap(); assert!(matches!(received.await.unwrap(), Some(Method::Run { .. }))); let invocation = read_recorded_runtime_invocation(&output_path).await; - assert_eq!(invocation[0], allow_root.path().to_str().unwrap()); + assert_eq!(invocation[0], child_cwd.to_str().unwrap()); assert!( invocation .windows(2) @@ -323,17 +323,15 @@ async fn spawn_pod_launches_runtime_in_workspace_and_passes_tool_cwd() { "invocation should carry inherited workspace root: {invocation:?}" ); assert!( - invocation - .windows(2) - .any(|pair| pair[0] == "--tool-cwd" && pair[1] == child_cwd.to_str().unwrap()), - "invocation should carry tool cwd separately: {invocation:?}" + !invocation.iter().any(|arg| arg == "--tool-cwd"), + "cwd should be process current directory, not a runtime argument: {invocation:?}" ); clear_env(); } #[tokio::test] -async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() { +async fn spawn_pod_omitted_cwd_preserves_spawner_cwd() { let _env = EnvGuard::acquire(); let allow_root = TempDir::new().unwrap(); @@ -378,10 +376,8 @@ async fn spawn_pod_omitted_cwd_preserves_spawner_pwd() { let invocation = read_recorded_runtime_invocation(&output_path).await; assert_eq!(invocation[0], allow_root.path().to_str().unwrap()); assert!( - invocation - .windows(2) - .any(|pair| pair[0] == "--tool-cwd" && pair[1] == allow_root.path().to_str().unwrap()), - "omitted cwd should preserve spawner pwd as tool cwd: {invocation:?}" + !invocation.iter().any(|arg| arg == "--tool-cwd"), + "omitted cwd should preserve spawner cwd as process cwd: {invocation:?}" ); clear_env(); diff --git a/crates/tools/src/bash.rs b/crates/tools/src/bash.rs index 352d5d7b..95bc3517 100644 --- a/crates/tools/src/bash.rs +++ b/crates/tools/src/bash.rs @@ -76,7 +76,7 @@ pub(crate) struct BashParams { pub(crate) struct BashTool { /// Workspace root that every invocation starts in. Snapshot of - /// `ScopedFs::pwd()` at registration time; never mutated, since we + /// `ScopedFs::cwd()` at registration time; never mutated, since we /// don't track `cd` across calls. cwd: PathBuf, /// Directory to spill long outputs into. Caller is expected to have @@ -329,7 +329,7 @@ fn shell_single_quote(s: &str) -> String { /// /// `output_dir` is where long outputs spill to; the caller is responsible /// for arranging that the path is in the agent's readable scope. Every -/// invocation starts at `fs.pwd()` — the tool is intentionally stateless +/// invocation starts at `fs.cwd()` — the tool is intentionally stateless /// w.r.t. the working directory. pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition { Arc::new(move || { @@ -339,7 +339,7 @@ pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition { .description(DESCRIPTION) .input_schema(schema_value); let tool: Arc = Arc::new(BashTool { - cwd: fs.pwd().to_path_buf(), + cwd: fs.cwd().to_path_buf(), output_dir: output_dir.clone(), spilled_outputs: std::sync::Mutex::new(Vec::new()), }); diff --git a/crates/tools/src/glob.rs b/crates/tools/src/glob.rs index d9f9efea..0cd47537 100644 --- a/crates/tools/src/glob.rs +++ b/crates/tools/src/glob.rs @@ -52,7 +52,7 @@ impl Tool for GlobTool { let base = params .path .clone() - .unwrap_or_else(|| self.fs.pwd().to_path_buf()); + .unwrap_or_else(|| self.fs.cwd().to_path_buf()); let pattern = params.pattern.clone(); let scope = self.fs.scope().clone(); diff --git a/crates/tools/src/grep.rs b/crates/tools/src/grep.rs index 8d4a63e2..35672dbb 100644 --- a/crates/tools/src/grep.rs +++ b/crates/tools/src/grep.rs @@ -96,7 +96,7 @@ impl Tool for GrepTool { "Grep" ); - let default_base = self.fs.pwd().to_path_buf(); + let default_base = self.fs.cwd().to_path_buf(); let scope = self.fs.scope().clone(); let report = tokio::task::spawn_blocking(move || run_grep(default_base, params, &scope)) .await diff --git a/crates/tools/src/scoped_fs.rs b/crates/tools/src/scoped_fs.rs index 72d9d892..5ab02f49 100644 --- a/crates/tools/src/scoped_fs.rs +++ b/crates/tools/src/scoped_fs.rs @@ -2,7 +2,7 @@ //! //! `ScopedFs` is the write/read gate layered on top of a [`manifest::Scope`] //! and a Pod's working directory. The scope decides which paths are -//! readable and writable; the pwd is carried alongside for convenience +//! readable and writable; the cwd is carried alongside for convenience //! (Glob/Grep default their search base to it). //! //! `ScopedFs` is cheap to clone (`Arc` inside) and carries no per-session @@ -20,7 +20,7 @@ use crate::error::ToolsError; #[derive(Debug)] struct ScopedFsInner { scope: SharedScope, - pwd: PathBuf, + cwd: PathBuf, } /// Scope-aware filesystem handle. Clone-cheap (`Arc` inside). @@ -60,20 +60,20 @@ pub struct SymlinkInfo { } impl ScopedFs { - /// Create a new [`ScopedFs`] wrapping `scope` and `pwd` in a fresh + /// Create a new [`ScopedFs`] wrapping `scope` and `cwd` in a fresh /// [`SharedScope`]. Use [`ScopedFs::with_shared_scope`] when you /// need the resulting `ScopedFs` to share scope state with another /// holder of the `SharedScope` (typically the Pod). - pub fn new(scope: Scope, pwd: PathBuf) -> Self { - Self::with_shared_scope(SharedScope::new(scope), pwd) + pub fn new(scope: Scope, cwd: PathBuf) -> Self { + Self::with_shared_scope(SharedScope::new(scope), cwd) } /// Build a [`ScopedFs`] over an existing [`SharedScope`]. The /// resulting handle and any future updates the caller pushes to /// `scope` are observed by every clone of this `ScopedFs`. - pub fn with_shared_scope(scope: SharedScope, pwd: PathBuf) -> Self { + pub fn with_shared_scope(scope: SharedScope, cwd: PathBuf) -> Self { Self { - inner: Arc::new(ScopedFsInner { scope, pwd }), + inner: Arc::new(ScopedFsInner { scope, cwd }), } } @@ -93,8 +93,8 @@ impl ScopedFs { /// The Pod's working directory. Glob/Grep default their search base /// to this path when callers omit an explicit `path` parameter. - pub fn pwd(&self) -> &Path { - &self.inner.pwd + pub fn cwd(&self) -> &Path { + &self.inner.cwd } // ========================================================================= diff --git a/crates/tui/src/multi_pod.rs b/crates/tui/src/multi_pod.rs index 97083175..a69cb625 100644 --- a/crates/tui/src/multi_pod.rs +++ b/crates/tui/src/multi_pod.rs @@ -1693,9 +1693,10 @@ fn build_orchestrator_launch_context( pod_name: &str, ) -> TicketRoleLaunchContext { let mut context = TicketRoleLaunchContext::new( - orchestration_workspace_root.to_path_buf(), + original_workspace_root.to_path_buf(), TicketRole::Orchestrator, ) + .with_cwd(orchestration_workspace_root.to_path_buf()) .with_original_workspace_root(original_workspace_root.to_path_buf()) .with_target_workspace_root(original_workspace_root.to_path_buf()); context.pod_name = Some(pod_name.to_string()); @@ -1996,6 +1997,7 @@ async fn orchestrator_lifecycle( match prepare_orchestration_worktree_for_restore(workspace_root) { Ok(worktree) => { match restore_orchestrator_pod( + workspace_root, &worktree.layout.path, &pod_name, runtime_command.clone(), @@ -2083,6 +2085,7 @@ async fn restore_workspace_companion_pod( profile: None, ticket_role: None, workspace_root: workspace_root.to_path_buf(), + cwd: None, resume_from: None, }; spawn_pod(config, |_| {}).await.map(|_| ()) @@ -2099,12 +2102,14 @@ async fn spawn_workspace_companion_pod( profile: None, ticket_role: None, workspace_root: workspace_root.to_path_buf(), + cwd: None, resume_from: None, }; spawn_pod(config, |_| {}).await.map(|_| ()) } async fn restore_orchestrator_pod( + original_workspace_root: &Path, workspace_root: &Path, pod_name: &str, runtime_command: PodRuntimeCommand, @@ -2113,8 +2118,9 @@ async fn restore_orchestrator_pod( runtime_command, pod_name: pod_name.to_string(), profile: None, - ticket_role: None, - workspace_root: workspace_root.to_path_buf(), + ticket_role: Some("orchestrator".to_string()), + workspace_root: original_workspace_root.to_path_buf(), + cwd: Some(workspace_root.to_path_buf()), resume_from: None, }; spawn_pod(config, |_| {}).await.map(|_| ()) diff --git a/crates/tui/src/spawn.rs b/crates/tui/src/spawn.rs index 70eedf64..d3df30ae 100644 --- a/crates/tui/src/spawn.rs +++ b/crates/tui/src/spawn.rs @@ -380,6 +380,7 @@ async fn wait_for_ready( profile: form.selected_profile_selector(), ticket_role: None, workspace_root: form.cwd.clone(), + cwd: None, resume_from: form.resume_from, }; let ready = spawn_pod(config, |line| {