Skip to main content

winx_code_agent/tools/
initialize.rs

1//! Implementation of the Initialize tool.
2
3use std::collections::VecDeque;
4use std::fmt::Write as FmtWrite;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::sync::{Arc, Mutex as StdMutex, OnceLock};
8use tokio::sync::Mutex;
9use tracing::{info, instrument, warn};
10
11use crate::errors::{Result, WinxError};
12use crate::runtime::{EmbeddedShellRuntime, ShellRuntime, ShellSessionTransition};
13use crate::state::bash_state::{generate_thread_id, BashState};
14use crate::types::{
15    normalize_thread_id, AllowedCommands, AllowedGlobs, BashCommandMode, BashMode,
16    CodeWriterConfig, FileEditMode, Initialize, InitializeType, Modes, WriteIfEmptyMode,
17};
18use crate::utils::mmap::read_file_to_string;
19use crate::utils::path::{ensure_directory_exists, expand_user, validate_path_in_workspace};
20
21const POLICY_WARNING_CACHE_CAPACITY: usize = 128;
22static WARNED_POLICY_CONFIGURATIONS: OnceLock<StdMutex<VecDeque<Vec<String>>>> = OnceLock::new();
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub(crate) enum InitializeTransition {
26    Created,
27    AttachedExisting,
28    ModeChanged,
29    ShellReset,
30    WorkspaceChanged,
31}
32
33impl InitializeTransition {
34    pub(crate) const fn as_str(self) -> &'static str {
35        match self {
36            Self::Created => "created",
37            Self::AttachedExisting => "attached_existing",
38            Self::ModeChanged => "mode_changed",
39            Self::ShellReset => "shell_reset",
40            Self::WorkspaceChanged => "workspace_changed",
41        }
42    }
43}
44
45#[derive(Debug)]
46pub(crate) struct InitializeOutcome {
47    pub(crate) text: String,
48    pub(crate) transition: InitializeTransition,
49    pub(crate) context_bytes: usize,
50    pub(crate) guidelines_bytes: usize,
51    pub(crate) initial_files_count: usize,
52    pub(crate) compact_response: bool,
53    pub(crate) code_writer_policy_strength: Option<&'static str>,
54    pub(crate) shell_spawners_present: bool,
55    pub(crate) temporary_artifact_dir: PathBuf,
56    pub(crate) temporary_artifact_ttl_seconds: u64,
57    pub(crate) temporary_artifact_max_bytes: u64,
58    pub(crate) temporary_artifact_max_file_bytes: u64,
59}
60
61/// Create a unique scratch workspace under the system temp dir, used when the
62/// caller initializes without a workspace path.
63fn create_playground_dir(thread_id: &str) -> Result<PathBuf> {
64    #[cfg(unix)]
65    let owner = crate::os::unix::effective_uid().to_string();
66    #[cfg(not(unix))]
67    let owner = std::process::id().to_string();
68
69    let dir = std::env::temp_dir().join(format!("winx-playground-{owner}-{thread_id}"));
70    ensure_directory_exists(&dir)?;
71    Ok(dir)
72}
73
74/// Whether `cmd` is on PATH (best-effort, used only for advisory hints).
75fn command_exists(cmd: &str) -> bool {
76    std::process::Command::new("sh")
77        .args(["-c", &format!("command -v {cmd}")])
78        .output()
79        .is_ok_and(|o| o.status.success())
80}
81
82fn code_writer_state(
83    config: &CodeWriterConfig,
84    workspace_root: &Path,
85) -> (BashCommandMode, FileEditMode, WriteIfEmptyMode) {
86    let mut config = config.clone();
87    // Forgive the common `["all"]` mistake before turning relative globs absolute.
88    config.allowed_globs.normalize();
89    config.allowed_commands.normalize();
90    config.update_relative_globs(&workspace_root.to_string_lossy());
91
92    (
93        BashCommandMode {
94            bash_mode: BashMode::NormalMode,
95            allowed_commands: config.allowed_commands,
96        },
97        FileEditMode { allowed_globs: config.allowed_globs.clone() },
98        WriteIfEmptyMode { allowed_globs: config.allowed_globs },
99    )
100}
101
102fn mode_to_state(
103    mode: Modes,
104    config: Option<&CodeWriterConfig>,
105    workspace_root: &Path,
106) -> Result<(BashCommandMode, FileEditMode, WriteIfEmptyMode)> {
107    match mode {
108        Modes::Wcgw => Ok((
109            BashCommandMode {
110                bash_mode: BashMode::NormalMode,
111                allowed_commands: AllowedCommands::All("all".to_string()),
112            },
113            FileEditMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
114            WriteIfEmptyMode { allowed_globs: AllowedGlobs::All("all".to_string()) },
115        )),
116        Modes::Architect => Ok((
117            BashCommandMode {
118                bash_mode: BashMode::RestrictedMode,
119                allowed_commands: AllowedCommands::List(
120                    crate::utils::bash_parser::architect_allowed_commands(),
121                ),
122            },
123            FileEditMode { allowed_globs: AllowedGlobs::List(vec![]) },
124            WriteIfEmptyMode { allowed_globs: AllowedGlobs::List(vec![]) },
125        )),
126        Modes::CodeWriter => {
127            let config = config.ok_or_else(|| {
128                WinxError::ArgumentParseError(
129                    "code_writer_config is required when mode_name is code_writer.".to_string(),
130                )
131            })?;
132            Ok(code_writer_state(config, workspace_root))
133        }
134    }
135}
136
137fn active_code_writer_config(state: &BashState) -> Option<CodeWriterConfig> {
138    (state.mode == Modes::CodeWriter).then(|| CodeWriterConfig {
139        allowed_globs: state.file_edit_mode.allowed_globs.clone(),
140        allowed_commands: state.bash_command_mode.allowed_commands.clone(),
141    })
142}
143
144fn active_code_writer_policy(state: &BashState) -> (Option<&'static str>, Vec<String>) {
145    if state.mode != Modes::CodeWriter {
146        return (None, Vec::new());
147    }
148
149    match &state.bash_command_mode.allowed_commands {
150        AllowedCommands::All(value) if value == "all" => (Some("unrestricted"), Vec::new()),
151        AllowedCommands::All(_) => (Some("restricted"), Vec::new()),
152        AllowedCommands::List(commands) => {
153            let bypass = crate::utils::bash_parser::detect_allowlist_bypass(commands);
154            let strength = if bypass.is_empty() { "restricted" } else { "weak" };
155            (Some(strength), bypass)
156        }
157    }
158}
159
160fn warn_code_writer_policy_once(state: &BashState, bypass: &[String]) {
161    if bypass.is_empty() {
162        return;
163    }
164    let AllowedCommands::List(commands) = &state.bash_command_mode.allowed_commands else {
165        return;
166    };
167
168    let cache = WARNED_POLICY_CONFIGURATIONS
169        .get_or_init(|| StdMutex::new(VecDeque::with_capacity(POLICY_WARNING_CACHE_CAPACITY)));
170    let mut cache = cache.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
171    if cache.iter().any(|seen| seen == commands) {
172        return;
173    }
174    if cache.len() == POLICY_WARNING_CACHE_CAPACITY {
175        cache.pop_front();
176    }
177    cache.push_back(commands.clone());
178    drop(cache);
179
180    warn!(
181        commands = ?bypass,
182        "code_writer allowlist includes shell-spawning commands; the command allowlist is \
183         effectively bypassable and does not sandbox the agent"
184    );
185}
186
187fn append_code_writer_policy_warning(response: &mut String, bypass: &[String]) {
188    if bypass.is_empty() {
189        return;
190    }
191    let _ = writeln!(
192        response,
193        "\n⚠️  SECURITY: code_writer allowlist includes shell/eval commands ({}). \
194         They execute arbitrary code from string arguments (e.g. `bash -c …`, \
195         `find -exec …`), so the command allowlist is effectively bypassable and \
196         does NOT sandbox the agent. Drop them if you intended a hard restriction.",
197        bypass.join(", ")
198    );
199}
200
201fn read_initial_files_simple(files: &[String], workspace: &std::path::Path) -> String {
202    let mut output = String::new();
203    for file_path in files {
204        let expanded = expand_user(file_path);
205        let path = if std::path::Path::new(&expanded).is_absolute() {
206            PathBuf::from(&expanded)
207        } else {
208            workspace.join(&expanded)
209        };
210
211        if let Ok(validated) = validate_path_in_workspace(&path, workspace) {
212            if validated.exists() && validated.is_file() {
213                if let Ok(content) = read_file_to_string(&validated, 10_000_000) {
214                    let _ = write!(output, "\n{file_path}\n```\n{content}\n```\n");
215                }
216            }
217        }
218    }
219    output
220}
221
222fn prepare_workspace(
223    initialize: &Initialize,
224    thread_id: &str,
225    response: &mut String,
226) -> Result<PathBuf> {
227    let workspace_path_str = expand_user(&initialize.any_workspace_path);
228    if workspace_path_str.is_empty() {
229        // wcgw parity: no path given → spin up a scratch playground instead of
230        // forcing the agent to always supply a workspace.
231        let playground = create_playground_dir(thread_id)?;
232        let _ = writeln!(
233            response,
234            "No workspace path provided; created a playground at {}",
235            playground.display()
236        );
237        return Ok(playground);
238    }
239
240    let workspace_path = PathBuf::from(&workspace_path_str);
241    let mut folder_to_start = workspace_path.clone();
242
243    if workspace_path.exists() {
244        if workspace_path.is_file() {
245            folder_to_start = workspace_path.parent().unwrap_or(&workspace_path).to_path_buf();
246            let _ =
247                writeln!(response, "Using parent directory of file: {}", folder_to_start.display());
248        } else if workspace_path.is_dir() {
249            let _ = writeln!(response, "Using workspace directory: {}", folder_to_start.display());
250        }
251    } else if workspace_path.is_absolute() {
252        ensure_directory_exists(&workspace_path).map_err(|e| {
253            WinxError::WorkspacePathError(format!("Failed to create workspace: {e}"))
254        })?;
255        let _ = writeln!(response, "Created workspace directory: {}", workspace_path.display());
256    }
257
258    // Canonicalize so downstream comparisons (workspace checks, glob prefixes) match
259    // paths that were canonicalized via fs::canonicalize — important on macOS where
260    // /var, /tmp etc. are symlinks to /private/var, /private/tmp.
261    if folder_to_start.exists() {
262        if let Ok(canonical) = folder_to_start.canonicalize() {
263            folder_to_start = canonical;
264        }
265    }
266
267    Ok(folder_to_start)
268}
269
270fn initialize_thread_id(initialize: &Initialize) -> String {
271    let thread_id = normalize_thread_id(&initialize.thread_id);
272    if thread_id.is_empty() {
273        generate_thread_id()
274    } else {
275        thread_id
276    }
277}
278
279fn validate_thread_id(initialize: &Initialize) -> Result<()> {
280    if initialize.init_type != InitializeType::FirstCall
281        && normalize_thread_id(&initialize.thread_id).is_empty()
282    {
283        return Err(WinxError::ThreadIdMismatch(
284            "Thread id should be provided if type != 'first_call', including when resetting."
285                .to_string(),
286        ));
287    }
288
289    Ok(())
290}
291
292fn load_guidelines(workspace: &Path) -> String {
293    let mut output = String::new();
294    let mut candidates = Vec::new();
295    if let Some(home) = home::home_dir() {
296        candidates.push(home.join(".winx").join("AGENTS.md"));
297        candidates.push(home.join(".winx").join("CLAUDE.md"));
298        candidates.push(home.join(".wcgw").join("AGENTS.md"));
299        candidates.push(home.join(".wcgw").join("CLAUDE.md"));
300    }
301    candidates.push(workspace.join("AGENTS.md"));
302    candidates.push(workspace.join("CLAUDE.md"));
303
304    for path in candidates {
305        if path.is_file() {
306            if let Ok(content) = fs::read_to_string(&path) {
307                let _ = writeln!(output, "\n## {}\n{}", path.display(), content);
308            }
309        }
310    }
311    output
312}
313
314pub async fn handle_tool_call(
315    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
316    initialize: Initialize,
317) -> Result<String> {
318    handle_tool_call_with_runtime(&EmbeddedShellRuntime, bash_state_arc, initialize).await
319}
320
321pub async fn handle_tool_call_with_runtime(
322    runtime: &dyn ShellRuntime,
323    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
324    initialize: Initialize,
325) -> Result<String> {
326    Ok(handle_tool_call_with_runtime_detailed(runtime, bash_state_arc, initialize).await?.text)
327}
328
329#[instrument(level = "info", skip(runtime, bash_state_arc, initialize))]
330#[allow(clippy::too_many_lines)]
331pub(crate) async fn handle_tool_call_with_runtime_detailed(
332    runtime: &dyn ShellRuntime,
333    bash_state_arc: &Arc<Mutex<Option<BashState>>>,
334    initialize: Initialize,
335) -> Result<InitializeOutcome> {
336    let mut response = String::new();
337    let mut context_bytes = 0;
338    let mut guidelines_bytes = 0;
339    let initial_files_count = initialize.initial_files_to_read.len();
340
341    info!("Initialize called for workspace: {}", initialize.any_workspace_path);
342
343    validate_thread_id(&initialize)?;
344    let thread_id = initialize_thread_id(&initialize);
345    let mut bash_state_guard = bash_state_arc.lock().await;
346    if initialize.init_type != InitializeType::FirstCall && bash_state_guard.is_none() {
347        return Err(WinxError::BashStateNotInitialized);
348    }
349    let local_attach = initialize.init_type == InitializeType::FirstCall
350        && bash_state_guard
351            .as_ref()
352            .is_some_and(|state| state.initialized && state.current_thread_id == thread_id);
353    let folder_to_start = if local_attach {
354        bash_state_guard
355            .as_ref()
356            .map_or_else(|| PathBuf::from("/tmp"), |state| state.workspace_root.clone())
357    } else {
358        prepare_workspace(&initialize, &thread_id, &mut response)?
359    };
360    let requested_mode = Modes::from(&initialize.mode_name);
361    let preserve_active_mode =
362        local_attach || initialize.init_type == InitializeType::UserAskedChangeWorkspace;
363    let (mode, bash_command_mode, file_edit_mode, write_if_empty_mode) = if preserve_active_mode {
364        let Some(state) = bash_state_guard.as_ref() else {
365            return Err(WinxError::BashStateNotInitialized);
366        };
367        (
368            state.mode,
369            state.bash_command_mode.clone(),
370            state.file_edit_mode.clone(),
371            state.write_if_empty_mode.clone(),
372        )
373    } else {
374        let (bash_command_mode, file_edit_mode, write_if_empty_mode) = mode_to_state(
375            requested_mode,
376            initialize.code_writer_config.as_ref(),
377            &folder_to_start,
378        )?;
379        (requested_mode, bash_command_mode, file_edit_mode, write_if_empty_mode)
380    };
381
382    let transition;
383    let mut compact_response = false;
384
385    match initialize.init_type {
386        InitializeType::FirstCall => {
387            let configured = if local_attach {
388                let Some(state) = bash_state_guard.as_mut() else {
389                    return Err(WinxError::BashStateNotInitialized);
390                };
391                // Refresh guardian activity without resetting the PTY. Reusing the
392                // current snapshot as a mode transition is also compatible with
393                // protocol-1.2 guardians that predate attach-or-create.
394                let mut configured =
395                    runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
396                configured.attached_existing = true;
397                configured
398            } else {
399                let mut new_bash_state = BashState::new();
400                new_bash_state.current_thread_id.clone_from(&thread_id);
401                new_bash_state.mode = mode;
402                new_bash_state.bash_command_mode = bash_command_mode;
403                new_bash_state.file_edit_mode = file_edit_mode;
404                new_bash_state.write_if_empty_mode = write_if_empty_mode;
405                new_bash_state.initialized = true;
406
407                let resumed_context = if initialize.task_id_to_resume.is_empty() {
408                    None
409                } else {
410                    crate::tools::context_save::load_saved_context(&initialize.task_id_to_resume)?
411                };
412
413                if let Some((memory_data, snapshot)) = &resumed_context {
414                    if let Some(snapshot) = snapshot {
415                        new_bash_state.apply_snapshot(snapshot);
416                        new_bash_state.current_thread_id.clone_from(&thread_id);
417                    }
418                    let _ = writeln!(
419                        response,
420                        "\n# Resumed task {}\nFollowing is the retrieved task context:\n{}",
421                        initialize.task_id_to_resume, memory_data
422                    );
423                }
424
425                // A bash snapshot already carries cwd/workspace. Without one, prefer
426                // the project root recorded in the resumed memory (so the agent lands
427                // back in the right repo), then fall back to the provided folder.
428                if resumed_context.as_ref().and_then(|(_, snapshot)| snapshot.as_ref()).is_none() {
429                    let resumed_root = resumed_context
430                        .as_ref()
431                        .and_then(|(memory, _)| {
432                            crate::tools::context_save::extract_project_root(memory)
433                        })
434                        .filter(|root| root.exists());
435                    let target = resumed_root.as_deref().unwrap_or(folder_to_start.as_path());
436                    if target.exists() {
437                        new_bash_state.update_cwd(target)?;
438                        new_bash_state.update_workspace_root(target)?;
439                    }
440                }
441                let configured = runtime
442                    .configure_session(&mut new_bash_state, ShellSessionTransition::FirstCall)
443                    .await?;
444                *bash_state_guard = Some(new_bash_state);
445                configured
446            };
447
448            transition = if configured.attached_existing {
449                InitializeTransition::AttachedExisting
450            } else {
451                InitializeTransition::Created
452            };
453            compact_response = configured.attached_existing
454                && initialize.initial_files_to_read.is_empty()
455                && initialize.task_id_to_resume.is_empty();
456
457            if compact_response {
458                response.clear();
459                let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
460                let _ = writeln!(
461                    response,
462                    "Attached to the existing durable Winx session; PTY, cwd, mode, and prior \
463                     context were preserved.\ncwd={}\nmode={}",
464                    state.cwd.display(),
465                    state.mode
466                );
467                if let Some(attach_hint) = configured.attach_hint.as_deref() {
468                    let _ = writeln!(response, "Attach terminal: {attach_hint}");
469                }
470                response.push_str(
471                    "Context and instructions are unchanged. Continue with the existing thread; \
472                     use an explicit mode/workspace change or reset only when intended.\n",
473                );
474            } else {
475                if configured.attached_existing {
476                    response.push_str(
477                        "\nAttached to the existing durable session for this principal/workspace; \
478                         the guardian-owned PTY and cwd were preserved. Use `reset_shell` or an \
479                         explicit workspace/mode change when replacement is intended.\n",
480                    );
481                }
482
483                let _ = write!(
484                    response,
485                    "\n# Environment\nSystem: {}\nMachine: {}\nInitialized in directory: {}\n",
486                    std::env::consts::OS,
487                    std::env::consts::ARCH,
488                    bash_state_guard
489                        .as_ref()
490                        .map_or(folder_to_start.as_path(), |state| state.cwd.as_path())
491                        .display()
492                );
493
494                if command_exists("rg") {
495                    let _ = writeln!(
496                        response,
497                        "\n# Available commands\nUse ripgrep `rg` instead of `grep`/`find -name` — \
498                         it's much faster and respects .gitignore."
499                    );
500                }
501
502                if let Some(attach_hint) = configured.attach_hint.as_deref() {
503                    let _ = writeln!(response, "\nAttach terminal: {attach_hint}");
504                }
505
506                // Explain the actual active policy. A daemon reattach can restore a
507                // different mode than the model redundantly supplied.
508                let active_config = bash_state_guard.as_ref().and_then(active_code_writer_config);
509                let active_mode = bash_state_guard.as_ref().map_or(mode, |state| state.mode);
510                let _ = writeln!(
511                    response,
512                    "\n{}",
513                    crate::utils::mode_prompts::mode_prompt(active_mode, active_config.as_ref())
514                );
515
516                let active_workspace = bash_state_guard
517                    .as_ref()
518                    .map_or(folder_to_start.as_path(), |state| state.workspace_root.as_path());
519
520                let guidelines = load_guidelines(active_workspace);
521                guidelines_bytes = guidelines.len();
522                if !guidelines.is_empty() {
523                    let _ = writeln!(response, "\n# Agent guidelines\n{guidelines}");
524                }
525
526                if let Ok((repo_context, _)) =
527                    crate::utils::repo::get_repo_context(active_workspace)
528                {
529                    context_bytes = repo_context.len();
530                    let _ = writeln!(response, "\n# Workspace structure\n{repo_context}");
531                }
532
533                if !initialize.initial_files_to_read.is_empty() {
534                    let content = read_initial_files_simple(
535                        &initialize.initial_files_to_read,
536                        active_workspace,
537                    );
538                    if !content.is_empty() {
539                        let _ = writeln!(response, "\n# Requested files\n{content}");
540                    }
541                }
542            }
543        }
544        InitializeType::UserAskedModeChange => {
545            transition = InitializeTransition::ModeChanged;
546            if let Some(state) = bash_state_guard.as_mut() {
547                state.mode = mode;
548                state.bash_command_mode = bash_command_mode;
549                state.file_edit_mode = file_edit_mode;
550                state.write_if_empty_mode = write_if_empty_mode;
551                runtime.configure_session(state, ShellSessionTransition::ModeChange).await?;
552                let _ = writeln!(response, "Changed mode to: {mode:?}");
553                let active_config = active_code_writer_config(state);
554                let _ = writeln!(
555                    response,
556                    "\n{}",
557                    crate::utils::mode_prompts::mode_prompt(mode, active_config.as_ref())
558                );
559            } else {
560                return Err(WinxError::BashStateNotInitialized);
561            }
562        }
563        InitializeType::ResetShell => {
564            transition = InitializeTransition::ShellReset;
565            if let Some(state) = bash_state_guard.as_mut() {
566                state.mode = mode;
567                state.bash_command_mode = bash_command_mode;
568                state.file_edit_mode = file_edit_mode;
569                state.write_if_empty_mode = write_if_empty_mode;
570                runtime.configure_session(state, ShellSessionTransition::Reset).await?;
571                response.push_str("Reset shell (new PTY created)\n");
572            } else {
573                return Err(WinxError::BashStateNotInitialized);
574            }
575        }
576        InitializeType::UserAskedChangeWorkspace => {
577            transition = InitializeTransition::WorkspaceChanged;
578            if let Some(state) = bash_state_guard.as_mut() {
579                if folder_to_start.exists() {
580                    state.update_cwd(&folder_to_start)?;
581                    state.update_workspace_root(&folder_to_start)?;
582                    runtime
583                        .configure_session(state, ShellSessionTransition::WorkspaceChange)
584                        .await?;
585                    let _ =
586                        writeln!(response, "Changed workspace to: {}", folder_to_start.display());
587                } else {
588                    let _ = writeln!(
589                        response,
590                        "Warning: Workspace path {} does not exist",
591                        folder_to_start.display()
592                    );
593                }
594            } else {
595                return Err(WinxError::BashStateNotInitialized);
596            }
597        }
598    }
599
600    let state = bash_state_guard.as_ref().ok_or(WinxError::BashStateNotInitialized)?;
601    let temporary_artifact = if compact_response {
602        crate::utils::agent_temp::session_info(&state.workspace_root, &state.current_thread_id)
603    } else {
604        crate::utils::agent_temp::prepare_session(&state.workspace_root, &state.current_thread_id)
605    };
606    let temporary_artifact_instruction = if compact_response {
607        format!(
608            "Use temporary_artifact_dir={} for session-local derived helpers; BashCommand exports \
609             the same path as WINX_TEMP_DIR.",
610            temporary_artifact.directory.display()
611        )
612    } else {
613        format!(
614            "Use temporary_artifact_dir={} for session-local derived helpers only; keep names \
615             short, preserve source-path/line provenance, and treat helpers as non-canonical. The \
616             directory is created on demand and expired after {} seconds of inactivity. Every \
617             BashCommand PTY exports the same path as WINX_TEMP_DIR; shell-generated helpers must \
618             stay beneath it.",
619            temporary_artifact.directory.display(),
620            temporary_artifact.ttl_seconds,
621        )
622    };
623    let _ = writeln!(
624        response,
625        "\nUse thread_id={thread_id} for all winx tool calls.\nUse workspace_root={} for all winx tool calls.\n{temporary_artifact_instruction}\nBefore every reuse, confirm workspace_root still matches the user's current project. Keep this exact pair together. workspace_root identifies this project session; it does not restrict target paths allowed by policy.",
626        state.workspace_root.display(),
627    );
628
629    let (code_writer_policy_strength, bypass) =
630        bash_state_guard.as_ref().map_or((None, Vec::new()), active_code_writer_policy);
631    if let Some(state) = bash_state_guard.as_ref() {
632        warn_code_writer_policy_once(state, &bypass);
633    }
634    if compact_response {
635        if let Some(strength) = code_writer_policy_strength {
636            let _ = writeln!(
637                response,
638                "code_writer_policy={strength} shell_spawners_present={}",
639                !bypass.is_empty()
640            );
641        }
642    } else {
643        append_code_writer_policy_warning(&mut response, &bypass);
644        crate::utils::orchestration::append_initialize_instructions(&mut response);
645    }
646
647    Ok(InitializeOutcome {
648        text: response,
649        transition,
650        context_bytes,
651        guidelines_bytes,
652        initial_files_count,
653        compact_response,
654        code_writer_policy_strength,
655        shell_spawners_present: !bypass.is_empty(),
656        temporary_artifact_dir: temporary_artifact.directory,
657        temporary_artifact_ttl_seconds: temporary_artifact.ttl_seconds,
658        temporary_artifact_max_bytes: temporary_artifact.max_total_bytes,
659        temporary_artifact_max_file_bytes: temporary_artifact.max_file_bytes,
660    })
661}