fix: separate workspace root from cwd

This commit is contained in:
Keisuke Hirata 2026-06-12 01:03:33 +09:00
parent 23a5b53807
commit 7eff9301b9
No known key found for this signature in database
14 changed files with 314 additions and 254 deletions

View File

@ -35,10 +35,14 @@ pub struct SpawnConfig {
/// Process-local Ticket role marker supplied only by Ticket role launches. /// Process-local Ticket role marker supplied only by Ticket role launches.
/// This does not alter prompts, manifests, or Ticket claim records. /// This does not alter prompts, manifests, or Ticket claim records.
pub ticket_role: Option<String>, pub ticket_role: Option<String>,
/// Explicit runtime workspace root. The child uses it as process cwd and /// Explicit runtime workspace root. The child receives it via
/// receives it via `--workspace` so startup does not infer workspace /// `--workspace` so startup does not infer workspace identity from the
/// identity from the parent process cwd. /// parent process cwd.
pub workspace_root: PathBuf, 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<PathBuf>,
/// `Some(id)` のとき `--session <id>` を付与し、当該セッションから /// `Some(id)` のとき `--session <id>` を付与し、当該セッションから
/// resume させる。 /// resume させる。
pub resume_from: Option<Uuid>, pub resume_from: Option<Uuid>,
@ -149,7 +153,7 @@ where
let mut command = Command::new(config.runtime_command.program()); let mut command = Command::new(config.runtime_command.program());
command command
.args(config.runtime_command.prefix_args()) .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()) .stdin(Stdio::null())
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::from(stderr_file)) .stderr(Stdio::from(stderr_file))
@ -335,6 +339,7 @@ mod tests {
profile: Some("project:companion".to_string()), profile: Some("project:companion".to_string()),
ticket_role: None, ticket_role: None,
workspace_root: PathBuf::from("/work/other-project"), workspace_root: PathBuf::from("/work/other-project"),
cwd: None,
resume_from: None, resume_from: None,
} }
} }
@ -372,9 +377,10 @@ mod tests {
} }
#[test] #[test]
fn runtime_args_pass_ticket_role_marker_when_present() { fn runtime_args_do_not_include_child_cwd() {
let mut config = base_config(); 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!( assert_eq!(
runtime_args(&config), runtime_args(&config),
@ -386,7 +392,7 @@ mod tests {
"--profile", "--profile",
"project:companion", "project:companion",
"--ticket-role", "--ticket-role",
"intake", "orchestrator",
] ]
); );
} }

View File

@ -78,6 +78,7 @@ impl TicketIntakeHandoff {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketRoleLaunchContext { pub struct TicketRoleLaunchContext {
pub workspace_root: PathBuf, pub workspace_root: PathBuf,
pub cwd: Option<PathBuf>,
pub original_workspace_root: Option<PathBuf>, pub original_workspace_root: Option<PathBuf>,
pub target_workspace_root: Option<PathBuf>, pub target_workspace_root: Option<PathBuf>,
pub role: TicketRole, pub role: TicketRole,
@ -97,6 +98,7 @@ impl TicketRoleLaunchContext {
pub fn new(workspace_root: impl Into<PathBuf>, role: TicketRole) -> Self { pub fn new(workspace_root: impl Into<PathBuf>, role: TicketRole) -> Self {
Self { Self {
workspace_root: workspace_root.into(), workspace_root: workspace_root.into(),
cwd: None,
original_workspace_root: None, original_workspace_root: None,
target_workspace_root: None, target_workspace_root: None,
role, role,
@ -113,6 +115,11 @@ impl TicketRoleLaunchContext {
} }
} }
pub fn with_cwd(mut self, root: impl Into<PathBuf>) -> Self {
self.cwd = Some(root.into());
self
}
pub fn with_original_workspace_root(mut self, root: impl Into<PathBuf>) -> Self { pub fn with_original_workspace_root(mut self, root: impl Into<PathBuf>) -> Self {
self.original_workspace_root = Some(root.into()); self.original_workspace_root = Some(root.into());
self self
@ -144,6 +151,7 @@ impl TicketRoleLaunchContext {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct TicketRoleLaunchPlan { pub struct TicketRoleLaunchPlan {
pub workspace_root: PathBuf, pub workspace_root: PathBuf,
pub cwd: Option<PathBuf>,
pub original_workspace_root: PathBuf, pub original_workspace_root: PathBuf,
pub target_workspace_root: PathBuf, pub target_workspace_root: PathBuf,
pub implementation_worktree_root: PathBuf, pub implementation_worktree_root: PathBuf,
@ -175,6 +183,7 @@ impl TicketRoleLaunchPlan {
profile: Some(self.profile.clone()), profile: Some(self.profile.clone()),
ticket_role: Some(self.role.as_str().to_string()), ticket_role: Some(self.role.as_str().to_string()),
workspace_root: self.workspace_root.clone(), workspace_root: self.workspace_root.clone(),
cwd: self.cwd.clone(),
resume_from: None, resume_from: None,
}) })
} }
@ -285,6 +294,7 @@ pub fn plan_ticket_role_launch_with_config(
Ok(TicketRoleLaunchPlan { Ok(TicketRoleLaunchPlan {
workspace_root: context.workspace_root, workspace_root: context.workspace_root,
cwd: context.cwd,
original_workspace_root, original_workspace_root,
target_workspace_root, target_workspace_root,
implementation_worktree_root, implementation_worktree_root,
@ -678,6 +688,9 @@ fn append_workspace_routing_context(out: &mut String, context: &TicketRoleLaunch
"role_workspace_root", "role_workspace_root",
&context.workspace_root.display().to_string(), &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( push_bounded_bullet(
out, out,
"original_workspace_root", "original_workspace_root",
@ -875,6 +888,7 @@ mod tests {
fn test_launch_plan(workspace: &std::path::Path) -> TicketRoleLaunchPlan { fn test_launch_plan(workspace: &std::path::Path) -> TicketRoleLaunchPlan {
TicketRoleLaunchPlan { TicketRoleLaunchPlan {
workspace_root: workspace.to_path_buf(), workspace_root: workspace.to_path_buf(),
cwd: None,
original_workspace_root: workspace.to_path_buf(), original_workspace_root: workspace.to_path_buf(),
target_workspace_root: workspace.to_path_buf(), target_workspace_root: workspace.to_path_buf(),
implementation_worktree_root: workspace.join(".worktree"), implementation_worktree_root: workspace.join(".worktree"),
@ -1330,6 +1344,7 @@ workflow = "ticket-review-workflow"
.spawn_config(PodRuntimeCommand::for_executable("/bin/yoi")) .spawn_config(PodRuntimeCommand::for_executable("/bin/yoi"))
.unwrap(); .unwrap();
assert_eq!(spawn_config.workspace_root, temp.path()); assert_eq!(spawn_config.workspace_root, temp.path());
assert_eq!(spawn_config.cwd, None);
assert!(text.contains("Workspace routing context:")); assert!(text.contains("Workspace routing context:"));
assert!(text.contains("role_workspace_root")); assert!(text.contains("role_workspace_root"));

View File

@ -508,7 +508,7 @@ where
// Pod-immutable snapshots taken before the mutable worker borrow // Pod-immutable snapshots taken before the mutable worker borrow
// below so the worker borrow doesn't conflict with reads on `pod`. // below so the worker borrow doesn't conflict with reads on `pod`.
let scope_handle = pod.scope().clone(); 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 workspace_root = pod.workspace_root().to_path_buf();
let task_feature = pod.task_feature(); let task_feature = pod.task_feature();
let session_id_for_usage = pod.segment_id().to_string(); 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, // ScopedFs (builtin tools, fs_view, compact worker) reads from it,
// and any future scope mutation (SpawnPod-style revoke, future // and any future scope mutation (SpawnPod-style revoke, future
// GrantScope) propagates through it. // 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(); let tracker = tools::Tracker::new();
// Same ScopedFs also powers the IPC `ListCompletions` query — keep // Same ScopedFs also powers the IPC `ListCompletions` query — keep
// a clone for the FS view we attach below, since the tools consume // a clone for the FS view we attach below, since the tools consume
@ -607,7 +607,7 @@ where
spawner_socket, spawner_socket,
runtime_base.clone(), runtime_base.clone(),
workspace_root.clone(), workspace_root.clone(),
pwd.clone(), cwd.clone(),
spawned_registry.clone(), spawned_registry.clone(),
self_parent_socket, self_parent_socket,
spawner_manifest, spawner_manifest,
@ -618,7 +618,7 @@ where
worker.register_tool(read_pod_output_tool(spawned_registry.clone())); worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
worker.register_tool(stop_pod_tool(spawned_registry.clone())); worker.register_tool(stop_pod_tool(spawned_registry.clone()));
let discovery = 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(list_pods_tool(discovery.clone()));
worker.register_tool(restore_pod_tool(discovery.clone())); worker.register_tool(restore_pod_tool(discovery.clone()));
worker.register_tool(send_to_peer_pod_tool(discovery)); worker.register_tool(send_to_peer_pod_tool(discovery));
@ -664,7 +664,7 @@ async fn controller_loop<C, St>(
pod.store().clone(), pod.store().clone(),
spawner_name.clone(), spawner_name.clone(),
discovery_runtime_base, discovery_runtime_base,
pod.pwd().to_path_buf(), pod.cwd().to_path_buf(),
spawned_registry.clone(), spawned_registry.clone(),
); );
let mut pending: Option<PendingRun> = None; let mut pending: Option<PendingRun> = None;
@ -1292,7 +1292,7 @@ where
.collect(); .collect();
protocol::Greeting { protocol::Greeting {
pod_name: manifest.pod.name.clone(), pod_name: manifest.pod.name.clone(),
cwd: pod.pwd().display().to_string(), cwd: pod.cwd().display().to_string(),
provider: provider_name, provider: provider_name,
model: model_id, model: model_id,
scope_summary: pod.scope_snapshot().summary(), scope_summary: pod.scope_snapshot().summary(),

View File

@ -29,12 +29,6 @@ struct Cli {
#[arg(long, value_name = "PATH")] #[arg(long, value_name = "PATH")]
workspace: Option<PathBuf>, workspace: Option<PathBuf>,
/// 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<PathBuf>,
/// Manifest TOML to use directly as a one-file compatibility/debug input. /// Manifest TOML to use directly as a one-file compatibility/debug input.
/// This bypasses profile discovery but still applies builtin defaults and /// This bypasses profile discovery but still applies builtin defaults and
/// the same required-field validation boundary. /// the same required-field validation boundary.
@ -107,17 +101,6 @@ fn runtime_workspace_root(cli: &Cli) -> Result<PathBuf, String> {
} }
} }
fn runtime_tool_cwd(cli: &Cli, workspace_root: &Path) -> Result<PathBuf, String> {
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 { fn runtime_pod_name(cli: &Cli, workspace_root: &Path) -> String {
cli.pod cli.pod
.as_deref() .as_deref()
@ -300,6 +283,13 @@ fn exit_code_from_i32(code: i32) -> ExitCode {
} }
async fn run_cli_inner(cli: Cli) -> 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) { let workspace_root = match runtime_workspace_root(&cli) {
Ok(root) => root, Ok(root) => root,
Err(e) => { Err(e) => {
@ -314,15 +304,6 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE; 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 // Initialize persistent store. `paths::sessions_dir()` only
// returns None when none of YOI_HOME / YOI_DATA_DIR / // returns None when none of YOI_HOME / YOI_DATA_DIR /
// HOME is set — surface that as a hard error to match the // 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; 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( match Pod::from_manifest_spawned_with_context(
manifest, manifest,
store, store,
loader, loader,
callback, callback,
workspace_root.clone(), workspace_root.clone(),
tool_cwd.clone(), cwd.clone(),
) )
.await .await
{ {
Ok(p) => { Ok(p) => 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
}
Err(e) => { Err(e) => {
eprintln!("error: failed to create spawned pod: {e}"); eprintln!("error: failed to create spawned pod: {e}");
return ExitCode::FAILURE; return ExitCode::FAILURE;
@ -418,12 +383,14 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
}; };
match Pod::restore_from_manifest( match Pod::restore_from_manifest_with_context(
source_session_id, source_session_id,
source_segment_id, source_segment_id,
manifest, manifest,
store, store,
loader, loader,
workspace_root.clone(),
cwd.clone(),
) )
.await .await
{ {
@ -437,7 +404,16 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
manifest.pod.name = pod_name.to_string(); manifest.pod.name = pod_name.to_string();
match store.read_by_name(pod_name) { match store.read_by_name(pod_name) {
Ok(Some(_)) => { 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, Ok(p) => p,
Err(e) => { Err(e) => {
eprintln!("error: failed to restore pod {pod_name}: {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}"); eprintln!("error: pod state missing for {pod_name}");
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
Ok(None) => match Pod::from_manifest(manifest, store, loader).await { Ok(None) => {
Ok(p) => p, match Pod::from_manifest_with_context(
Err(e) => { manifest,
eprintln!("error: failed to create pod {pod_name}: {e}"); store,
return ExitCode::FAILURE; 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) => { Err(e) => {
eprintln!("error: failed to read pod state for {pod_name}: {e}"); eprintln!("error: failed to read pod state for {pod_name}: {e}");
return ExitCode::FAILURE; return ExitCode::FAILURE;
} }
} }
} else { } 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, Ok(p) => p,
Err(e) => { Err(e) => {
eprintln!("error: failed to create pod: {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)); pod.set_runtime_ticket_role(Some(role));
} }
let pod_name = pod.manifest().pod.name.clone(); let pod_name = pod.manifest().pod.name.clone();
// Spawn the controller (starts socket server) // Spawn the controller (starts socket server)
let runtime_base = match paths::runtime_dir() { let runtime_base = match paths::runtime_dir() {
Some(d) => d, Some(d) => d,

View File

@ -45,7 +45,7 @@ pub struct PodFsView {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileCandidate { pub struct FileCandidate {
/// 入力 prefix と整合する形のパスprefix が absolute なら absolute、 /// 入力 prefix と整合する形のパスprefix が absolute なら absolute、
/// relative なら pwd 相対)。 /// relative なら cwd 相対)。
pub path: String, pub path: String,
pub is_dir: bool, pub is_dir: bool,
} }
@ -114,7 +114,7 @@ impl PodFsView {
/// `path` を ScopedFs 経由で解決し、submit 時の `Segment::FileRef` /// `path` を ScopedFs 経由で解決し、submit 時の `Segment::FileRef`
/// attachment 用 system message を返す。 /// attachment 用 system message を返す。
/// ///
/// - `path` は relative なら pwd 相対、absolute なら absolute として解釈 /// - `path` は relative なら cwd 相対、absolute なら absolute として解釈
/// - 通常ディレクトリは浅い entry listing として `[Dir: <path>]\n<body>` に展開する /// - 通常ディレクトリは浅い entry listing として `[Dir: <path>]\n<body>` に展開する
/// - ディレクトリ listing は hidden / gitignore を特別扱いせず、scope 上 readable な /// - ディレクトリ listing は hidden / gitignore を特別扱いせず、scope 上 readable な
/// 直下 entry だけを最大 `DIR_FILE_REF_ENTRY_LIMIT` 件返す /// 直下 entry だけを最大 `DIR_FILE_REF_ENTRY_LIMIT` 件返す
@ -126,7 +126,7 @@ impl PodFsView {
let abs = if p.is_absolute() { let abs = if p.is_absolute() {
p.to_path_buf() p.to_path_buf()
} else { } else {
self.fs.pwd().join(p) self.fs.cwd().join(p)
}; };
// 通常ディレクトリだけを FileRef listing として扱う。symlink を含むパスは // 通常ディレクトリだけを FileRef listing として扱う。symlink を含むパスは
@ -163,16 +163,16 @@ impl PodFsView {
/// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。 /// `prefix` にマッチするファイル / ディレクトリを scope 内で浅く列挙する。
/// ///
/// - `prefix` が空 or `pwd` 相対のときは pwd 直下を見る /// - `prefix` が空 or `cwd` 相対のときは cwd 直下を見る
/// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙 /// - `prefix` が末尾 `/` のときはそのディレクトリ直下を全列挙
/// - 末尾が名前部分のときは、その名前を starts_with でフィルタ /// - 末尾が名前部分のときは、その名前を starts_with でフィルタ
/// - scope 上 readable なエントリのみ返す /// - scope 上 readable なエントリのみ返す
/// - ディレクトリ → ファイル の順、各グループ内は名前昇順 /// - ディレクトリ → ファイル の順、各グループ内は名前昇順
/// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない) /// - 上限 `COMPLETION_LIMIT` 件で打ち切り(深い列挙はしない)
pub fn list_file_completions(&self, prefix: &str) -> Vec<FileCandidate> { pub fn list_file_completions(&self, prefix: &str) -> Vec<FileCandidate> {
let pwd = self.fs.pwd(); let cwd = self.fs.cwd();
let scope = self.fs.scope(); 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) { let read_dir = match std::fs::read_dir(&dir) {
Ok(rd) => rd, Ok(rd) => rd,
@ -194,7 +194,7 @@ impl PodFsView {
let display = if is_absolute { let display = if is_absolute {
path.display().to_string() path.display().to_string()
} else { } else {
path.strip_prefix(pwd) path.strip_prefix(cwd)
.map(|p| p.display().to_string()) .map(|p| p.display().to_string())
.unwrap_or_else(|_| path.display().to_string()) .unwrap_or_else(|_| path.display().to_string())
}; };
@ -343,7 +343,7 @@ fn format_range(offset: Option<usize>, limit: Option<usize>) -> 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 is_absolute = Path::new(prefix).is_absolute();
let p = Path::new(prefix); let p = Path::new(prefix);
let (parent, name) = if prefix.is_empty() || prefix.ends_with('/') { 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 { let dir = if is_absolute {
parent parent
} else if parent.as_os_str().is_empty() { } else if parent.as_os_str().is_empty() {
pwd.to_path_buf() cwd.to_path_buf()
} else { } else {
pwd.join(parent) cwd.join(parent)
}; };
(dir, name, is_absolute) (dir, name, is_absolute)
} }

View File

@ -232,7 +232,7 @@ pub struct Pod<C: LlmClient, St: Store> {
/// wrapper over `segment_state.segment_id()`. /// wrapper over `segment_state.segment_id()`.
segment_state: Arc<SegmentState>, segment_state: Arc<SegmentState>,
/// Absolute tool/process working directory of the Pod. /// Absolute tool/process working directory of the Pod.
pwd: PathBuf, cwd: PathBuf,
/// Absolute runtime workspace root used for project records, workflow, /// Absolute runtime workspace root used for project records, workflow,
/// memory, Ticket config, Profile context, and spawned-child inheritance. /// memory, Ticket config, Profile context, and spawned-child inheritance.
workspace_root: PathBuf, workspace_root: PathBuf,
@ -423,7 +423,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
store: self.store.clone(), store: self.store.clone(),
pod_metadata_writer: None, pod_metadata_writer: None,
segment_state: self.segment_state.clone(), segment_state: self.segment_state.clone(),
pwd: self.pwd.clone(), cwd: self.cwd.clone(),
workspace_root: self.workspace_root.clone(), workspace_root: self.workspace_root.clone(),
scope: self.scope.clone(), scope: self.scope.clone(),
delegation_scope: self.delegation_scope.clone(), delegation_scope: self.delegation_scope.clone(),
@ -577,7 +577,7 @@ impl<C: LlmClient + Clone + 'static, St: Store + Clone + 'static> Pod<C, St> {
impl<C: LlmClient, St: Store> Pod<C, St> { impl<C: LlmClient, St: Store> Pod<C, St> {
/// Create a new Pod from a pre-built Worker and store. /// 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 /// — typically via [`Scope::from_config`] when coming from a
/// manifest, or [`Scope::writable`] in tests. /// manifest, or [`Scope::writable`] in tests.
/// ///
@ -589,7 +589,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
manifest: PodManifest, manifest: PodManifest,
worker: Worker<C>, worker: Worker<C>,
store: St, store: St,
pwd: PathBuf, cwd: PathBuf,
scope: Scope, scope: Scope,
) -> Result<Self, PodError> { ) -> Result<Self, PodError> {
// Segment creation is deferred to `ensure_segment_head` at first // Segment creation is deferred to `ensure_segment_head` at first
@ -606,8 +606,8 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
store, store,
pod_metadata_writer: None, pod_metadata_writer: None,
segment_state: SegmentState::new(session_id, segment_id, 0), segment_state: SegmentState::new(session_id, segment_id, 0),
workspace_root: pwd.clone(), workspace_root: cwd.clone(),
pwd, cwd,
scope: SharedScope::new(scope), scope: SharedScope::new(scope),
delegation_scope, delegation_scope,
hook_builder: HookRegistryBuilder::new(), hook_builder: HookRegistryBuilder::new(),
@ -718,11 +718,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
} }
/// The Pod's tool/process working directory. /// The Pod's tool/process working directory.
pub fn pwd(&self) -> &Path { pub fn cwd(&self) -> &Path {
&self.pwd &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. /// spawned children whose SpawnPod `cwd` only changes tool defaults.
pub fn workspace_root(&self) -> &Path { pub fn workspace_root(&self) -> &Path {
&self.workspace_root &self.workspace_root
@ -1325,7 +1325,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
let scope_snapshot = self.scope.snapshot(); let scope_snapshot = self.scope.snapshot();
let ctx = SystemPromptContext { let ctx = SystemPromptContext {
now: chrono::Utc::now(), now: chrono::Utc::now(),
cwd: &self.pwd, cwd: &self.cwd,
language: worker_language, language: worker_language,
scope: &scope_snapshot, scope: &scope_snapshot,
tool_names, tool_names,
@ -1562,7 +1562,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
fn resolve_file_refs(&self, segments: &[Segment]) -> Vec<SystemItem> { fn resolve_file_refs(&self, segments: &[Segment]) -> Vec<SystemItem> {
let view = crate::fs_view::PodFsView::new(tools::ScopedFs::with_shared_scope( let view = crate::fs_view::PodFsView::new(tools::ScopedFs::with_shared_scope(
self.scope.clone(), self.scope.clone(),
self.pwd.clone(), self.cwd.clone(),
)); ));
let mut out = Vec::new(); let mut out = Vec::new();
for seg in segments { for seg in segments {
@ -2461,11 +2461,11 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
auto_read_budget, 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 // with the main Pod (reads go through the same policy) but the
// Tracker is fresh — compact-time reads must not pollute the // Tracker is fresh — compact-time reads must not pollute the
// main session's recency list, which feeds `default_refs` above. // 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_tracker = tools::Tracker::new();
let summary_client: Box<dyn LlmClient> = self.build_compactor_client()?; let summary_client: Box<dyn LlmClient> = self.build_compactor_client()?;
let summary_system_prompt = self let summary_system_prompt = self
@ -3708,7 +3708,7 @@ where
/// process's `std::env::current_dir()` — callers that want a /// process's `std::env::current_dir()` — callers that want a
/// different cwd must `cd` before constructing the Pod (e.g. the /// different cwd must `cd` before constructing the Pod (e.g. the
/// `SpawnPod` tool sets `Command::current_dir` on the child). 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`. /// `manifest.scope`.
/// ///
/// `loader` is installed into the system-prompt template /// `loader` is installed into the system-prompt template
@ -3720,7 +3720,25 @@ where
store: St, store: St,
loader: PromptLoader, loader: PromptLoader,
) -> Result<Self, PodError> { ) -> Result<Self, PodError> {
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<Self, PodError> {
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); let skill_shadows = std::mem::take(&mut common.skill_shadows);
// Segment creation is deferred to the first run (see // Segment creation is deferred to the first run (see
@ -3757,7 +3775,7 @@ where
store, store,
pod_metadata_writer, pod_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, 0), segment_state: SegmentState::new(session_id, segment_id, 0),
pwd: common.pwd, cwd: common.cwd,
workspace_root: common.workspace_root, workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope), scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope, delegation_scope: common.delegation_scope,
@ -3814,14 +3832,14 @@ where
loader: PromptLoader, loader: PromptLoader,
callback_socket: PathBuf, callback_socket: PathBuf,
) -> Result<Self, PodError> { ) -> Result<Self, PodError> {
let pwd = current_pwd()?; let cwd = current_cwd()?;
Self::from_manifest_spawned_with_context( Self::from_manifest_spawned_with_context(
manifest, manifest,
store, store,
loader, loader,
callback_socket, callback_socket,
pwd.clone(), cwd.clone(),
pwd, cwd,
) )
.await .await
} }
@ -3832,14 +3850,14 @@ where
loader: PromptLoader, loader: PromptLoader,
callback_socket: PathBuf, callback_socket: PathBuf,
workspace_root: PathBuf, workspace_root: PathBuf,
tool_cwd: PathBuf, cwd: PathBuf,
) -> Result<Self, PodError> { ) -> Result<Self, PodError> {
let mut common = prepare_pod_common_with_context( let mut common = prepare_pod_common_with_context(
&manifest, &manifest,
&loader, &loader,
/* parse_template */ true, /* parse_template */ true,
workspace_root, workspace_root,
tool_cwd, cwd,
manifest.scope.clone(), manifest.scope.clone(),
)?; )?;
let skill_shadows = std::mem::take(&mut common.skill_shadows); let skill_shadows = std::mem::take(&mut common.skill_shadows);
@ -3865,7 +3883,7 @@ where
store, store,
pod_metadata_writer, pod_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, 0), segment_state: SegmentState::new(session_id, segment_id, 0),
pwd: common.pwd, cwd: common.cwd,
workspace_root: common.workspace_root, workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope), scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope, delegation_scope: common.delegation_scope,
@ -3917,6 +3935,26 @@ where
manifest: PodManifest, manifest: PodManifest,
store: St, store: St,
loader: PromptLoader, loader: PromptLoader,
) -> Result<Self, PodError> {
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<Self, PodError> { ) -> Result<Self, PodError> {
let metadata = let metadata =
store store
@ -3936,15 +3974,36 @@ where
session_id: active.session_id, session_id: active.session_id,
})?; })?;
let manifest = match metadata.resolved_manifest_snapshot { let manifest = match metadata.resolved_manifest_snapshot {
Some(snapshot) => serde_json::from_value(snapshot).map_err(|source| { Some(snapshot) => {
PodError::PodMetadataManifestSnapshot { let mut restored: PodManifest =
pod_name: pod_name.to_string(), serde_json::from_value(snapshot).map_err(|source| {
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, 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. /// Restore a Pod from an existing session log.
@ -3970,6 +4029,28 @@ where
manifest: PodManifest, manifest: PodManifest,
store: St, store: St,
loader: PromptLoader, loader: PromptLoader,
) -> Result<Self, PodError> {
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<Self, PodError> { ) -> Result<Self, PodError> {
// Read raw entries once so we can both reconstruct state and // Read raw entries once so we can both reconstruct state and
// seed the broadcast sink's mirror with the same prefix that // seed the broadcast sink's mirror with the same prefix that
@ -3982,10 +4063,12 @@ where
let mirror_entries: Vec<LogEntry> = raw_entries.clone(); let mirror_entries: Vec<LogEntry> = raw_entries.clone();
let scope_config = effective_restore_scope_config(&store, &manifest)?; 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, &manifest,
&loader, &loader,
/* parse_template */ false, /* parse_template */ false,
workspace_root,
cwd,
scope_config, scope_config,
)?; )?;
let skill_shadows = std::mem::take(&mut common.skill_shadows); let skill_shadows = std::mem::take(&mut common.skill_shadows);
@ -4045,7 +4128,7 @@ where
store, store,
pod_metadata_writer, pod_metadata_writer,
segment_state: SegmentState::new(session_id, segment_id, state.entries_count), segment_state: SegmentState::new(session_id, segment_id, state.entries_count),
pwd: common.pwd, cwd: common.cwd,
workspace_root: common.workspace_root, workspace_root: common.workspace_root,
scope: SharedScope::new(common.scope), scope: SharedScope::new(common.scope),
delegation_scope: common.delegation_scope, delegation_scope: common.delegation_scope,
@ -4584,12 +4667,12 @@ pub enum PodError {
#[error(transparent)] #[error(transparent)]
Scope(ScopeError), Scope(ScopeError),
#[error("pwd is not readable under the configured scope: {}", .pwd.display())] #[error("cwd is not readable under the configured scope: {}", .cwd.display())]
PwdOutsideScope { pwd: PathBuf }, CwdOutsideScope { cwd: PathBuf },
#[error("failed to resolve pwd {}: {source}", .pwd.display())] #[error("failed to resolve cwd {}: {source}", .cwd.display())]
InvalidPwd { InvalidCwd {
pwd: PathBuf, cwd: PathBuf,
#[source] #[source]
source: std::io::Error, source: std::io::Error,
}, },
@ -4671,12 +4754,12 @@ pub enum PodError {
} }
/// Bundle of resources that every high-level Pod constructor needs: /// 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 /// 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. /// fields.
struct PodCommon { struct PodCommon {
pwd: PathBuf, cwd: PathBuf,
workspace_root: PathBuf, workspace_root: PathBuf,
scope: Scope, scope: Scope,
delegation_scope: DelegationScope, delegation_scope: DelegationScope,
@ -4752,58 +4835,42 @@ fn delegated_write_rule_to_deny(rule: PodSpawnedScopeRule) -> Option<ScopeRule>
(rule.permission == Permission::Write).then_some(rule) (rule.permission == Permission::Write).then_some(rule)
} }
/// Resolve pwd / scope / LLM client / prompt catalog from a validated /// Build the runtime pieces that are derivable directly from the resolved
/// manifest. Used by `from_manifest`, `from_manifest_spawned`, /// manifest. Used by new, spawned, and restored Pods so they share one
/// and `restore_from_manifest` so they share one definition of "what /// definition of "what pieces fall out of a manifest".
/// pieces fall out of a manifest".
/// ///
/// `parse_template` controls whether the manifest's instruction is /// `parse_template` controls whether the manifest's instruction is parsed as a
/// parsed as a system-prompt template. New Pods always parse so the /// system-prompt template. New Pods always parse so the template is rendered at
/// template is rendered at first turn; restored Pods skip parsing /// first turn; restored Pods skip parsing because the saved session log replays
/// because the saved session log replays a previously-rendered /// a previously-rendered `system_prompt` verbatim.
/// `system_prompt` verbatim.
fn prepare_pod_common(
manifest: &PodManifest,
loader: &PromptLoader,
parse_template: bool,
) -> Result<PodCommon, PodError> {
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<PodCommon, PodError> {
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)
}
fn prepare_pod_common_with_context( fn prepare_pod_common_with_context(
manifest: &PodManifest, manifest: &PodManifest,
loader: &PromptLoader, loader: &PromptLoader,
parse_template: bool, parse_template: bool,
workspace_root: PathBuf, workspace_root: PathBuf,
pwd: PathBuf, cwd: PathBuf,
scope_config: ScopeConfig, scope_config: ScopeConfig,
) -> Result<PodCommon, PodError> { ) -> Result<PodCommon, PodError> {
let workspace_root = let workspace_root =
std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidPwd { std::fs::canonicalize(&workspace_root).map_err(|source| PodError::InvalidCwd {
pwd: workspace_root.clone(), cwd: workspace_root.clone(),
source, source,
})?; })?;
let pwd = std::fs::canonicalize(&pwd).map_err(|source| PodError::InvalidPwd { let cwd = std::fs::canonicalize(&cwd).map_err(|source| PodError::InvalidCwd {
pwd: pwd.clone(), cwd: cwd.clone(),
source, 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)?; 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( fn prepare_pod_common_from_scope(
@ -4811,16 +4878,16 @@ fn prepare_pod_common_from_scope(
loader: &PromptLoader, loader: &PromptLoader,
parse_template: bool, parse_template: bool,
workspace_root: PathBuf, workspace_root: PathBuf,
pwd: PathBuf, cwd: PathBuf,
scope: Scope, scope: Scope,
) -> Result<PodCommon, PodError> { ) -> Result<PodCommon, PodError> {
if !scope.is_readable(&workspace_root) { if !scope.is_readable(&workspace_root) {
return Err(PodError::PwdOutsideScope { return Err(PodError::CwdOutsideScope {
pwd: workspace_root, cwd: workspace_root,
}); });
} }
if !scope.is_readable(&pwd) { if !scope.is_readable(&cwd) {
return Err(PodError::PwdOutsideScope { pwd }); return Err(PodError::CwdOutsideScope { cwd });
} }
let delegation_scope = let delegation_scope =
DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?; DelegationScope::from_config(&manifest.delegation_scope).map_err(PodError::Scope)?;
@ -4847,7 +4914,7 @@ fn prepare_pod_common_from_scope(
}; };
Ok(PodCommon { Ok(PodCommon {
pwd, cwd,
workspace_root, workspace_root,
scope, scope,
delegation_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 `<workspace>/memory/` or `<workspace>/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<Scope, PodError> {
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 /// Allow-rules granting `Read` access to every skill directory the Pod
/// will ingest from the manifest's `[skills] directories`. Returned /// will ingest from the manifest's `[skills] directories`. Returned
/// rules are recursive so the entire skill bundle (`SKILL.md` + /// rules are recursive so the entire skill bundle (`SKILL.md` +
@ -4941,17 +4986,17 @@ fn skill_dir_read_rules(manifest: &PodManifest) -> Vec<ScopeRule> {
.collect() .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 /// canonicalising symlinks and any `.`/`..` components. The Pod keeps
/// this value for its lifetime; changes to the process-wide cwd after /// this value for its lifetime; changes to the process-wide cwd after
/// construction do not affect scope checks or the system prompt. /// construction do not affect scope checks or the system prompt.
fn current_pwd() -> Result<PathBuf, PodError> { fn current_cwd() -> Result<PathBuf, PodError> {
let cwd = std::env::current_dir().map_err(|source| PodError::InvalidPwd { let cwd = std::env::current_dir().map_err(|source| PodError::InvalidCwd {
pwd: PathBuf::from("."), cwd: PathBuf::from("."),
source, source,
})?; })?;
cwd.canonicalize() cwd.canonicalize()
.map_err(|source| PodError::InvalidPwd { pwd: cwd, source }) .map_err(|source| PodError::InvalidCwd { cwd: cwd, source })
} }
#[cfg(test)] #[cfg(test)]
@ -4962,18 +5007,18 @@ mod spawned_context_tests {
fn spawn_pod_context_keeps_workspace_root_separate_from_tool_pwd() { fn spawn_pod_context_keeps_workspace_root_separate_from_tool_pwd() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let workspace_root = tmp.path().join("workspace-root"); 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(&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()); manifest.memory = Some(manifest::MemoryConfig::default());
let common = prepare_pod_common_with_context( let common = prepare_pod_common_with_context(
&manifest, &manifest,
&PromptLoader::builtins_only(), &PromptLoader::builtins_only(),
false, false,
workspace_root.clone(), workspace_root.clone(),
tool_cwd.clone(), cwd.clone(),
manifest.scope.clone(), manifest.scope.clone(),
) )
.unwrap(); .unwrap();
@ -4982,14 +5027,14 @@ mod spawned_context_tests {
common.workspace_root, common.workspace_root,
workspace_root.canonicalize().unwrap() workspace_root.canonicalize().unwrap()
); );
assert_eq!(common.pwd, tool_cwd.canonicalize().unwrap()); assert_eq!(common.cwd, cwd.canonicalize().unwrap());
assert_eq!( assert_eq!(
common.memory_layout.as_ref().unwrap().root(), common.memory_layout.as_ref().unwrap().root(),
workspace_root.canonicalize().unwrap() 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!( let toml_str = format!(
r#" r#"
[pod] [pod]
@ -5010,7 +5055,7 @@ target = "{}"
permission = "write" permission = "write"
"#, "#,
workspace_root.display(), workspace_root.display(),
tool_cwd.display() cwd.display()
); );
let mut manifest = PodManifest::from_toml(&toml_str).unwrap(); let mut manifest = PodManifest::from_toml(&toml_str).unwrap();
manifest.model.auth = Some(manifest::AuthRef::None); manifest.model.auth = Some(manifest::AuthRef::None);
@ -5226,10 +5271,10 @@ mod build_summary_prompt_tests {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let manifest = minimal_manifest_with_skills(vec![]); let manifest = minimal_manifest_with_skills(vec![]);
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace"); let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap(); std::fs::create_dir_all(&cwd).unwrap();
let scope = Scope::writable(&pwd).unwrap(); let scope = Scope::writable(&cwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope) let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope)
.await .await
.unwrap(); .unwrap();
pod.ensure_segment_head().unwrap(); pod.ensure_segment_head().unwrap();
@ -5371,10 +5416,10 @@ mod build_summary_prompt_tests {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let manifest = minimal_manifest_with_skills(vec![]); let manifest = minimal_manifest_with_skills(vec![]);
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace"); let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap(); std::fs::create_dir_all(&cwd).unwrap();
let scope = Scope::writable(&pwd).unwrap(); let scope = Scope::writable(&cwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd, scope) let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd, scope)
.await .await
.unwrap(); .unwrap();
@ -5474,24 +5519,24 @@ mod build_summary_prompt_tests {
) -> String { ) -> String {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap();
let pwd = dir.path().join("workspace"); let cwd = dir.path().join("workspace");
std::fs::create_dir_all(&pwd).unwrap(); std::fs::create_dir_all(&cwd).unwrap();
if let Some(doc) = summary_doc { if let Some(doc) = summary_doc {
std::fs::create_dir_all(pwd.join(".yoi/memory")).unwrap(); std::fs::create_dir_all(cwd.join(".yoi/memory")).unwrap();
std::fs::write(pwd.join(".yoi/memory/summary.md"), doc).unwrap(); std::fs::write(cwd.join(".yoi/memory/summary.md"), doc).unwrap();
} }
if include_knowledge { 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( std::fs::write(
pwd.join(".yoi/knowledge/resident-policy.md"), cwd.join(".yoi/knowledge/resident-policy.md"),
knowledge_doc("knowledge resident desc"), knowledge_doc("knowledge resident desc"),
) )
.unwrap(); .unwrap();
} }
if include_workflow { 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( std::fs::write(
pwd.join(".yoi/workflow/resident-flow.md"), cwd.join(".yoi/workflow/resident-flow.md"),
workflow_doc("workflow resident desc"), workflow_doc("workflow resident desc"),
) )
.unwrap(); .unwrap();
@ -5499,15 +5544,15 @@ mod build_summary_prompt_tests {
let mut manifest = minimal_manifest_with_skills(vec![]); let mut manifest = minimal_manifest_with_skills(vec![]);
manifest.memory = memory_config; manifest.memory = memory_config;
let scope = Scope::writable(&pwd).unwrap(); let scope = Scope::writable(&cwd).unwrap();
let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, pwd.clone(), scope) let mut pod = Pod::new(manifest, Worker::new(NoopClient), store, cwd.clone(), scope)
.await .await
.unwrap(); .unwrap();
pod.memory_layout = pod pod.memory_layout = pod
.manifest .manifest
.memory .memory
.as_ref() .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() { if let Some(layout) = pod.memory_layout.as_ref() {
pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap(); pod.workflow_registry = workflow_crate::load_workflows(layout).unwrap();
} }

View File

@ -228,8 +228,8 @@ pub struct SpawnPodTool {
/// memory context. SpawnPod `cwd` must not affect this value. /// memory context. SpawnPod `cwd` must not affect this value.
workspace_root: PathBuf, workspace_root: PathBuf,
/// Directory the spawned Pod's tools should use when the LLM did not /// Directory the spawned Pod's tools should use when the LLM did not
/// override it. Defaults to the spawner's tool pwd. /// override it. Defaults to the spawner's cwd.
spawner_pwd: PathBuf, spawner_cwd: PathBuf,
/// Optional typed runtime command injected by tests. Production resolves /// Optional typed runtime command injected by tests. Production resolves
/// the runtime command from `std::env::current_exe()` at launch time. /// the runtime command from `std::env::current_exe()` at launch time.
runtime_command: Option<PodRuntimeCommand>, runtime_command: Option<PodRuntimeCommand>,
@ -270,7 +270,7 @@ impl SpawnPodTool {
callback_socket: PathBuf, callback_socket: PathBuf,
runtime_base: PathBuf, runtime_base: PathBuf,
workspace_root: PathBuf, workspace_root: PathBuf,
spawner_pwd: PathBuf, spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>, registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>, parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest, spawner_manifest: PodManifest,
@ -284,7 +284,7 @@ impl SpawnPodTool {
callback_socket, callback_socket,
runtime_base, runtime_base,
workspace_root, workspace_root,
spawner_pwd, spawner_cwd,
runtime_command, runtime_command,
registry, registry,
parent_socket, parent_socket,
@ -318,7 +318,7 @@ impl Tool for SpawnPodTool {
let scope_allow = parse_scope(&input.scope)?; let scope_allow = parse_scope(&input.scope)?;
self.validate_delegation_scope(&scope_allow)?; 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 = let spawn_selector =
parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| { parse_spawn_profile_selector(input.profile.as_deref()).map_err(|msg| {
@ -481,9 +481,7 @@ impl SpawnPodTool {
.arg(spawn_config_json) .arg(spawn_config_json)
.arg("--workspace") .arg("--workspace")
.arg(&self.workspace_root) .arg(&self.workspace_root)
.arg("--tool-cwd") .current_dir(child_cwd)
.arg(child_cwd)
.current_dir(&self.workspace_root)
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::null()) .stdout(Stdio::null())
.stderr(Stdio::from(stderr_file)) .stderr(Stdio::from(stderr_file))
@ -881,7 +879,7 @@ pub fn spawn_pod_tool(
callback_socket: PathBuf, callback_socket: PathBuf,
runtime_base: PathBuf, runtime_base: PathBuf,
workspace_root: PathBuf, workspace_root: PathBuf,
spawner_pwd: PathBuf, spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>, registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>, parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest, spawner_manifest: PodManifest,
@ -893,7 +891,7 @@ pub fn spawn_pod_tool(
callback_socket, callback_socket,
runtime_base, runtime_base,
workspace_root, workspace_root,
spawner_pwd, spawner_cwd,
registry, registry,
parent_socket, parent_socket,
spawner_manifest, spawner_manifest,
@ -909,7 +907,7 @@ pub fn spawn_pod_tool_with_runtime_command(
callback_socket: PathBuf, callback_socket: PathBuf,
runtime_base: PathBuf, runtime_base: PathBuf,
workspace_root: PathBuf, workspace_root: PathBuf,
spawner_pwd: PathBuf, spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>, registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>, parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest, spawner_manifest: PodManifest,
@ -922,7 +920,7 @@ pub fn spawn_pod_tool_with_runtime_command(
callback_socket, callback_socket,
runtime_base, runtime_base,
workspace_root, workspace_root,
spawner_pwd, spawner_cwd,
registry, registry,
parent_socket, parent_socket,
spawner_manifest, spawner_manifest,
@ -937,7 +935,7 @@ fn spawn_pod_tool_impl(
callback_socket: PathBuf, callback_socket: PathBuf,
runtime_base: PathBuf, runtime_base: PathBuf,
workspace_root: PathBuf, workspace_root: PathBuf,
spawner_pwd: PathBuf, spawner_cwd: PathBuf,
registry: Arc<SpawnedPodRegistry>, registry: Arc<SpawnedPodRegistry>,
parent_socket: Option<PathBuf>, parent_socket: Option<PathBuf>,
spawner_manifest: PodManifest, spawner_manifest: PodManifest,
@ -969,7 +967,7 @@ fn spawn_pod_tool_impl(
callback_socket.clone(), callback_socket.clone(),
runtime_base.clone(), runtime_base.clone(),
workspace_root.clone(), workspace_root.clone(),
spawner_pwd.clone(), spawner_cwd.clone(),
registry.clone(), registry.clone(),
parent_socket.clone(), parent_socket.clone(),
spawner_manifest.clone(), spawner_manifest.clone(),

View File

@ -270,7 +270,7 @@ fn clear_env() {
} }
#[tokio::test] #[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 _env = EnvGuard::acquire();
let allow_root = TempDir::new().unwrap(); 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(); tool.execute(&input, Default::default()).await.unwrap();
assert!(matches!(received.await.unwrap(), Some(Method::Run { .. }))); assert!(matches!(received.await.unwrap(), Some(Method::Run { .. })));
let invocation = read_recorded_runtime_invocation(&output_path).await; 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!( assert!(
invocation invocation
.windows(2) .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:?}" "invocation should carry inherited workspace root: {invocation:?}"
); );
assert!( assert!(
invocation !invocation.iter().any(|arg| arg == "--tool-cwd"),
.windows(2) "cwd should be process current directory, not a runtime argument: {invocation:?}"
.any(|pair| pair[0] == "--tool-cwd" && pair[1] == child_cwd.to_str().unwrap()),
"invocation should carry tool cwd separately: {invocation:?}"
); );
clear_env(); clear_env();
} }
#[tokio::test] #[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 _env = EnvGuard::acquire();
let allow_root = TempDir::new().unwrap(); 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; let invocation = read_recorded_runtime_invocation(&output_path).await;
assert_eq!(invocation[0], allow_root.path().to_str().unwrap()); assert_eq!(invocation[0], allow_root.path().to_str().unwrap());
assert!( assert!(
invocation !invocation.iter().any(|arg| arg == "--tool-cwd"),
.windows(2) "omitted cwd should preserve spawner cwd as process cwd: {invocation:?}"
.any(|pair| pair[0] == "--tool-cwd" && pair[1] == allow_root.path().to_str().unwrap()),
"omitted cwd should preserve spawner pwd as tool cwd: {invocation:?}"
); );
clear_env(); clear_env();

View File

@ -76,7 +76,7 @@ pub(crate) struct BashParams {
pub(crate) struct BashTool { pub(crate) struct BashTool {
/// Workspace root that every invocation starts in. Snapshot of /// 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. /// don't track `cd` across calls.
cwd: PathBuf, cwd: PathBuf,
/// Directory to spill long outputs into. Caller is expected to have /// 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 /// `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 /// 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. /// w.r.t. the working directory.
pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition { pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
Arc::new(move || { Arc::new(move || {
@ -339,7 +339,7 @@ pub fn bash_tool(fs: ScopedFs, output_dir: PathBuf) -> ToolDefinition {
.description(DESCRIPTION) .description(DESCRIPTION)
.input_schema(schema_value); .input_schema(schema_value);
let tool: Arc<dyn Tool> = Arc::new(BashTool { let tool: Arc<dyn Tool> = Arc::new(BashTool {
cwd: fs.pwd().to_path_buf(), cwd: fs.cwd().to_path_buf(),
output_dir: output_dir.clone(), output_dir: output_dir.clone(),
spilled_outputs: std::sync::Mutex::new(Vec::new()), spilled_outputs: std::sync::Mutex::new(Vec::new()),
}); });

View File

@ -52,7 +52,7 @@ impl Tool for GlobTool {
let base = params let base = params
.path .path
.clone() .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 pattern = params.pattern.clone();
let scope = self.fs.scope().clone(); let scope = self.fs.scope().clone();

View File

@ -96,7 +96,7 @@ impl Tool for GrepTool {
"Grep" "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 scope = self.fs.scope().clone();
let report = tokio::task::spawn_blocking(move || run_grep(default_base, params, &scope)) let report = tokio::task::spawn_blocking(move || run_grep(default_base, params, &scope))
.await .await

View File

@ -2,7 +2,7 @@
//! //!
//! `ScopedFs` is the write/read gate layered on top of a [`manifest::Scope`] //! `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 //! 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). //! (Glob/Grep default their search base to it).
//! //!
//! `ScopedFs` is cheap to clone (`Arc` inside) and carries no per-session //! `ScopedFs` is cheap to clone (`Arc` inside) and carries no per-session
@ -20,7 +20,7 @@ use crate::error::ToolsError;
#[derive(Debug)] #[derive(Debug)]
struct ScopedFsInner { struct ScopedFsInner {
scope: SharedScope, scope: SharedScope,
pwd: PathBuf, cwd: PathBuf,
} }
/// Scope-aware filesystem handle. Clone-cheap (`Arc` inside). /// Scope-aware filesystem handle. Clone-cheap (`Arc` inside).
@ -60,20 +60,20 @@ pub struct SymlinkInfo {
} }
impl ScopedFs { 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 /// [`SharedScope`]. Use [`ScopedFs::with_shared_scope`] when you
/// need the resulting `ScopedFs` to share scope state with another /// need the resulting `ScopedFs` to share scope state with another
/// holder of the `SharedScope` (typically the Pod). /// holder of the `SharedScope` (typically the Pod).
pub fn new(scope: Scope, pwd: PathBuf) -> Self { pub fn new(scope: Scope, cwd: PathBuf) -> Self {
Self::with_shared_scope(SharedScope::new(scope), pwd) Self::with_shared_scope(SharedScope::new(scope), cwd)
} }
/// Build a [`ScopedFs`] over an existing [`SharedScope`]. The /// Build a [`ScopedFs`] over an existing [`SharedScope`]. The
/// resulting handle and any future updates the caller pushes to /// resulting handle and any future updates the caller pushes to
/// `scope` are observed by every clone of this `ScopedFs`. /// `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 { 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 /// The Pod's working directory. Glob/Grep default their search base
/// to this path when callers omit an explicit `path` parameter. /// to this path when callers omit an explicit `path` parameter.
pub fn pwd(&self) -> &Path { pub fn cwd(&self) -> &Path {
&self.inner.pwd &self.inner.cwd
} }
// ========================================================================= // =========================================================================

View File

@ -1693,9 +1693,10 @@ fn build_orchestrator_launch_context(
pod_name: &str, pod_name: &str,
) -> TicketRoleLaunchContext { ) -> TicketRoleLaunchContext {
let mut context = TicketRoleLaunchContext::new( let mut context = TicketRoleLaunchContext::new(
orchestration_workspace_root.to_path_buf(), original_workspace_root.to_path_buf(),
TicketRole::Orchestrator, TicketRole::Orchestrator,
) )
.with_cwd(orchestration_workspace_root.to_path_buf())
.with_original_workspace_root(original_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()); .with_target_workspace_root(original_workspace_root.to_path_buf());
context.pod_name = Some(pod_name.to_string()); context.pod_name = Some(pod_name.to_string());
@ -1996,6 +1997,7 @@ async fn orchestrator_lifecycle(
match prepare_orchestration_worktree_for_restore(workspace_root) { match prepare_orchestration_worktree_for_restore(workspace_root) {
Ok(worktree) => { Ok(worktree) => {
match restore_orchestrator_pod( match restore_orchestrator_pod(
workspace_root,
&worktree.layout.path, &worktree.layout.path,
&pod_name, &pod_name,
runtime_command.clone(), runtime_command.clone(),
@ -2083,6 +2085,7 @@ async fn restore_workspace_companion_pod(
profile: None, profile: None,
ticket_role: None, ticket_role: None,
workspace_root: workspace_root.to_path_buf(), workspace_root: workspace_root.to_path_buf(),
cwd: None,
resume_from: None, resume_from: None,
}; };
spawn_pod(config, |_| {}).await.map(|_| ()) spawn_pod(config, |_| {}).await.map(|_| ())
@ -2099,12 +2102,14 @@ async fn spawn_workspace_companion_pod(
profile: None, profile: None,
ticket_role: None, ticket_role: None,
workspace_root: workspace_root.to_path_buf(), workspace_root: workspace_root.to_path_buf(),
cwd: None,
resume_from: None, resume_from: None,
}; };
spawn_pod(config, |_| {}).await.map(|_| ()) spawn_pod(config, |_| {}).await.map(|_| ())
} }
async fn restore_orchestrator_pod( async fn restore_orchestrator_pod(
original_workspace_root: &Path,
workspace_root: &Path, workspace_root: &Path,
pod_name: &str, pod_name: &str,
runtime_command: PodRuntimeCommand, runtime_command: PodRuntimeCommand,
@ -2113,8 +2118,9 @@ async fn restore_orchestrator_pod(
runtime_command, runtime_command,
pod_name: pod_name.to_string(), pod_name: pod_name.to_string(),
profile: None, profile: None,
ticket_role: None, ticket_role: Some("orchestrator".to_string()),
workspace_root: workspace_root.to_path_buf(), workspace_root: original_workspace_root.to_path_buf(),
cwd: Some(workspace_root.to_path_buf()),
resume_from: None, resume_from: None,
}; };
spawn_pod(config, |_| {}).await.map(|_| ()) spawn_pod(config, |_| {}).await.map(|_| ())

View File

@ -380,6 +380,7 @@ async fn wait_for_ready(
profile: form.selected_profile_selector(), profile: form.selected_profile_selector(),
ticket_role: None, ticket_role: None,
workspace_root: form.cwd.clone(), workspace_root: form.cwd.clone(),
cwd: None,
resume_from: form.resume_from, resume_from: form.resume_from,
}; };
let ready = spawn_pod(config, |line| { let ready = spawn_pod(config, |line| {