Skip to main content

kaish_kernel/
kernel.rs

1//! The Kernel (核) — the heart of kaish.
2//!
3//! The Kernel owns and coordinates all core components:
4//! - Interpreter state (scope, $?)
5//! - Tool registry (builtins, user tools, MCP)
6//! - VFS router (mount points)
7//! - Job manager (background jobs)
8//!
9//! # Architecture
10//!
11//! ```text
12//! ┌────────────────────────────────────────────────────────────┐
13//! │                         Kernel (核)                         │
14//! │  ┌──────────────┐  ┌──────────────┐  ┌──────────────────┐  │
15//! │  │   Scope      │  │ ToolRegistry │  │  VfsRouter       │  │
16//! │  │  (variables) │  │   (builtins, │  │  (mount points)  │  │
17//! │  │              │  │    MCP, user)│  │                  │  │
18//! │  └──────────────┘  └──────────────┘  └──────────────────┘  │
19//! │  ┌──────────────────────────────┐  ┌──────────────────┐    │
20//! │  │  JobManager (background)     │  │  ExecResult ($?) │    │
21//! │  └──────────────────────────────┘  └──────────────────┘    │
22//! └────────────────────────────────────────────────────────────┘
23//! ```
24
25use std::collections::HashMap;
26use std::path::PathBuf;
27use std::sync::Arc;
28
29use anyhow::{Context, Result};
30use tokio::sync::RwLock;
31
32use async_trait::async_trait;
33
34use crate::ast::{Arg, Command, Expr, Stmt, StringPart, ToolDef, Value, BinaryOp};
35use crate::backend::{BackendError, KernelBackend};
36use kaish_glob::glob_match;
37use crate::dispatch::{CommandDispatcher, PipelinePosition};
38use crate::interpreter::{apply_output_format, eval_expr, expand_tilde, json_to_value, value_to_string, ControlFlow, ExecResult, Scope};
39use crate::parser::parse;
40use crate::scheduler::{drain_to_stream, is_bool_type, schema_param_lookup, BoundedStream, JobManager, PipelineRunner, DEFAULT_STREAM_MAX_SIZE};
41use crate::tools::{extract_output_format, register_builtins, resolve_in_path, ExecContext, ToolArgs, ToolRegistry};
42use crate::validator::{Severity, Validator};
43use crate::vfs::{JobFs, LocalFs, MemoryFs, VfsRouter};
44
45/// VFS mount mode determines how the local filesystem is exposed.
46///
47/// Different modes trade off convenience vs. security:
48/// - `Passthrough` gives native path access (best for human REPL use)
49/// - `Sandboxed` restricts access to a subtree (safer for agents)
50/// - `NoLocal` provides complete isolation (tests, pure memory mode)
51#[derive(Debug, Clone)]
52pub enum VfsMountMode {
53    /// LocalFs at "/" — native paths work directly.
54    ///
55    /// Full filesystem access. Use for human-operated REPL sessions where
56    /// native paths like `/home/user/project` should just work.
57    ///
58    /// Mounts:
59    /// - `/` → LocalFs("/")
60    /// - `/v` → MemoryFs (blob storage)
61    /// - `/scratch` → MemoryFs (ephemeral)
62    Passthrough,
63
64    /// Transparent sandbox — paths look native but access is restricted.
65    ///
66    /// The local filesystem is mounted at its real path (e.g., `/home/user`),
67    /// so `/home/user/src/project` just works. But paths outside the sandbox
68    /// root are not accessible.
69    ///
70    /// Mounts:
71    /// - `/` → MemoryFs (catches paths outside sandbox)
72    /// - `{root}` → LocalFs(root)  (e.g., `/home/user` → LocalFs)
73    /// - `/tmp` → MemoryFs
74    /// - `/v` → MemoryFs (blob storage)
75    /// - `/scratch` → MemoryFs
76    Sandboxed {
77        /// Root path for local filesystem. Defaults to `$HOME`.
78        /// Can be restricted further, e.g., `~/src`.
79        root: Option<PathBuf>,
80    },
81
82    /// No local filesystem. Memory only.
83    ///
84    /// Complete isolation — no access to the host filesystem.
85    /// Useful for tests or pure sandboxed execution.
86    ///
87    /// Mounts:
88    /// - `/` → MemoryFs
89    /// - `/tmp` → MemoryFs
90    /// - `/v` → MemoryFs
91    /// - `/scratch` → MemoryFs
92    NoLocal,
93}
94
95impl Default for VfsMountMode {
96    fn default() -> Self {
97        VfsMountMode::Sandboxed { root: None }
98    }
99}
100
101/// Configuration for kernel initialization.
102#[derive(Debug, Clone)]
103pub struct KernelConfig {
104    /// Name of this kernel (for identification).
105    pub name: String,
106
107    /// VFS mount mode — controls how local filesystem is exposed.
108    pub vfs_mode: VfsMountMode,
109
110    /// Initial working directory (VFS path).
111    pub cwd: PathBuf,
112
113    /// Whether to skip pre-execution validation.
114    ///
115    /// When false (default), scripts are validated before execution to catch
116    /// errors early. Set to true to skip validation for performance or to
117    /// allow dynamic/external commands.
118    pub skip_validation: bool,
119
120    /// When true, standalone external commands inherit stdio for real-time output.
121    ///
122    /// Set by script runner and REPL for human-visible output.
123    /// Not set by MCP server (output must be captured for structured responses).
124    pub interactive: bool,
125}
126
127/// Get the default sandbox root ($HOME).
128fn default_sandbox_root() -> PathBuf {
129    std::env::var("HOME")
130        .map(PathBuf::from)
131        .unwrap_or_else(|_| PathBuf::from("/"))
132}
133
134impl Default for KernelConfig {
135    fn default() -> Self {
136        let home = default_sandbox_root();
137        Self {
138            name: "default".to_string(),
139            vfs_mode: VfsMountMode::Sandboxed { root: None },
140            cwd: home,
141            skip_validation: false,
142            interactive: false,
143        }
144    }
145}
146
147impl KernelConfig {
148    /// Create a transient kernel config (sandboxed, for temporary use).
149    pub fn transient() -> Self {
150        let home = default_sandbox_root();
151        Self {
152            name: "transient".to_string(),
153            vfs_mode: VfsMountMode::Sandboxed { root: None },
154            cwd: home,
155            skip_validation: false,
156            interactive: false,
157        }
158    }
159
160    /// Create a kernel config with the given name (sandboxed by default).
161    pub fn named(name: &str) -> Self {
162        let home = default_sandbox_root();
163        Self {
164            name: name.to_string(),
165            vfs_mode: VfsMountMode::Sandboxed { root: None },
166            cwd: home,
167            skip_validation: false,
168            interactive: false,
169        }
170    }
171
172    /// Create a REPL config with passthrough filesystem access.
173    ///
174    /// Native paths like `/home/user/project` work directly.
175    /// The cwd is set to the actual current working directory.
176    pub fn repl() -> Self {
177        let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
178        Self {
179            name: "repl".to_string(),
180            vfs_mode: VfsMountMode::Passthrough,
181            cwd,
182            skip_validation: false,
183            interactive: false,
184        }
185    }
186
187    /// Create an MCP server config with sandboxed filesystem access.
188    ///
189    /// Local filesystem is accessible at its real path (e.g., `/home/user`),
190    /// but sandboxed to `$HOME`. Paths outside the sandbox are not accessible.
191    pub fn mcp() -> Self {
192        let home = default_sandbox_root();
193        Self {
194            name: "mcp".to_string(),
195            vfs_mode: VfsMountMode::Sandboxed { root: None },
196            cwd: home,
197            skip_validation: false,
198            interactive: false,
199        }
200    }
201
202    /// Create an MCP server config with a custom sandbox root.
203    ///
204    /// Use this to restrict access to a subdirectory like `~/src`.
205    pub fn mcp_with_root(root: PathBuf) -> Self {
206        Self {
207            name: "mcp".to_string(),
208            vfs_mode: VfsMountMode::Sandboxed { root: Some(root.clone()) },
209            cwd: root,
210            skip_validation: false,
211            interactive: false,
212        }
213    }
214
215    /// Create a config with no local filesystem (memory only).
216    ///
217    /// Useful for tests or pure sandboxed execution.
218    pub fn isolated() -> Self {
219        Self {
220            name: "isolated".to_string(),
221            vfs_mode: VfsMountMode::NoLocal,
222            cwd: PathBuf::from("/"),
223            skip_validation: false,
224            interactive: false,
225        }
226    }
227
228    /// Set the VFS mount mode.
229    pub fn with_vfs_mode(mut self, mode: VfsMountMode) -> Self {
230        self.vfs_mode = mode;
231        self
232    }
233
234    /// Set the initial working directory.
235    pub fn with_cwd(mut self, cwd: PathBuf) -> Self {
236        self.cwd = cwd;
237        self
238    }
239
240    /// Skip pre-execution validation.
241    pub fn with_skip_validation(mut self, skip: bool) -> Self {
242        self.skip_validation = skip;
243        self
244    }
245
246    /// Enable interactive mode (external commands inherit stdio).
247    pub fn with_interactive(mut self, interactive: bool) -> Self {
248        self.interactive = interactive;
249        self
250    }
251}
252
253/// The Kernel (核) — executes kaish code.
254///
255/// This is the primary interface for running kaish commands. It owns all
256/// the runtime state: variables, tools, VFS, jobs, and persistence.
257pub struct Kernel {
258    /// Kernel name.
259    name: String,
260    /// Variable scope.
261    scope: RwLock<Scope>,
262    /// Tool registry.
263    tools: Arc<ToolRegistry>,
264    /// User-defined tools (from `tool name { body }` statements).
265    user_tools: RwLock<HashMap<String, ToolDef>>,
266    /// Virtual filesystem router.
267    vfs: Arc<VfsRouter>,
268    /// Background job manager.
269    jobs: Arc<JobManager>,
270    /// Pipeline runner.
271    runner: PipelineRunner,
272    /// Execution context (cwd, stdin, etc.).
273    exec_ctx: RwLock<ExecContext>,
274    /// Whether to skip pre-execution validation.
275    skip_validation: bool,
276    /// When true, standalone external commands inherit stdio for real-time output.
277    interactive: bool,
278}
279
280impl Kernel {
281    /// Create a new kernel with the given configuration.
282    pub fn new(config: KernelConfig) -> Result<Self> {
283        let mut vfs = Self::setup_vfs(&config);
284        let jobs = Arc::new(JobManager::new());
285
286        // Mount JobFs for job observability at /v/jobs
287        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
288
289        let vfs = Arc::new(vfs);
290
291        // Set up tools
292        let mut tools = ToolRegistry::new();
293        register_builtins(&mut tools);
294        let tools = Arc::new(tools);
295
296        // Pipeline runner
297        let runner = PipelineRunner::new(tools.clone());
298
299        let scope = Scope::new();
300        let cwd = config.cwd;
301
302        // Create execution context with VFS and tools for backend dispatch
303        let mut exec_ctx = ExecContext::with_vfs_and_tools(vfs.clone(), tools.clone());
304        exec_ctx.set_cwd(cwd);
305        exec_ctx.set_job_manager(jobs.clone());
306        exec_ctx.set_tool_schemas(tools.schemas());
307
308        Ok(Self {
309            name: config.name,
310            scope: RwLock::new(scope),
311            tools,
312            user_tools: RwLock::new(HashMap::new()),
313            vfs,
314            jobs,
315            runner,
316            exec_ctx: RwLock::new(exec_ctx),
317            skip_validation: config.skip_validation,
318            interactive: config.interactive,
319        })
320    }
321
322    /// Set up VFS based on mount mode.
323    fn setup_vfs(config: &KernelConfig) -> VfsRouter {
324        let mut vfs = VfsRouter::new();
325
326        match &config.vfs_mode {
327            VfsMountMode::Passthrough => {
328                // LocalFs at "/" — native paths work directly
329                vfs.mount("/", LocalFs::new(PathBuf::from("/")));
330                // Memory for blobs and scratch
331                vfs.mount("/v", MemoryFs::new());
332                vfs.mount("/scratch", MemoryFs::new());
333            }
334            VfsMountMode::Sandboxed { root } => {
335                // Memory at root for safety (catches paths outside sandbox)
336                vfs.mount("/", MemoryFs::new());
337                vfs.mount("/v", MemoryFs::new());
338                vfs.mount("/scratch", MemoryFs::new());
339
340                // Real /tmp for interop with other processes
341                vfs.mount("/tmp", LocalFs::new(PathBuf::from("/tmp")));
342
343                // Resolve the sandbox root (defaults to $HOME)
344                let local_root = root.clone().unwrap_or_else(|| {
345                    std::env::var("HOME")
346                        .map(PathBuf::from)
347                        .unwrap_or_else(|_| PathBuf::from("/"))
348                });
349
350                // Mount at the real path for transparent access
351                // e.g., /home/atobey → LocalFs("/home/atobey")
352                // so /home/atobey/src/kaish just works
353                let mount_point = local_root.to_string_lossy().to_string();
354                vfs.mount(&mount_point, LocalFs::new(local_root));
355            }
356            VfsMountMode::NoLocal => {
357                // Pure memory mode — no local filesystem
358                vfs.mount("/", MemoryFs::new());
359                vfs.mount("/tmp", MemoryFs::new());
360                vfs.mount("/v", MemoryFs::new());
361                vfs.mount("/scratch", MemoryFs::new());
362            }
363        }
364
365        vfs
366    }
367
368    /// Create a transient kernel (no persistence).
369    pub fn transient() -> Result<Self> {
370        Self::new(KernelConfig::transient())
371    }
372
373    /// Create a kernel with a custom backend.
374    ///
375    /// This constructor allows embedding kaish in other systems that provide
376    /// their own storage backend (e.g., CRDT-backed storage in kaijutsu).
377    /// The provided backend will be used for all file operations in builtins.
378    ///
379    /// Note: A VfsRouter is still created internally for compatibility with
380    /// the `vfs()` method, but it won't be used for execution context operations.
381    pub fn with_backend(backend: Arc<dyn KernelBackend>, config: KernelConfig) -> Result<Self> {
382        // Create VFS for compatibility (but exec_ctx will use the provided backend)
383        let mut vfs = Self::setup_vfs(&config);
384        let jobs = Arc::new(JobManager::new());
385
386        // Mount JobFs for job observability at /v/jobs
387        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
388
389        let vfs = Arc::new(vfs);
390
391        // Set up tools
392        let mut tools = ToolRegistry::new();
393        register_builtins(&mut tools);
394        let tools = Arc::new(tools);
395
396        // Pipeline runner
397        let runner = PipelineRunner::new(tools.clone());
398
399        let scope = Scope::new();
400        let cwd = config.cwd;
401
402        // Create execution context with custom backend
403        let mut exec_ctx = ExecContext::with_backend(backend);
404        exec_ctx.set_cwd(cwd);
405        exec_ctx.set_job_manager(jobs.clone());
406        exec_ctx.set_tool_schemas(tools.schemas());
407        exec_ctx.set_tools(tools.clone());
408
409        Ok(Self {
410            name: config.name,
411            scope: RwLock::new(scope),
412            tools,
413            user_tools: RwLock::new(HashMap::new()),
414            vfs,
415            jobs,
416            runner,
417            exec_ctx: RwLock::new(exec_ctx),
418            skip_validation: config.skip_validation,
419            interactive: config.interactive,
420        })
421    }
422
423    /// Create a kernel with a custom backend and automatic `/v/*` path support.
424    ///
425    /// This is the recommended constructor for embedders who want their custom backend
426    /// to also support kaish's virtual filesystems (like `/v/jobs` for job observability).
427    ///
428    /// Paths are routed as follows:
429    /// - `/v/*` → Internal VFS (JobFs at `/v/jobs`, MemoryFs at `/v/blobs`, etc.)
430    /// - Everything else → Your custom backend
431    ///
432    /// # Example
433    ///
434    /// ```ignore
435    /// let my_backend: Arc<dyn KernelBackend> = Arc::new(MyBackend::new());
436    /// let kernel = Kernel::with_backend_and_virtual_paths(my_backend, config)?;
437    ///
438    /// // Now agents can use /v/jobs to monitor background commands
439    /// kernel.execute("cargo build &").await?;
440    /// kernel.execute("cat /v/jobs/1/stdout").await?;
441    /// ```
442    pub fn with_backend_and_virtual_paths(
443        backend: Arc<dyn KernelBackend>,
444        config: KernelConfig,
445    ) -> Result<Self> {
446        use crate::backend::VirtualOverlayBackend;
447
448        // Create VFS with virtual filesystems
449        let mut vfs = VfsRouter::new();
450        let jobs = Arc::new(JobManager::new());
451
452        // Mount JobFs for job observability
453        vfs.mount("/v/jobs", JobFs::new(jobs.clone()));
454        // Mount MemoryFs for blob storage
455        vfs.mount("/v/blobs", MemoryFs::new());
456        // Mount MemoryFs for scratch space
457        vfs.mount("/v/scratch", MemoryFs::new());
458
459        let vfs = Arc::new(vfs);
460
461        // Wrap the backend with virtual overlay
462        let overlay: Arc<dyn KernelBackend> = Arc::new(VirtualOverlayBackend::new(backend, vfs.clone()));
463
464        // Set up tools
465        let mut tools = ToolRegistry::new();
466        register_builtins(&mut tools);
467        let tools = Arc::new(tools);
468
469        // Pipeline runner
470        let runner = PipelineRunner::new(tools.clone());
471
472        let scope = Scope::new();
473        let cwd = config.cwd;
474
475        // Create execution context with the overlay backend
476        let mut exec_ctx = ExecContext::with_backend(overlay);
477        exec_ctx.set_cwd(cwd);
478        exec_ctx.set_job_manager(jobs.clone());
479        exec_ctx.set_tool_schemas(tools.schemas());
480        exec_ctx.set_tools(tools.clone());
481
482        Ok(Self {
483            name: config.name,
484            scope: RwLock::new(scope),
485            tools,
486            user_tools: RwLock::new(HashMap::new()),
487            vfs,
488            jobs,
489            runner,
490            exec_ctx: RwLock::new(exec_ctx),
491            skip_validation: config.skip_validation,
492            interactive: config.interactive,
493        })
494    }
495
496    /// Get the kernel name.
497    pub fn name(&self) -> &str {
498        &self.name
499    }
500
501    /// Execute kaish source code.
502    ///
503    /// Returns the result of the last statement executed.
504    pub async fn execute(&self, input: &str) -> Result<ExecResult> {
505        self.execute_streaming(input, &mut |_| {}).await
506    }
507
508    /// Execute kaish source code with a per-statement callback.
509    ///
510    /// Each statement's result is passed to `on_output` as it completes,
511    /// allowing callers to flush output incrementally (e.g., print builtin
512    /// output immediately rather than buffering until the script finishes).
513    ///
514    /// External commands in interactive mode already stream to the terminal
515    /// via `Stdio::inherit()`, so the callback mainly handles builtins.
516    pub async fn execute_streaming(
517        &self,
518        input: &str,
519        on_output: &mut dyn FnMut(&ExecResult),
520    ) -> Result<ExecResult> {
521        let program = parse(input).map_err(|errors| {
522            let msg = errors
523                .iter()
524                .map(|e| e.to_string())
525                .collect::<Vec<_>>()
526                .join("; ");
527            anyhow::anyhow!("parse error: {}", msg)
528        })?;
529
530        // Pre-execution validation
531        if !self.skip_validation {
532            let user_tools = self.user_tools.read().await;
533            let validator = Validator::new(&self.tools, &user_tools);
534            let issues = validator.validate(&program);
535
536            // Collect errors (warnings are logged but don't prevent execution)
537            let errors: Vec<_> = issues
538                .iter()
539                .filter(|i| i.severity == Severity::Error)
540                .collect();
541
542            if !errors.is_empty() {
543                let error_msg = errors
544                    .iter()
545                    .map(|e| e.format(input))
546                    .collect::<Vec<_>>()
547                    .join("\n");
548                return Err(anyhow::anyhow!("validation failed:\n{}", error_msg));
549            }
550
551            // Log warnings via tracing (trace level to avoid noise)
552            for warning in issues.iter().filter(|i| i.severity == Severity::Warning) {
553                tracing::trace!("validation: {}", warning.format(input));
554            }
555        }
556
557        let mut result = ExecResult::success("");
558
559        for stmt in program.statements {
560            if matches!(stmt, Stmt::Empty) {
561                continue;
562            }
563            let flow = self.execute_stmt_flow(&stmt).await?;
564            match flow {
565                ControlFlow::Normal(r) => {
566                    on_output(&r);
567                    // Carry the last statement's structured output for MCP TOON encoding.
568                    // Must be done here (not in accumulate_result) because accumulate_result
569                    // is also used in loops where per-iteration output would be wrong.
570                    let last_output = r.output.clone();
571                    accumulate_result(&mut result, &r);
572                    result.output = last_output;
573                }
574                ControlFlow::Exit { code } => {
575                    let exit_result = ExecResult::success(code.to_string());
576                    return Ok(exit_result);
577                }
578                ControlFlow::Return { value } => {
579                    on_output(&value);
580                    result = value;
581                }
582                ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
583                    on_output(&r);
584                    result = r;
585                }
586            }
587        }
588
589        Ok(result)
590    }
591
592    /// Execute a single statement, returning control flow information.
593    fn execute_stmt_flow<'a>(
594        &'a self,
595        stmt: &'a Stmt,
596    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<ControlFlow>> + Send + 'a>> {
597        Box::pin(async move {
598        match stmt {
599            Stmt::Assignment(assign) => {
600                // Use async evaluator to support command substitution
601                let value = self.eval_expr_async(&assign.value).await
602                    .context("failed to evaluate assignment")?;
603                let mut scope = self.scope.write().await;
604                if assign.local {
605                    // local: set in innermost (current function) frame
606                    scope.set(&assign.name, value.clone());
607                } else {
608                    // non-local: update existing or create in root frame
609                    scope.set_global(&assign.name, value.clone());
610                }
611                drop(scope);
612
613                // Assignments don't produce output (like sh)
614                Ok(ControlFlow::ok(ExecResult::success("")))
615            }
616            Stmt::Command(cmd) => {
617                // Route single commands through execute_pipeline for a unified path.
618                // This ensures all commands go through the dispatcher chain.
619                let pipeline = crate::ast::Pipeline {
620                    commands: vec![cmd.clone()],
621                    background: false,
622                };
623                let result = self.execute_pipeline(&pipeline).await?;
624                self.update_last_result(&result).await;
625
626                // Check for error exit mode (set -e)
627                if !result.ok() {
628                    let scope = self.scope.read().await;
629                    if scope.error_exit_enabled() {
630                        return Ok(ControlFlow::exit_code(result.code));
631                    }
632                }
633
634                Ok(ControlFlow::ok(result))
635            }
636            Stmt::Pipeline(pipeline) => {
637                let result = self.execute_pipeline(pipeline).await?;
638                self.update_last_result(&result).await;
639
640                // Check for error exit mode (set -e)
641                if !result.ok() {
642                    let scope = self.scope.read().await;
643                    if scope.error_exit_enabled() {
644                        return Ok(ControlFlow::exit_code(result.code));
645                    }
646                }
647
648                Ok(ControlFlow::ok(result))
649            }
650            Stmt::If(if_stmt) => {
651                // Use async evaluator to support command substitution in conditions
652                let cond_value = self.eval_expr_async(&if_stmt.condition).await?;
653
654                let branch = if is_truthy(&cond_value) {
655                    &if_stmt.then_branch
656                } else {
657                    if_stmt.else_branch.as_deref().unwrap_or(&[])
658                };
659
660                let mut flow = ControlFlow::ok(ExecResult::success(""));
661                for stmt in branch {
662                    flow = self.execute_stmt_flow(stmt).await?;
663                    if !flow.is_normal() {
664                        return Ok(flow);
665                    }
666                }
667                Ok(flow)
668            }
669            Stmt::For(for_loop) => {
670                // Evaluate all items and collect values for iteration
671                // Use async evaluator to support command substitution like $(seq 1 5)
672                let mut items: Vec<Value> = Vec::new();
673                for item_expr in &for_loop.items {
674                    let item = self.eval_expr_async(item_expr).await?;
675                    // NO implicit word splitting - arrays iterate, strings stay whole
676                    match &item {
677                        // JSON arrays iterate over elements
678                        Value::Json(serde_json::Value::Array(arr)) => {
679                            for elem in arr {
680                                items.push(json_to_value(elem.clone()));
681                            }
682                        }
683                        // Strings are ONE value - no splitting!
684                        // Use $(split "$VAR") for explicit splitting
685                        Value::String(_) => {
686                            items.push(item);
687                        }
688                        // Other values as-is
689                        _ => items.push(item),
690                    }
691                }
692
693                let mut result = ExecResult::success("");
694                {
695                    let mut scope = self.scope.write().await;
696                    scope.push_frame();
697                }
698
699                'outer: for item in items {
700                    {
701                        let mut scope = self.scope.write().await;
702                        scope.set(&for_loop.variable, item);
703                    }
704                    for stmt in &for_loop.body {
705                        let mut flow = self.execute_stmt_flow(stmt).await?;
706                        match &mut flow {
707                            ControlFlow::Normal(r) => accumulate_result(&mut result, r),
708                            ControlFlow::Break { .. } => {
709                                if flow.decrement_level() {
710                                    // Break handled at this level
711                                    break 'outer;
712                                }
713                                // Propagate to outer loop
714                                let mut scope = self.scope.write().await;
715                                scope.pop_frame();
716                                return Ok(flow);
717                            }
718                            ControlFlow::Continue { .. } => {
719                                if flow.decrement_level() {
720                                    // Continue handled at this level
721                                    continue 'outer;
722                                }
723                                // Propagate to outer loop
724                                let mut scope = self.scope.write().await;
725                                scope.pop_frame();
726                                return Ok(flow);
727                            }
728                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
729                                let mut scope = self.scope.write().await;
730                                scope.pop_frame();
731                                return Ok(flow);
732                            }
733                        }
734                    }
735                }
736
737                {
738                    let mut scope = self.scope.write().await;
739                    scope.pop_frame();
740                }
741                Ok(ControlFlow::ok(result))
742            }
743            Stmt::While(while_loop) => {
744                let mut result = ExecResult::success("");
745
746                'outer: loop {
747                    // Evaluate condition - use async to support command substitution
748                    let cond_value = self.eval_expr_async(&while_loop.condition).await?;
749
750                    if !is_truthy(&cond_value) {
751                        break;
752                    }
753
754                    // Execute body
755                    for stmt in &while_loop.body {
756                        let mut flow = self.execute_stmt_flow(stmt).await?;
757                        match &mut flow {
758                            ControlFlow::Normal(r) => accumulate_result(&mut result, r),
759                            ControlFlow::Break { .. } => {
760                                if flow.decrement_level() {
761                                    // Break handled at this level
762                                    break 'outer;
763                                }
764                                // Propagate to outer loop
765                                return Ok(flow);
766                            }
767                            ControlFlow::Continue { .. } => {
768                                if flow.decrement_level() {
769                                    // Continue handled at this level
770                                    continue 'outer;
771                                }
772                                // Propagate to outer loop
773                                return Ok(flow);
774                            }
775                            ControlFlow::Return { .. } | ControlFlow::Exit { .. } => {
776                                return Ok(flow);
777                            }
778                        }
779                    }
780                }
781
782                Ok(ControlFlow::ok(result))
783            }
784            Stmt::Case(case_stmt) => {
785                // Evaluate the expression to match against
786                let match_value = {
787                    let mut scope = self.scope.write().await;
788                    let value = eval_expr(&case_stmt.expr, &mut scope)?;
789                    value_to_string(&value)
790                };
791
792                // Try each branch until we find a match
793                for branch in &case_stmt.branches {
794                    let matched = branch.patterns.iter().any(|pattern| {
795                        glob_match(pattern, &match_value)
796                    });
797
798                    if matched {
799                        // Execute the branch body
800                        let mut result = ControlFlow::ok(ExecResult::success(""));
801                        for stmt in &branch.body {
802                            result = self.execute_stmt_flow(stmt).await?;
803                            if !result.is_normal() {
804                                return Ok(result);
805                            }
806                        }
807                        return Ok(result);
808                    }
809                }
810
811                // No match - return success with empty output (like sh)
812                Ok(ControlFlow::ok(ExecResult::success("")))
813            }
814            Stmt::Break(levels) => {
815                Ok(ControlFlow::break_n(levels.unwrap_or(1)))
816            }
817            Stmt::Continue(levels) => {
818                Ok(ControlFlow::continue_n(levels.unwrap_or(1)))
819            }
820            Stmt::Return(expr) => {
821                // return [N] - N becomes the exit code, NOT stdout
822                // Shell semantics: return sets exit code, doesn't produce output
823                let result = if let Some(e) = expr {
824                    let mut scope = self.scope.write().await;
825                    let val = eval_expr(e, &mut scope)?;
826                    // Convert value to exit code
827                    let code = match val {
828                        Value::Int(n) => n,
829                        Value::Bool(b) => if b { 0 } else { 1 },
830                        _ => 0,
831                    };
832                    ExecResult {
833                        code,
834                        out: String::new(),
835                        err: String::new(),
836                        data: None,
837                        output: None,
838                    }
839                } else {
840                    ExecResult::success("")
841                };
842                Ok(ControlFlow::return_value(result))
843            }
844            Stmt::Exit(expr) => {
845                let code = if let Some(e) = expr {
846                    let mut scope = self.scope.write().await;
847                    let val = eval_expr(e, &mut scope)?;
848                    match val {
849                        Value::Int(n) => n,
850                        _ => 0,
851                    }
852                } else {
853                    0
854                };
855                Ok(ControlFlow::exit_code(code))
856            }
857            Stmt::ToolDef(tool_def) => {
858                let mut user_tools = self.user_tools.write().await;
859                user_tools.insert(tool_def.name.clone(), tool_def.clone());
860                Ok(ControlFlow::ok(ExecResult::success("")))
861            }
862            Stmt::AndChain { left, right } => {
863                // cmd1 && cmd2 - run cmd2 only if cmd1 succeeds (exit code 0)
864                let left_flow = self.execute_stmt_flow(left).await?;
865                match left_flow {
866                    ControlFlow::Normal(left_result) => {
867                        self.update_last_result(&left_result).await;
868                        if left_result.ok() {
869                            let right_flow = self.execute_stmt_flow(right).await?;
870                            match right_flow {
871                                ControlFlow::Normal(right_result) => {
872                                    self.update_last_result(&right_result).await;
873                                    // Combine left and right output
874                                    let mut combined = left_result;
875                                    accumulate_result(&mut combined, &right_result);
876                                    Ok(ControlFlow::ok(combined))
877                                }
878                                other => Ok(other), // Propagate non-normal flow
879                            }
880                        } else {
881                            Ok(ControlFlow::ok(left_result))
882                        }
883                    }
884                    _ => Ok(left_flow), // Propagate non-normal flow
885                }
886            }
887            Stmt::OrChain { left, right } => {
888                // cmd1 || cmd2 - run cmd2 only if cmd1 fails (non-zero exit code)
889                let left_flow = self.execute_stmt_flow(left).await?;
890                match left_flow {
891                    ControlFlow::Normal(left_result) => {
892                        self.update_last_result(&left_result).await;
893                        if !left_result.ok() {
894                            let right_flow = self.execute_stmt_flow(right).await?;
895                            match right_flow {
896                                ControlFlow::Normal(right_result) => {
897                                    self.update_last_result(&right_result).await;
898                                    // Combine left and right output
899                                    let mut combined = left_result;
900                                    accumulate_result(&mut combined, &right_result);
901                                    Ok(ControlFlow::ok(combined))
902                                }
903                                other => Ok(other), // Propagate non-normal flow
904                            }
905                        } else {
906                            Ok(ControlFlow::ok(left_result))
907                        }
908                    }
909                    _ => Ok(left_flow), // Propagate non-normal flow
910                }
911            }
912            Stmt::Test(test_expr) => {
913                // Evaluate the test expression by wrapping in Expr::Test
914                let expr = crate::ast::Expr::Test(Box::new(test_expr.clone()));
915                let mut scope = self.scope.write().await;
916                let value = eval_expr(&expr, &mut scope)?;
917                drop(scope);
918                let is_true = match value {
919                    crate::ast::Value::Bool(b) => b,
920                    _ => false,
921                };
922                if is_true {
923                    Ok(ControlFlow::ok(ExecResult::success("")))
924                } else {
925                    Ok(ControlFlow::ok(ExecResult::failure(1, "")))
926                }
927            }
928            Stmt::Empty => Ok(ControlFlow::ok(ExecResult::success(""))),
929        }
930        })
931    }
932
933    /// Execute a pipeline.
934    async fn execute_pipeline(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
935        if pipeline.commands.is_empty() {
936            return Ok(ExecResult::success(""));
937        }
938
939        // Handle background execution (`&` operator)
940        if pipeline.background {
941            return self.execute_background(pipeline).await;
942        }
943
944        // All commands go through the runner with the Kernel as dispatcher.
945        // This is the single execution path — no fast path for single commands.
946        //
947        // IMPORTANT: We snapshot exec_ctx into a local context and release the
948        // lock before running. This prevents deadlocks when dispatch_command
949        // is called from within the pipeline and recursively triggers another
950        // pipeline (e.g., via user-defined tools).
951        let mut ctx = {
952            let ec = self.exec_ctx.read().await;
953            let scope = self.scope.read().await;
954            ExecContext {
955                backend: ec.backend.clone(),
956                scope: scope.clone(),
957                cwd: ec.cwd.clone(),
958                prev_cwd: ec.prev_cwd.clone(),
959                stdin: None,
960                stdin_data: None,
961                tool_schemas: ec.tool_schemas.clone(),
962                tools: ec.tools.clone(),
963                job_manager: ec.job_manager.clone(),
964                pipeline_position: PipelinePosition::Only,
965            }
966        }; // locks released
967
968        let result = self.runner.run(&pipeline.commands, &mut ctx, self).await;
969
970        // Sync changes back from context
971        {
972            let mut ec = self.exec_ctx.write().await;
973            ec.cwd = ctx.cwd.clone();
974            ec.prev_cwd = ctx.prev_cwd.clone();
975        }
976        {
977            let mut scope = self.scope.write().await;
978            *scope = ctx.scope.clone();
979        }
980
981        Ok(result)
982    }
983
984    /// Execute a pipeline in the background.
985    ///
986    /// The command is spawned as a tokio task, registered with the JobManager,
987    /// and its output is captured via BoundedStreams. The job is observable via
988    /// `/v/jobs/{id}/stdout`, `/v/jobs/{id}/stderr`, and `/v/jobs/{id}/status`.
989    ///
990    /// Returns immediately with a job ID like "[1]".
991    async fn execute_background(&self, pipeline: &crate::ast::Pipeline) -> Result<ExecResult> {
992        use tokio::sync::oneshot;
993
994        // Format the command for display in /v/jobs/{id}/command
995        let command_str = self.format_pipeline(pipeline);
996
997        // Create bounded streams for output capture
998        let stdout = Arc::new(BoundedStream::default_size());
999        let stderr = Arc::new(BoundedStream::default_size());
1000
1001        // Create channel for result notification
1002        let (tx, rx) = oneshot::channel();
1003
1004        // Register with JobManager to get job ID and create VFS entries
1005        let job_id = self.jobs.register_with_streams(
1006            command_str.clone(),
1007            rx,
1008            stdout.clone(),
1009            stderr.clone(),
1010        ).await;
1011
1012        // Clone state needed for the spawned task
1013        let runner = self.runner.clone();
1014        let commands = pipeline.commands.clone();
1015        let backend = {
1016            let ctx = self.exec_ctx.read().await;
1017            ctx.backend.clone()
1018        };
1019        let scope = {
1020            let scope = self.scope.read().await;
1021            scope.clone()
1022        };
1023        let cwd = {
1024            let ctx = self.exec_ctx.read().await;
1025            ctx.cwd.clone()
1026        };
1027        let tools = self.tools.clone();
1028        let tool_schemas = self.tools.schemas();
1029
1030        // Spawn the background task
1031        tokio::spawn(async move {
1032            // Create execution context for the background job
1033            // It inherits env vars and cwd from the parent context
1034            let mut bg_ctx = ExecContext::with_backend(backend);
1035            bg_ctx.scope = scope;
1036            bg_ctx.cwd = cwd;
1037            bg_ctx.set_tools(tools.clone());
1038            bg_ctx.set_tool_schemas(tool_schemas);
1039
1040            // Use BackendDispatcher for background jobs (builtins only).
1041            // Full Kernel dispatch requires Arc<Kernel> — planned for a future phase.
1042            let dispatcher = crate::dispatch::BackendDispatcher::new(tools);
1043
1044            // Execute the pipeline
1045            let result = runner.run(&commands, &mut bg_ctx, &dispatcher).await;
1046
1047            // Write output to streams
1048            if !result.out.is_empty() {
1049                stdout.write(result.out.as_bytes()).await;
1050            }
1051            if !result.err.is_empty() {
1052                stderr.write(result.err.as_bytes()).await;
1053            }
1054
1055            // Close streams
1056            stdout.close().await;
1057            stderr.close().await;
1058
1059            // Send result to JobManager (ignore error if receiver dropped)
1060            let _ = tx.send(result);
1061        });
1062
1063        Ok(ExecResult::success(format!("[{}]", job_id)))
1064    }
1065
1066    /// Format a pipeline as a command string for display.
1067    fn format_pipeline(&self, pipeline: &crate::ast::Pipeline) -> String {
1068        pipeline.commands
1069            .iter()
1070            .map(|cmd| {
1071                let mut parts = vec![cmd.name.clone()];
1072                for arg in &cmd.args {
1073                    match arg {
1074                        Arg::Positional(expr) => {
1075                            parts.push(self.format_expr(expr));
1076                        }
1077                        Arg::Named { key, value } => {
1078                            parts.push(format!("{}={}", key, self.format_expr(value)));
1079                        }
1080                        Arg::ShortFlag(name) => {
1081                            parts.push(format!("-{}", name));
1082                        }
1083                        Arg::LongFlag(name) => {
1084                            parts.push(format!("--{}", name));
1085                        }
1086                        Arg::DoubleDash => {
1087                            parts.push("--".to_string());
1088                        }
1089                    }
1090                }
1091                parts.join(" ")
1092            })
1093            .collect::<Vec<_>>()
1094            .join(" | ")
1095    }
1096
1097    /// Format an expression as a string for display.
1098    fn format_expr(&self, expr: &Expr) -> String {
1099        match expr {
1100            Expr::Literal(Value::String(s)) => {
1101                if s.contains(' ') || s.contains('"') {
1102                    format!("'{}'", s.replace('\'', "\\'"))
1103                } else {
1104                    s.clone()
1105                }
1106            }
1107            Expr::Literal(Value::Int(i)) => i.to_string(),
1108            Expr::Literal(Value::Float(f)) => f.to_string(),
1109            Expr::Literal(Value::Bool(b)) => b.to_string(),
1110            Expr::Literal(Value::Null) => "null".to_string(),
1111            Expr::VarRef(path) => {
1112                let name = path.segments.iter()
1113                    .map(|seg| match seg {
1114                        crate::ast::VarSegment::Field(f) => f.clone(),
1115                    })
1116                    .collect::<Vec<_>>()
1117                    .join(".");
1118                format!("${{{}}}", name)
1119            }
1120            Expr::Interpolated(_) => "\"...\"".to_string(),
1121            _ => "...".to_string(),
1122        }
1123    }
1124
1125    /// Execute a single command.
1126    async fn execute_command(&self, name: &str, args: &[Arg]) -> Result<ExecResult> {
1127        // Special built-ins
1128        match name {
1129            "true" => return Ok(ExecResult::success("")),
1130            "false" => return Ok(ExecResult::failure(1, "")),
1131            "source" | "." => return self.execute_source(args).await,
1132            _ => {}
1133        }
1134
1135        // Check user-defined tools first
1136        {
1137            let user_tools = self.user_tools.read().await;
1138            if let Some(tool_def) = user_tools.get(name) {
1139                let tool_def = tool_def.clone();
1140                drop(user_tools);
1141                return self.execute_user_tool(tool_def, args).await;
1142            }
1143        }
1144
1145        // Look up builtin tool
1146        let tool = match self.tools.get(name) {
1147            Some(t) => t,
1148            None => {
1149                // Try executing as .kai script from PATH
1150                if let Some(result) = self.try_execute_script(name, args).await? {
1151                    return Ok(result);
1152                }
1153                // Try executing as external command from PATH
1154                if let Some(result) = self.try_execute_external(name, args).await? {
1155                    return Ok(result);
1156                }
1157
1158                // Try backend-registered tools (embedder engines, MCP tools, etc.)
1159                let tool_args = self.build_args_async(args, None).await?;
1160                let mut ctx = self.exec_ctx.write().await;
1161                {
1162                    let scope = self.scope.read().await;
1163                    ctx.scope = scope.clone();
1164                }
1165                let backend = ctx.backend.clone();
1166                match backend.call_tool(name, tool_args, &mut ctx).await {
1167                    Ok(tool_result) => {
1168                        let mut scope = self.scope.write().await;
1169                        *scope = ctx.scope.clone();
1170                        let mut exec = ExecResult::from_output(
1171                            tool_result.code as i64, tool_result.stdout, tool_result.stderr,
1172                        );
1173                        exec.output = tool_result.output;
1174                        return Ok(exec);
1175                    }
1176                    Err(BackendError::ToolNotFound(_)) => {
1177                        // Fall through to "command not found"
1178                    }
1179                    Err(e) => {
1180                        return Ok(ExecResult::failure(1, e.to_string()));
1181                    }
1182                }
1183
1184                return Ok(ExecResult::failure(127, format!("command not found: {}", name)));
1185            }
1186        };
1187
1188        // Build arguments (async to support command substitution, schema-aware for flag values)
1189        let schema = tool.schema();
1190        let mut tool_args = self.build_args_async(args, Some(&schema)).await?;
1191        let output_format = extract_output_format(&mut tool_args, Some(&schema));
1192
1193        // Execute
1194        let mut ctx = self.exec_ctx.write().await;
1195        {
1196            let scope = self.scope.read().await;
1197            ctx.scope = scope.clone();
1198        }
1199
1200        let result = tool.execute(tool_args, &mut ctx).await;
1201
1202        // Sync scope changes back (e.g., from cd)
1203        {
1204            let mut scope = self.scope.write().await;
1205            *scope = ctx.scope.clone();
1206        }
1207
1208        let result = match output_format {
1209            Some(format) => apply_output_format(result, format),
1210            None => result,
1211        };
1212
1213        Ok(result)
1214    }
1215
1216    /// Build tool arguments from AST args.
1217    ///
1218    /// Uses async evaluation to support command substitution in arguments.
1219    async fn build_args_async(&self, args: &[Arg], schema: Option<&crate::tools::ToolSchema>) -> Result<ToolArgs> {
1220        let mut tool_args = ToolArgs::new();
1221        let param_lookup = schema.map(schema_param_lookup).unwrap_or_default();
1222
1223        // Track which positional indices have been consumed as flag values
1224        let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1225        let mut past_double_dash = false;
1226
1227        // Find positional arg indices for flag value consumption
1228        let positional_indices: Vec<usize> = args.iter().enumerate()
1229            .filter_map(|(i, a)| matches!(a, Arg::Positional(_)).then_some(i))
1230            .collect();
1231
1232        let mut i = 0;
1233        while i < args.len() {
1234            match &args[i] {
1235                Arg::DoubleDash => {
1236                    past_double_dash = true;
1237                }
1238                Arg::Positional(expr) => {
1239                    if !consumed.contains(&i) {
1240                        let value = self.eval_expr_async(expr).await?;
1241                        let value = apply_tilde_expansion(value);
1242                        tool_args.positional.push(value);
1243                    }
1244                }
1245                Arg::Named { key, value } => {
1246                    let val = self.eval_expr_async(value).await?;
1247                    let val = apply_tilde_expansion(val);
1248                    tool_args.named.insert(key.clone(), val);
1249                }
1250                Arg::ShortFlag(name) => {
1251                    if past_double_dash {
1252                        tool_args.positional.push(Value::String(format!("-{name}")));
1253                    } else if name.len() == 1 {
1254                        let flag_name = name.as_str();
1255                        let lookup = param_lookup.get(flag_name);
1256                        let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1257
1258                        if is_bool {
1259                            tool_args.flags.insert(flag_name.to_string());
1260                        } else {
1261                            // Non-bool: consume next positional as value
1262                            let canonical = lookup.map(|(name, _)| *name).unwrap_or(flag_name);
1263                            let next_pos = positional_indices.iter()
1264                                .find(|idx| **idx > i && !consumed.contains(idx));
1265
1266                            if let Some(&pos_idx) = next_pos {
1267                                if let Arg::Positional(expr) = &args[pos_idx] {
1268                                    let value = self.eval_expr_async(expr).await?;
1269                                    let value = apply_tilde_expansion(value);
1270                                    tool_args.named.insert(canonical.to_string(), value);
1271                                    consumed.insert(pos_idx);
1272                                }
1273                            } else {
1274                                tool_args.flags.insert(flag_name.to_string());
1275                            }
1276                        }
1277                    } else {
1278                        // Multi-char combined flags like -la: always boolean
1279                        for c in name.chars() {
1280                            tool_args.flags.insert(c.to_string());
1281                        }
1282                    }
1283                }
1284                Arg::LongFlag(name) => {
1285                    if past_double_dash {
1286                        tool_args.positional.push(Value::String(format!("--{name}")));
1287                    } else {
1288                        let lookup = param_lookup.get(name.as_str());
1289                        let is_bool = lookup.map(|(_, typ)| is_bool_type(typ)).unwrap_or(true);
1290
1291                        if is_bool {
1292                            tool_args.flags.insert(name.clone());
1293                        } else {
1294                            let canonical = lookup.map(|(name, _)| *name).unwrap_or(name.as_str());
1295                            let next_pos = positional_indices.iter()
1296                                .find(|idx| **idx > i && !consumed.contains(idx));
1297
1298                            if let Some(&pos_idx) = next_pos {
1299                                if let Arg::Positional(expr) = &args[pos_idx] {
1300                                    let value = self.eval_expr_async(expr).await?;
1301                                    let value = apply_tilde_expansion(value);
1302                                    tool_args.named.insert(canonical.to_string(), value);
1303                                    consumed.insert(pos_idx);
1304                                }
1305                            } else {
1306                                tool_args.flags.insert(name.clone());
1307                            }
1308                        }
1309                    }
1310                }
1311            }
1312            i += 1;
1313        }
1314
1315        Ok(tool_args)
1316    }
1317
1318    /// Build arguments as flat string list for external commands.
1319    ///
1320    /// Unlike `build_args_async` which separates flags into a HashSet (for schema-aware builtins),
1321    /// this preserves the original flag format as strings for external commands:
1322    /// - `-l` stays as `-l`
1323    /// - `--verbose` stays as `--verbose`
1324    /// - `key=value` stays as `key=value`
1325    ///
1326    /// This is what external commands expect in their argv.
1327    async fn build_args_flat(&self, args: &[Arg]) -> Result<Vec<String>> {
1328        let mut argv = Vec::new();
1329        for arg in args {
1330            match arg {
1331                Arg::Positional(expr) => {
1332                    let value = self.eval_expr_async(expr).await?;
1333                    let value = apply_tilde_expansion(value);
1334                    argv.push(value_to_string(&value));
1335                }
1336                Arg::Named { key, value } => {
1337                    let val = self.eval_expr_async(value).await?;
1338                    let val = apply_tilde_expansion(val);
1339                    argv.push(format!("{}={}", key, value_to_string(&val)));
1340                }
1341                Arg::ShortFlag(name) => {
1342                    // Preserve original format: -l, -la (combined flags)
1343                    argv.push(format!("-{}", name));
1344                }
1345                Arg::LongFlag(name) => {
1346                    // Preserve original format: --verbose
1347                    argv.push(format!("--{}", name));
1348                }
1349                Arg::DoubleDash => {
1350                    // Preserve the -- marker
1351                    argv.push("--".to_string());
1352                }
1353            }
1354        }
1355        Ok(argv)
1356    }
1357
1358    /// Async expression evaluator that supports command substitution.
1359    ///
1360    /// This is used for contexts where expressions may contain `$(...)` command
1361    /// substitution. Unlike the sync `eval_expr`, this can execute pipelines.
1362    fn eval_expr_async<'a>(&'a self, expr: &'a Expr) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Value>> + Send + 'a>> {
1363        Box::pin(async move {
1364        match expr {
1365            Expr::Literal(value) => Ok(value.clone()),
1366            Expr::VarRef(path) => {
1367                let scope = self.scope.read().await;
1368                scope.resolve_path(path)
1369                    .ok_or_else(|| anyhow::anyhow!("undefined variable"))
1370            }
1371            Expr::Interpolated(parts) => {
1372                let mut result = String::new();
1373                for part in parts {
1374                    result.push_str(&self.eval_string_part_async(part).await?);
1375                }
1376                Ok(Value::String(result))
1377            }
1378            Expr::BinaryOp { left, op, right } => {
1379                match op {
1380                    BinaryOp::And => {
1381                        let left_val = self.eval_expr_async(left).await?;
1382                        if !is_truthy(&left_val) {
1383                            return Ok(left_val);
1384                        }
1385                        self.eval_expr_async(right).await
1386                    }
1387                    BinaryOp::Or => {
1388                        let left_val = self.eval_expr_async(left).await?;
1389                        if is_truthy(&left_val) {
1390                            return Ok(left_val);
1391                        }
1392                        self.eval_expr_async(right).await
1393                    }
1394                    _ => {
1395                        // For other operators, fall back to sync eval
1396                        let mut scope = self.scope.write().await;
1397                        eval_expr(expr, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1398                    }
1399                }
1400            }
1401            Expr::CommandSubst(pipeline) => {
1402                // Snapshot scope+cwd before running — only output escapes,
1403                // not side effects like `cd` or variable assignments.
1404                let saved_scope = { self.scope.read().await.clone() };
1405                let saved_cwd = {
1406                    let ec = self.exec_ctx.read().await;
1407                    (ec.cwd.clone(), ec.prev_cwd.clone())
1408                };
1409
1410                let result = self.execute_pipeline(pipeline).await?;
1411                self.update_last_result(&result).await;
1412
1413                // Restore scope (preserving last_result) and cwd
1414                {
1415                    let mut scope = self.scope.write().await;
1416                    let last_result = scope.last_result().clone();
1417                    *scope = saved_scope;
1418                    scope.set_last_result(last_result);
1419                }
1420                {
1421                    let mut ec = self.exec_ctx.write().await;
1422                    ec.cwd = saved_cwd.0;
1423                    ec.prev_cwd = saved_cwd.1;
1424                }
1425
1426                // Prefer structured data (enables `for i in $(cmd)` iteration)
1427                if let Some(data) = &result.data {
1428                    Ok(data.clone())
1429                } else {
1430                    // Otherwise return stdout as single string (NO implicit splitting)
1431                    Ok(Value::String(result.out.trim_end().to_string()))
1432                }
1433            }
1434            Expr::Test(test_expr) => {
1435                // For test expressions, use the sync evaluator
1436                let expr = Expr::Test(test_expr.clone());
1437                let mut scope = self.scope.write().await;
1438                eval_expr(&expr, &mut scope).map_err(|e| anyhow::anyhow!("{}", e))
1439            }
1440            Expr::Positional(n) => {
1441                let scope = self.scope.read().await;
1442                match scope.get_positional(*n) {
1443                    Some(s) => Ok(Value::String(s.to_string())),
1444                    None => Ok(Value::String(String::new())),
1445                }
1446            }
1447            Expr::AllArgs => {
1448                let scope = self.scope.read().await;
1449                Ok(Value::String(scope.all_args().join(" ")))
1450            }
1451            Expr::ArgCount => {
1452                let scope = self.scope.read().await;
1453                Ok(Value::Int(scope.arg_count() as i64))
1454            }
1455            Expr::VarLength(name) => {
1456                let scope = self.scope.read().await;
1457                match scope.get(name) {
1458                    Some(value) => Ok(Value::Int(value_to_string(value).len() as i64)),
1459                    None => Ok(Value::Int(0)),
1460                }
1461            }
1462            Expr::VarWithDefault { name, default } => {
1463                let scope = self.scope.read().await;
1464                let use_default = match scope.get(name) {
1465                    Some(value) => value_to_string(value).is_empty(),
1466                    None => true,
1467                };
1468                drop(scope); // Release the lock before recursive evaluation
1469                if use_default {
1470                    // Evaluate the default parts (supports nested expansions)
1471                    self.eval_string_parts_async(default).await.map(Value::String)
1472                } else {
1473                    let scope = self.scope.read().await;
1474                    scope.get(name).cloned().ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))
1475                }
1476            }
1477            Expr::Arithmetic(expr_str) => {
1478                let scope = self.scope.read().await;
1479                crate::arithmetic::eval_arithmetic(expr_str, &scope)
1480                    .map(Value::Int)
1481                    .map_err(|e| anyhow::anyhow!("arithmetic error: {}", e))
1482            }
1483            Expr::Command(cmd) => {
1484                // Execute command and return boolean based on exit code
1485                let result = self.execute_command(&cmd.name, &cmd.args).await?;
1486                Ok(Value::Bool(result.code == 0))
1487            }
1488            Expr::LastExitCode => {
1489                let scope = self.scope.read().await;
1490                Ok(Value::Int(scope.last_result().code))
1491            }
1492            Expr::CurrentPid => {
1493                let scope = self.scope.read().await;
1494                Ok(Value::Int(scope.pid() as i64))
1495            }
1496        }
1497        })
1498    }
1499
1500    /// Async helper to evaluate multiple StringParts into a single string.
1501    fn eval_string_parts_async<'a>(&'a self, parts: &'a [StringPart]) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
1502        Box::pin(async move {
1503            let mut result = String::new();
1504            for part in parts {
1505                result.push_str(&self.eval_string_part_async(part).await?);
1506            }
1507            Ok(result)
1508        })
1509    }
1510
1511    /// Async helper to evaluate a StringPart.
1512    fn eval_string_part_async<'a>(&'a self, part: &'a StringPart) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String>> + Send + 'a>> {
1513        Box::pin(async move {
1514            match part {
1515                StringPart::Literal(s) => Ok(s.clone()),
1516                StringPart::Var(path) => {
1517                    let scope = self.scope.read().await;
1518                    match scope.resolve_path(path) {
1519                        Some(value) => Ok(value_to_string(&value)),
1520                        None => Ok(String::new()), // Unset vars expand to empty
1521                    }
1522                }
1523                StringPart::VarWithDefault { name, default } => {
1524                    let scope = self.scope.read().await;
1525                    let use_default = match scope.get(name) {
1526                        Some(value) => value_to_string(value).is_empty(),
1527                        None => true,
1528                    };
1529                    drop(scope); // Release lock before recursive evaluation
1530                    if use_default {
1531                        // Evaluate the default parts (supports nested expansions)
1532                        self.eval_string_parts_async(default).await
1533                    } else {
1534                        let scope = self.scope.read().await;
1535                        Ok(value_to_string(scope.get(name).ok_or_else(|| anyhow::anyhow!("variable '{}' not found", name))?))
1536                    }
1537                }
1538            StringPart::VarLength(name) => {
1539                let scope = self.scope.read().await;
1540                match scope.get(name) {
1541                    Some(value) => Ok(value_to_string(value).len().to_string()),
1542                    None => Ok("0".to_string()),
1543                }
1544            }
1545            StringPart::Positional(n) => {
1546                let scope = self.scope.read().await;
1547                match scope.get_positional(*n) {
1548                    Some(s) => Ok(s.to_string()),
1549                    None => Ok(String::new()),
1550                }
1551            }
1552            StringPart::AllArgs => {
1553                let scope = self.scope.read().await;
1554                Ok(scope.all_args().join(" "))
1555            }
1556            StringPart::ArgCount => {
1557                let scope = self.scope.read().await;
1558                Ok(scope.arg_count().to_string())
1559            }
1560            StringPart::Arithmetic(expr) => {
1561                let scope = self.scope.read().await;
1562                match crate::arithmetic::eval_arithmetic(expr, &scope) {
1563                    Ok(value) => Ok(value.to_string()),
1564                    Err(_) => Ok(String::new()),
1565                }
1566            }
1567            StringPart::CommandSubst(pipeline) => {
1568                // Snapshot scope+cwd — command substitution in strings must
1569                // not leak side effects (e.g., `"dir: $(cd /; pwd)"` must not change cwd).
1570                let saved_scope = { self.scope.read().await.clone() };
1571                let saved_cwd = {
1572                    let ec = self.exec_ctx.read().await;
1573                    (ec.cwd.clone(), ec.prev_cwd.clone())
1574                };
1575
1576                let result = self.execute_pipeline(pipeline).await?;
1577
1578                // Restore scope (preserving last_result) and cwd
1579                {
1580                    let mut scope = self.scope.write().await;
1581                    let last_result = scope.last_result().clone();
1582                    *scope = saved_scope;
1583                    scope.set_last_result(last_result);
1584                }
1585                {
1586                    let mut ec = self.exec_ctx.write().await;
1587                    ec.cwd = saved_cwd.0;
1588                    ec.prev_cwd = saved_cwd.1;
1589                }
1590
1591                Ok(result.out.trim_end_matches('\n').to_string())
1592            }
1593            StringPart::LastExitCode => {
1594                let scope = self.scope.read().await;
1595                Ok(scope.last_result().code.to_string())
1596            }
1597            StringPart::CurrentPid => {
1598                let scope = self.scope.read().await;
1599                Ok(scope.pid().to_string())
1600            }
1601        }
1602        })
1603    }
1604
1605    /// Update the last result in scope.
1606    async fn update_last_result(&self, result: &ExecResult) {
1607        let mut scope = self.scope.write().await;
1608        scope.set_last_result(result.clone());
1609    }
1610
1611    /// Execute a user-defined function with local variable scoping.
1612    ///
1613    /// Functions push a new scope frame for local variables. Variables declared
1614    /// with `local` are scoped to the function; other assignments modify outer
1615    /// scopes (or create in root if new).
1616    async fn execute_user_tool(&self, def: ToolDef, args: &[Arg]) -> Result<ExecResult> {
1617        // 1. Build function args from AST args (async to support command substitution)
1618        let tool_args = self.build_args_async(args, None).await?;
1619
1620        // 2. Push a new scope frame for local variables
1621        {
1622            let mut scope = self.scope.write().await;
1623            scope.push_frame();
1624        }
1625
1626        // 3. Save current positional parameters and set new ones for this function
1627        let saved_positional = {
1628            let mut scope = self.scope.write().await;
1629            let saved = scope.save_positional();
1630
1631            // Set up new positional parameters ($0 = function name, $1, $2, ... = args)
1632            let positional_args: Vec<String> = tool_args.positional
1633                .iter()
1634                .map(value_to_string)
1635                .collect();
1636            scope.set_positional(&def.name, positional_args);
1637
1638            saved
1639        };
1640
1641        // 3. Execute body statements with control flow handling
1642        // Accumulate output across statements (like sh)
1643        let mut accumulated_out = String::new();
1644        let mut accumulated_err = String::new();
1645        let mut last_code = 0i64;
1646        let mut last_data: Option<Value> = None;
1647
1648        for stmt in &def.body {
1649            match self.execute_stmt_flow(stmt).await {
1650                Ok(flow) => {
1651                    match flow {
1652                        ControlFlow::Normal(r) => {
1653                            accumulated_out.push_str(&r.out);
1654                            accumulated_err.push_str(&r.err);
1655                            last_code = r.code;
1656                            last_data = r.data;
1657                        }
1658                        ControlFlow::Return { value } => {
1659                            // Return from this function with the value
1660                            accumulated_out.push_str(&value.out);
1661                            accumulated_err.push_str(&value.err);
1662                            last_code = value.code;
1663                            last_data = value.data;
1664                            break;
1665                        }
1666                        ControlFlow::Exit { code } => {
1667                            // Exit propagates through - pop frame, restore positional params and return
1668                            let mut scope = self.scope.write().await;
1669                            scope.pop_frame();
1670                            scope.set_positional(saved_positional.0.clone(), saved_positional.1.clone());
1671                            return Ok(ExecResult::failure(code, "exit"));
1672                        }
1673                        ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
1674                            // Break/continue outside a loop - treat as normal
1675                            accumulated_out.push_str(&r.out);
1676                            accumulated_err.push_str(&r.err);
1677                            last_code = r.code;
1678                            last_data = r.data;
1679                        }
1680                    }
1681                }
1682                Err(e) => {
1683                    // Pop frame and restore positional params on error
1684                    let mut scope = self.scope.write().await;
1685                    scope.pop_frame();
1686                    scope.set_positional(saved_positional.0.clone(), saved_positional.1.clone());
1687                    return Err(e);
1688                }
1689            }
1690        }
1691
1692        let result = ExecResult {
1693            code: last_code,
1694            out: accumulated_out,
1695            err: accumulated_err,
1696            data: last_data,
1697            output: None,
1698        };
1699
1700        // 4. Pop scope frame and restore original positional parameters
1701        {
1702            let mut scope = self.scope.write().await;
1703            scope.pop_frame();
1704            scope.set_positional(saved_positional.0, saved_positional.1);
1705        }
1706
1707        // 5. Return final result
1708        Ok(result)
1709    }
1710
1711    /// Execute the `source` / `.` command to include and run a script.
1712    ///
1713    /// Unlike regular tool execution, `source` executes in the CURRENT scope,
1714    /// allowing the sourced script to set variables and modify shell state.
1715    async fn execute_source(&self, args: &[Arg]) -> Result<ExecResult> {
1716        // Get the file path from the first positional argument
1717        let tool_args = self.build_args_async(args, None).await?;
1718        let path = match tool_args.positional.first() {
1719            Some(Value::String(s)) => s.clone(),
1720            Some(v) => value_to_string(v),
1721            None => {
1722                return Ok(ExecResult::failure(1, "source: missing filename"));
1723            }
1724        };
1725
1726        // Resolve path relative to cwd
1727        let full_path = {
1728            let ctx = self.exec_ctx.read().await;
1729            if path.starts_with('/') {
1730                std::path::PathBuf::from(&path)
1731            } else {
1732                ctx.cwd.join(&path)
1733            }
1734        };
1735
1736        // Read file content via backend
1737        let content = {
1738            let ctx = self.exec_ctx.read().await;
1739            match ctx.backend.read(&full_path, None).await {
1740                Ok(bytes) => {
1741                    String::from_utf8(bytes).map_err(|e| {
1742                        anyhow::anyhow!("source: {}: invalid UTF-8: {}", path, e)
1743                    })?
1744                }
1745                Err(e) => {
1746                    return Ok(ExecResult::failure(
1747                        1,
1748                        format!("source: {}: {}", path, e),
1749                    ));
1750                }
1751            }
1752        };
1753
1754        // Parse the content
1755        let program = match crate::parser::parse(&content) {
1756            Ok(p) => p,
1757            Err(errors) => {
1758                let msg = errors
1759                    .iter()
1760                    .map(|e| format!("{}:{}: {}", path, e.span.start, e.message))
1761                    .collect::<Vec<_>>()
1762                    .join("\n");
1763                return Ok(ExecResult::failure(1, format!("source: {}", msg)));
1764            }
1765        };
1766
1767        // Execute each statement in the CURRENT scope (not isolated)
1768        let mut result = ExecResult::success("");
1769        for stmt in program.statements {
1770            if matches!(stmt, crate::ast::Stmt::Empty) {
1771                continue;
1772            }
1773
1774            match self.execute_stmt_flow(&stmt).await {
1775                Ok(flow) => {
1776                    match flow {
1777                        ControlFlow::Normal(r) => {
1778                            result = r.clone();
1779                            self.update_last_result(&r).await;
1780                        }
1781                        ControlFlow::Break { .. } | ControlFlow::Continue { .. } => {
1782                            // break/continue in sourced file - unusual but propagate
1783                            return Err(anyhow::anyhow!(
1784                                "source: {}: unexpected break/continue outside loop",
1785                                path
1786                            ));
1787                        }
1788                        ControlFlow::Return { value } => {
1789                            // Return from sourced script ends the source
1790                            return Ok(value);
1791                        }
1792                        ControlFlow::Exit { code } => {
1793                            // Exit from sourced script propagates
1794                            return Ok(ExecResult::failure(code, "exit"));
1795                        }
1796                    }
1797                }
1798                Err(e) => {
1799                    return Err(e.context(format!("source: {}", path)));
1800                }
1801            }
1802        }
1803
1804        Ok(result)
1805    }
1806
1807    /// Try to execute a script from PATH directories.
1808    ///
1809    /// Searches PATH for `{name}.kai` files and executes them in isolated scope
1810    /// (like user-defined tools). Returns None if no script is found.
1811    async fn try_execute_script(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
1812        // Get PATH from scope (default to "/bin")
1813        let path_value = {
1814            let scope = self.scope.read().await;
1815            scope
1816                .get("PATH")
1817                .map(value_to_string)
1818                .unwrap_or_else(|| "/bin".to_string())
1819        };
1820
1821        // Search PATH directories for script
1822        for dir in path_value.split(':') {
1823            if dir.is_empty() {
1824                continue;
1825            }
1826
1827            // Build script path: {dir}/{name}.kai
1828            let script_path = PathBuf::from(dir).join(format!("{}.kai", name));
1829
1830            // Check if script exists
1831            let exists = {
1832                let ctx = self.exec_ctx.read().await;
1833                ctx.backend.exists(&script_path).await
1834            };
1835
1836            if !exists {
1837                continue;
1838            }
1839
1840            // Read script content
1841            let content = {
1842                let ctx = self.exec_ctx.read().await;
1843                match ctx.backend.read(&script_path, None).await {
1844                    Ok(bytes) => match String::from_utf8(bytes) {
1845                        Ok(s) => s,
1846                        Err(e) => {
1847                            return Ok(Some(ExecResult::failure(
1848                                1,
1849                                format!("{}: invalid UTF-8: {}", script_path.display(), e),
1850                            )));
1851                        }
1852                    },
1853                    Err(e) => {
1854                        return Ok(Some(ExecResult::failure(
1855                            1,
1856                            format!("{}: {}", script_path.display(), e),
1857                        )));
1858                    }
1859                }
1860            };
1861
1862            // Parse the script
1863            let program = match crate::parser::parse(&content) {
1864                Ok(p) => p,
1865                Err(errors) => {
1866                    let msg = errors
1867                        .iter()
1868                        .map(|e| format!("{}:{}: {}", script_path.display(), e.span.start, e.message))
1869                        .collect::<Vec<_>>()
1870                        .join("\n");
1871                    return Ok(Some(ExecResult::failure(1, msg)));
1872                }
1873            };
1874
1875            // Build tool_args from args (async for command substitution support)
1876            let tool_args = self.build_args_async(args, None).await?;
1877
1878            // Create isolated scope (like user tools)
1879            let mut isolated_scope = Scope::new();
1880
1881            // Set up positional parameters ($0 = script name, $1, $2, ... = args)
1882            let positional_args: Vec<String> = tool_args.positional
1883                .iter()
1884                .map(value_to_string)
1885                .collect();
1886            isolated_scope.set_positional(name, positional_args);
1887
1888            // Save current scope and swap with isolated scope
1889            let original_scope = {
1890                let mut scope = self.scope.write().await;
1891                std::mem::replace(&mut *scope, isolated_scope)
1892            };
1893
1894            // Execute script statements
1895            let mut result = ExecResult::success("");
1896            for stmt in program.statements {
1897                if matches!(stmt, crate::ast::Stmt::Empty) {
1898                    continue;
1899                }
1900
1901                match self.execute_stmt_flow(&stmt).await {
1902                    Ok(flow) => {
1903                        match flow {
1904                            ControlFlow::Normal(r) => result = r,
1905                            ControlFlow::Return { value } => {
1906                                result = value;
1907                                break;
1908                            }
1909                            ControlFlow::Exit { code } => {
1910                                // Restore scope and return
1911                                let mut scope = self.scope.write().await;
1912                                *scope = original_scope;
1913                                return Ok(Some(ExecResult::failure(code, "exit")));
1914                            }
1915                            ControlFlow::Break { result: r, .. } | ControlFlow::Continue { result: r, .. } => {
1916                                result = r;
1917                            }
1918                        }
1919                    }
1920                    Err(e) => {
1921                        // Restore original scope on error
1922                        let mut scope = self.scope.write().await;
1923                        *scope = original_scope;
1924                        return Err(e.context(format!("script: {}", script_path.display())));
1925                    }
1926                }
1927            }
1928
1929            // Restore original scope
1930            {
1931                let mut scope = self.scope.write().await;
1932                *scope = original_scope;
1933            }
1934
1935            return Ok(Some(result));
1936        }
1937
1938        // No script found
1939        Ok(None)
1940    }
1941
1942    /// Try to execute an external command from PATH.
1943    ///
1944    /// This is the fallback when no builtin or user-defined tool matches.
1945    /// External commands receive a clean argv (flags preserved in their original format).
1946    ///
1947    /// # Requirements
1948    /// - Command must be found in PATH
1949    /// - Current working directory must be on a real filesystem (not virtual like /scratch)
1950    ///
1951    /// # Returns
1952    /// - `Ok(Some(result))` if command was found and executed
1953    /// - `Ok(None)` if command was not found in PATH
1954    /// - `Err` on execution errors
1955    async fn try_execute_external(&self, name: &str, args: &[Arg]) -> Result<Option<ExecResult>> {
1956        // Skip if name contains path separator (absolute/relative paths handled differently)
1957        if name.contains('/') {
1958            return Ok(None);
1959        }
1960
1961        // Get PATH from scope or environment
1962        let path_var = {
1963            let scope = self.scope.read().await;
1964            scope
1965                .get("PATH")
1966                .map(value_to_string)
1967                .unwrap_or_else(|| std::env::var("PATH").unwrap_or_default())
1968        };
1969
1970        // Resolve command in PATH
1971        let executable = match resolve_in_path(name, &path_var) {
1972            Some(path) => path,
1973            None => return Ok(None), // Not found - let caller handle error
1974        };
1975
1976        // Get current working directory and verify it's on real filesystem
1977        let real_cwd = {
1978            let ctx = self.exec_ctx.read().await;
1979            match ctx.backend.resolve_real_path(&ctx.cwd) {
1980                Some(p) => p,
1981                None => {
1982                    return Ok(Some(ExecResult::failure(
1983                        1,
1984                        format!(
1985                            "{}: cannot run external command from virtual directory '{}'",
1986                            name,
1987                            ctx.cwd.display()
1988                        ),
1989                    )));
1990                }
1991            }
1992        };
1993
1994        // Build flat argv (preserves flag format)
1995        let argv = self.build_args_flat(args).await?;
1996
1997        // Get stdin if available
1998        let stdin_data = {
1999            let mut ctx = self.exec_ctx.write().await;
2000            ctx.take_stdin()
2001        };
2002
2003        // Build and spawn the command
2004        use tokio::process::Command;
2005
2006        let mut cmd = Command::new(&executable);
2007        cmd.args(&argv);
2008        cmd.current_dir(&real_cwd);
2009
2010        // Handle stdin
2011        cmd.stdin(if stdin_data.is_some() {
2012            std::process::Stdio::piped()
2013        } else {
2014            std::process::Stdio::null()
2015        });
2016
2017        // In interactive mode, standalone commands (no piped stdin) inherit
2018        // the terminal's stdout/stderr so output streams in real-time.
2019        let inherit_output = self.interactive && stdin_data.is_none();
2020
2021        if inherit_output {
2022            cmd.stdout(std::process::Stdio::inherit());
2023            cmd.stderr(std::process::Stdio::inherit());
2024        } else {
2025            cmd.stdout(std::process::Stdio::piped());
2026            cmd.stderr(std::process::Stdio::piped());
2027        }
2028
2029        // Spawn the process
2030        let mut child = match cmd.spawn() {
2031            Ok(child) => child,
2032            Err(e) => {
2033                return Ok(Some(ExecResult::failure(
2034                    127,
2035                    format!("{}: {}", name, e),
2036                )));
2037            }
2038        };
2039
2040        // Write stdin if present
2041        if let Some(data) = stdin_data
2042            && let Some(mut stdin) = child.stdin.take()
2043        {
2044            use tokio::io::AsyncWriteExt;
2045            if let Err(e) = stdin.write_all(data.as_bytes()).await {
2046                return Ok(Some(ExecResult::failure(
2047                    1,
2048                    format!("{}: failed to write stdin: {}", name, e),
2049                )));
2050            }
2051            // Drop stdin to signal EOF
2052        }
2053
2054        if inherit_output {
2055            // Output goes directly to terminal — just wait for exit
2056            let status = match child.wait().await {
2057                Ok(s) => s,
2058                Err(e) => {
2059                    return Ok(Some(ExecResult::failure(
2060                        1,
2061                        format!("{}: failed to wait: {}", name, e),
2062                    )));
2063                }
2064            };
2065
2066            let code = status.code().unwrap_or_else(|| {
2067                #[cfg(unix)]
2068                {
2069                    use std::os::unix::process::ExitStatusExt;
2070                    128 + status.signal().unwrap_or(0)
2071                }
2072                #[cfg(not(unix))]
2073                {
2074                    -1
2075                }
2076            }) as i64;
2077
2078            // stdout/stderr already went to the terminal
2079            Ok(Some(ExecResult::from_output(code, String::new(), String::new())))
2080        } else {
2081            // Capture output via bounded streams
2082            let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2083            let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
2084
2085            let stdout_pipe = child.stdout.take();
2086            let stderr_pipe = child.stderr.take();
2087
2088            let stdout_clone = stdout_stream.clone();
2089            let stderr_clone = stderr_stream.clone();
2090
2091            let stdout_task = stdout_pipe.map(|pipe| {
2092                tokio::spawn(async move {
2093                    drain_to_stream(pipe, stdout_clone).await;
2094                })
2095            });
2096
2097            let stderr_task = stderr_pipe.map(|pipe| {
2098                tokio::spawn(async move {
2099                    drain_to_stream(pipe, stderr_clone).await;
2100                })
2101            });
2102
2103            let status = match child.wait().await {
2104                Ok(s) => s,
2105                Err(e) => {
2106                    return Ok(Some(ExecResult::failure(
2107                        1,
2108                        format!("{}: failed to wait: {}", name, e),
2109                    )));
2110                }
2111            };
2112
2113            if let Some(task) = stdout_task {
2114                // Ignore join error — the drain task logs its own errors
2115                let _ = task.await;
2116            }
2117            if let Some(task) = stderr_task {
2118                let _ = task.await;
2119            }
2120
2121            let code = status.code().unwrap_or_else(|| {
2122                #[cfg(unix)]
2123                {
2124                    use std::os::unix::process::ExitStatusExt;
2125                    128 + status.signal().unwrap_or(0)
2126                }
2127                #[cfg(not(unix))]
2128                {
2129                    -1
2130                }
2131            }) as i64;
2132
2133            let stdout = stdout_stream.read_string().await;
2134            let stderr = stderr_stream.read_string().await;
2135
2136            Ok(Some(ExecResult::from_output(code, stdout, stderr)))
2137        }
2138    }
2139
2140    // --- Variable Access ---
2141
2142    /// Get a variable value.
2143    pub async fn get_var(&self, name: &str) -> Option<Value> {
2144        let scope = self.scope.read().await;
2145        scope.get(name).cloned()
2146    }
2147
2148    /// Check if error-exit mode is enabled (for testing).
2149    #[cfg(test)]
2150    pub async fn error_exit_enabled(&self) -> bool {
2151        let scope = self.scope.read().await;
2152        scope.error_exit_enabled()
2153    }
2154
2155    /// Set a variable value.
2156    pub async fn set_var(&self, name: &str, value: Value) {
2157        let mut scope = self.scope.write().await;
2158        scope.set(name.to_string(), value);
2159    }
2160
2161    /// List all variables.
2162    pub async fn list_vars(&self) -> Vec<(String, Value)> {
2163        let scope = self.scope.read().await;
2164        scope.all()
2165    }
2166
2167    // --- CWD ---
2168
2169    /// Get current working directory.
2170    pub async fn cwd(&self) -> PathBuf {
2171        self.exec_ctx.read().await.cwd.clone()
2172    }
2173
2174    /// Set current working directory.
2175    pub async fn set_cwd(&self, path: PathBuf) {
2176        let mut ctx = self.exec_ctx.write().await;
2177        ctx.set_cwd(path);
2178    }
2179
2180    // --- Last Result ---
2181
2182    /// Get the last result ($?).
2183    pub async fn last_result(&self) -> ExecResult {
2184        let scope = self.scope.read().await;
2185        scope.last_result().clone()
2186    }
2187
2188    // --- Tools ---
2189
2190    /// Get available tool schemas.
2191    pub fn tool_schemas(&self) -> Vec<crate::tools::ToolSchema> {
2192        self.tools.schemas()
2193    }
2194
2195    // --- Jobs ---
2196
2197    /// Get job manager.
2198    pub fn jobs(&self) -> Arc<JobManager> {
2199        self.jobs.clone()
2200    }
2201
2202    // --- VFS ---
2203
2204    /// Get VFS router.
2205    pub fn vfs(&self) -> Arc<VfsRouter> {
2206        self.vfs.clone()
2207    }
2208
2209    // --- State ---
2210
2211    /// Reset kernel to initial state.
2212    ///
2213    /// Clears in-memory variables and resets cwd to root.
2214    /// History is not cleared (it persists across resets).
2215    pub async fn reset(&self) -> Result<()> {
2216        {
2217            let mut scope = self.scope.write().await;
2218            *scope = Scope::new();
2219        }
2220        {
2221            let mut ctx = self.exec_ctx.write().await;
2222            ctx.cwd = PathBuf::from("/");
2223        }
2224        Ok(())
2225    }
2226
2227    /// Shutdown the kernel.
2228    pub async fn shutdown(self) -> Result<()> {
2229        // Wait for all background jobs
2230        self.jobs.wait_all().await;
2231        Ok(())
2232    }
2233
2234    /// Dispatch a single command using the full resolution chain.
2235    ///
2236    /// This is the core of `CommandDispatcher` — it syncs state between the
2237    /// passed-in `ExecContext` and kernel-internal state (scope, exec_ctx),
2238    /// then delegates to `execute_command` for the actual dispatch.
2239    ///
2240    /// State flow:
2241    /// 1. ctx → self: sync scope, cwd, stdin so internal methods see current state
2242    /// 2. execute_command: full dispatch chain (user tools, builtins, scripts, external, backend)
2243    /// 3. self → ctx: sync scope, cwd changes back so the pipeline runner sees them
2244    async fn dispatch_command(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
2245        // 1. Sync ctx → self internals
2246        {
2247            let mut scope = self.scope.write().await;
2248            *scope = ctx.scope.clone();
2249        }
2250        {
2251            let mut ec = self.exec_ctx.write().await;
2252            ec.cwd = ctx.cwd.clone();
2253            ec.prev_cwd = ctx.prev_cwd.clone();
2254            ec.stdin = ctx.stdin.take();
2255            ec.stdin_data = ctx.stdin_data.take();
2256        }
2257
2258        // 2. Execute via the full dispatch chain
2259        let result = self.execute_command(&cmd.name, &cmd.args).await?;
2260
2261        // 3. Sync self → ctx
2262        {
2263            let scope = self.scope.read().await;
2264            ctx.scope = scope.clone();
2265        }
2266        {
2267            let ec = self.exec_ctx.read().await;
2268            ctx.cwd = ec.cwd.clone();
2269            ctx.prev_cwd = ec.prev_cwd.clone();
2270        }
2271
2272        Ok(result)
2273    }
2274}
2275
2276#[async_trait]
2277impl CommandDispatcher for Kernel {
2278    /// Dispatch a command through the Kernel's full resolution chain.
2279    ///
2280    /// This is the single path for all command execution when called from
2281    /// the pipeline runner. It provides the full dispatch chain:
2282    /// user tools → builtins → .kai scripts → external commands → backend tools.
2283    async fn dispatch(&self, cmd: &Command, ctx: &mut ExecContext) -> Result<ExecResult> {
2284        self.dispatch_command(cmd, ctx).await
2285    }
2286}
2287
2288/// Accumulate output from one result into another.
2289///
2290/// This appends stdout and stderr (with newlines as separators) and updates
2291/// the exit code to match the new result. Used to preserve output from
2292/// multiple statements, loop iterations, and command chains.
2293fn accumulate_result(accumulated: &mut ExecResult, new: &ExecResult) {
2294    if !accumulated.out.is_empty() && !new.out.is_empty() {
2295        accumulated.out.push('\n');
2296    }
2297    accumulated.out.push_str(&new.out);
2298    if !accumulated.err.is_empty() && !new.err.is_empty() {
2299        accumulated.err.push('\n');
2300    }
2301    accumulated.err.push_str(&new.err);
2302    accumulated.code = new.code;
2303    accumulated.data = new.data.clone();
2304}
2305
2306/// Check if a value is truthy.
2307fn is_truthy(value: &Value) -> bool {
2308    match value {
2309        Value::Null => false,
2310        Value::Bool(b) => *b,
2311        Value::Int(i) => *i != 0,
2312        Value::Float(f) => *f != 0.0,
2313        Value::String(s) => !s.is_empty(),
2314        Value::Json(json) => match json {
2315            serde_json::Value::Null => false,
2316            serde_json::Value::Array(arr) => !arr.is_empty(),
2317            serde_json::Value::Object(obj) => !obj.is_empty(),
2318            serde_json::Value::Bool(b) => *b,
2319            serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
2320            serde_json::Value::String(s) => !s.is_empty(),
2321        },
2322        Value::Blob(_) => true, // Blob references are always truthy
2323    }
2324}
2325
2326/// Apply tilde expansion to a value.
2327///
2328/// Only string values starting with `~` are expanded.
2329fn apply_tilde_expansion(value: Value) -> Value {
2330    match value {
2331        Value::String(s) if s.starts_with('~') => Value::String(expand_tilde(&s)),
2332        _ => value,
2333    }
2334}
2335
2336#[cfg(test)]
2337mod tests {
2338    use super::*;
2339
2340    #[tokio::test]
2341    async fn test_kernel_transient() {
2342        let kernel = Kernel::transient().expect("failed to create kernel");
2343        assert_eq!(kernel.name(), "transient");
2344    }
2345
2346    #[tokio::test]
2347    async fn test_kernel_execute_echo() {
2348        let kernel = Kernel::transient().expect("failed to create kernel");
2349        let result = kernel.execute("echo hello").await.expect("execution failed");
2350        assert!(result.ok());
2351        assert_eq!(result.out.trim(), "hello");
2352    }
2353
2354    #[tokio::test]
2355    async fn test_multiple_statements_accumulate_output() {
2356        let kernel = Kernel::transient().expect("failed to create kernel");
2357        let result = kernel
2358            .execute("echo one\necho two\necho three")
2359            .await
2360            .expect("execution failed");
2361        assert!(result.ok());
2362        // Should have all three outputs separated by newlines
2363        assert!(result.out.contains("one"), "missing 'one': {}", result.out);
2364        assert!(result.out.contains("two"), "missing 'two': {}", result.out);
2365        assert!(result.out.contains("three"), "missing 'three': {}", result.out);
2366    }
2367
2368    #[tokio::test]
2369    async fn test_and_chain_accumulates_output() {
2370        let kernel = Kernel::transient().expect("failed to create kernel");
2371        let result = kernel
2372            .execute("echo first && echo second")
2373            .await
2374            .expect("execution failed");
2375        assert!(result.ok());
2376        assert!(result.out.contains("first"), "missing 'first': {}", result.out);
2377        assert!(result.out.contains("second"), "missing 'second': {}", result.out);
2378    }
2379
2380    #[tokio::test]
2381    async fn test_for_loop_accumulates_output() {
2382        let kernel = Kernel::transient().expect("failed to create kernel");
2383        let result = kernel
2384            .execute(r#"for X in a b c; do echo "item: ${X}"; done"#)
2385            .await
2386            .expect("execution failed");
2387        assert!(result.ok());
2388        assert!(result.out.contains("item: a"), "missing 'item: a': {}", result.out);
2389        assert!(result.out.contains("item: b"), "missing 'item: b': {}", result.out);
2390        assert!(result.out.contains("item: c"), "missing 'item: c': {}", result.out);
2391    }
2392
2393    #[tokio::test]
2394    async fn test_while_loop_accumulates_output() {
2395        let kernel = Kernel::transient().expect("failed to create kernel");
2396        let result = kernel
2397            .execute(r#"
2398                N=3
2399                while [[ ${N} -gt 0 ]]; do
2400                    echo "N=${N}"
2401                    N=$((N - 1))
2402                done
2403            "#)
2404            .await
2405            .expect("execution failed");
2406        assert!(result.ok());
2407        assert!(result.out.contains("N=3"), "missing 'N=3': {}", result.out);
2408        assert!(result.out.contains("N=2"), "missing 'N=2': {}", result.out);
2409        assert!(result.out.contains("N=1"), "missing 'N=1': {}", result.out);
2410    }
2411
2412    #[tokio::test]
2413    async fn test_kernel_set_var() {
2414        let kernel = Kernel::transient().expect("failed to create kernel");
2415
2416        kernel.execute("X=42").await.expect("set failed");
2417
2418        let value = kernel.get_var("X").await;
2419        assert_eq!(value, Some(Value::Int(42)));
2420    }
2421
2422    #[tokio::test]
2423    async fn test_kernel_var_expansion() {
2424        let kernel = Kernel::transient().expect("failed to create kernel");
2425
2426        kernel.execute("NAME=\"world\"").await.expect("set failed");
2427        let result = kernel.execute("echo \"hello ${NAME}\"").await.expect("echo failed");
2428
2429        assert!(result.ok());
2430        assert_eq!(result.out.trim(), "hello world");
2431    }
2432
2433    #[tokio::test]
2434    async fn test_kernel_last_result() {
2435        let kernel = Kernel::transient().expect("failed to create kernel");
2436
2437        kernel.execute("echo test").await.expect("echo failed");
2438
2439        let last = kernel.last_result().await;
2440        assert!(last.ok());
2441        assert_eq!(last.out.trim(), "test");
2442    }
2443
2444    #[tokio::test]
2445    async fn test_kernel_tool_not_found() {
2446        let kernel = Kernel::transient().expect("failed to create kernel");
2447
2448        let result = kernel.execute("nonexistent_tool").await.expect("execution failed");
2449        assert!(!result.ok());
2450        assert_eq!(result.code, 127);
2451        assert!(result.err.contains("command not found"));
2452    }
2453
2454    #[tokio::test]
2455    async fn test_external_command_true() {
2456        // Use REPL config for passthrough filesystem access
2457        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
2458
2459        // /bin/true should be available on any Unix system
2460        let result = kernel.execute("true").await.expect("execution failed");
2461        // This should use the builtin true, which returns 0
2462        assert!(result.ok(), "true should succeed: {:?}", result);
2463    }
2464
2465    #[tokio::test]
2466    async fn test_external_command_basic() {
2467        // Use REPL config for passthrough filesystem access
2468        let kernel = Kernel::new(KernelConfig::repl()).expect("failed to create kernel");
2469
2470        // Test with /bin/echo which is external
2471        // Note: kaish has a builtin echo, so this will use the builtin
2472        // Let's test with a command that's not a builtin
2473        // Actually, let's just test that PATH resolution works by checking the PATH var
2474        let path_var = std::env::var("PATH").unwrap_or_default();
2475        eprintln!("System PATH: {}", path_var);
2476
2477        // Set PATH in kernel to ensure it's available
2478        kernel.execute(&format!(r#"PATH="{}""#, path_var)).await.expect("set PATH failed");
2479
2480        // Now try an external command like /usr/bin/env
2481        // But env is also a builtin... let's try uname
2482        let result = kernel.execute("uname").await.expect("execution failed");
2483        eprintln!("uname result: {:?}", result);
2484        // uname should succeed if external commands work
2485        assert!(result.ok() || result.code == 127, "uname: {:?}", result);
2486    }
2487
2488    #[tokio::test]
2489    async fn test_kernel_reset() {
2490        let kernel = Kernel::transient().expect("failed to create kernel");
2491
2492        kernel.execute("X=1").await.expect("set failed");
2493        assert!(kernel.get_var("X").await.is_some());
2494
2495        kernel.reset().await.expect("reset failed");
2496        assert!(kernel.get_var("X").await.is_none());
2497    }
2498
2499    #[tokio::test]
2500    async fn test_kernel_cwd() {
2501        let kernel = Kernel::transient().expect("failed to create kernel");
2502
2503        // Transient kernel uses sandboxed mode with cwd=$HOME
2504        let cwd = kernel.cwd().await;
2505        let home = std::env::var("HOME")
2506            .map(PathBuf::from)
2507            .unwrap_or_else(|_| PathBuf::from("/"));
2508        assert_eq!(cwd, home);
2509
2510        kernel.set_cwd(PathBuf::from("/tmp")).await;
2511        assert_eq!(kernel.cwd().await, PathBuf::from("/tmp"));
2512    }
2513
2514    #[tokio::test]
2515    async fn test_kernel_list_vars() {
2516        let kernel = Kernel::transient().expect("failed to create kernel");
2517
2518        kernel.execute("A=1").await.ok();
2519        kernel.execute("B=2").await.ok();
2520
2521        let vars = kernel.list_vars().await;
2522        assert!(vars.iter().any(|(n, v)| n == "A" && *v == Value::Int(1)));
2523        assert!(vars.iter().any(|(n, v)| n == "B" && *v == Value::Int(2)));
2524    }
2525
2526    #[tokio::test]
2527    async fn test_is_truthy() {
2528        assert!(!is_truthy(&Value::Null));
2529        assert!(!is_truthy(&Value::Bool(false)));
2530        assert!(is_truthy(&Value::Bool(true)));
2531        assert!(!is_truthy(&Value::Int(0)));
2532        assert!(is_truthy(&Value::Int(1)));
2533        assert!(!is_truthy(&Value::String("".into())));
2534        assert!(is_truthy(&Value::String("x".into())));
2535    }
2536
2537    #[tokio::test]
2538    async fn test_jq_in_pipeline() {
2539        let kernel = Kernel::transient().expect("failed to create kernel");
2540        // kaish uses double quotes only; escape inner quotes
2541        let result = kernel
2542            .execute(r#"echo "{\"name\": \"Alice\"}" | jq ".name" -r"#)
2543            .await
2544            .expect("execution failed");
2545        assert!(result.ok(), "jq pipeline failed: {}", result.err);
2546        assert_eq!(result.out.trim(), "Alice");
2547    }
2548
2549    #[tokio::test]
2550    async fn test_user_defined_tool() {
2551        let kernel = Kernel::transient().expect("failed to create kernel");
2552
2553        // Define a function
2554        kernel
2555            .execute(r#"greet() { echo "Hello, $1!" }"#)
2556            .await
2557            .expect("function definition failed");
2558
2559        // Call the function
2560        let result = kernel
2561            .execute(r#"greet "World""#)
2562            .await
2563            .expect("function call failed");
2564
2565        assert!(result.ok(), "greet failed: {}", result.err);
2566        assert_eq!(result.out.trim(), "Hello, World!");
2567    }
2568
2569    #[tokio::test]
2570    async fn test_user_tool_positional_args() {
2571        let kernel = Kernel::transient().expect("failed to create kernel");
2572
2573        // Define a function with positional param
2574        kernel
2575            .execute(r#"greet() { echo "Hi $1" }"#)
2576            .await
2577            .expect("function definition failed");
2578
2579        // Call with positional argument
2580        let result = kernel
2581            .execute(r#"greet "Amy""#)
2582            .await
2583            .expect("function call failed");
2584
2585        assert!(result.ok(), "greet failed: {}", result.err);
2586        assert_eq!(result.out.trim(), "Hi Amy");
2587    }
2588
2589    #[tokio::test]
2590    async fn test_function_shared_scope() {
2591        let kernel = Kernel::transient().expect("failed to create kernel");
2592
2593        // Set a variable in parent scope
2594        kernel
2595            .execute(r#"SECRET="hidden""#)
2596            .await
2597            .expect("set failed");
2598
2599        // Define a function that accesses and modifies parent variable
2600        kernel
2601            .execute(r#"access_parent() {
2602                echo "${SECRET}"
2603                SECRET="modified"
2604            }"#)
2605            .await
2606            .expect("function definition failed");
2607
2608        // Call the function - it SHOULD see SECRET (shared scope like sh)
2609        let result = kernel.execute("access_parent").await.expect("function call failed");
2610
2611        // Function should have access to parent scope
2612        assert!(
2613            result.out.contains("hidden"),
2614            "Function should access parent scope, got: {}",
2615            result.out
2616        );
2617
2618        // Function should have modified the parent variable
2619        let secret = kernel.get_var("SECRET").await;
2620        assert_eq!(
2621            secret,
2622            Some(Value::String("modified".into())),
2623            "Function should modify parent scope"
2624        );
2625    }
2626
2627    #[tokio::test]
2628    async fn test_exec_builtin() {
2629        let kernel = Kernel::transient().expect("failed to create kernel");
2630        // argv is now a space-separated string or JSON array string
2631        let result = kernel
2632            .execute(r#"exec command="/bin/echo" argv="hello world""#)
2633            .await
2634            .expect("exec failed");
2635
2636        assert!(result.ok(), "exec failed: {}", result.err);
2637        assert_eq!(result.out.trim(), "hello world");
2638    }
2639
2640    #[tokio::test]
2641    async fn test_while_false_never_runs() {
2642        let kernel = Kernel::transient().expect("failed to create kernel");
2643
2644        // A while loop with false condition should never run
2645        let result = kernel
2646            .execute(r#"
2647                while false; do
2648                    echo "should not run"
2649                done
2650            "#)
2651            .await
2652            .expect("while false failed");
2653
2654        assert!(result.ok());
2655        assert!(result.out.is_empty(), "while false should not execute body: {}", result.out);
2656    }
2657
2658    #[tokio::test]
2659    async fn test_while_string_comparison() {
2660        let kernel = Kernel::transient().expect("failed to create kernel");
2661
2662        // Set a flag
2663        kernel.execute(r#"FLAG="go""#).await.expect("set failed");
2664
2665        // Use string comparison as condition (shell-compatible [[ ]] syntax)
2666        // Note: Put echo last so we can check the output
2667        let result = kernel
2668            .execute(r#"
2669                while [[ ${FLAG} == "go" ]]; do
2670                    FLAG="stop"
2671                    echo "running"
2672                done
2673            "#)
2674            .await
2675            .expect("while with string cmp failed");
2676
2677        assert!(result.ok());
2678        assert!(result.out.contains("running"), "should have run once: {}", result.out);
2679
2680        // Verify flag was changed
2681        let flag = kernel.get_var("FLAG").await;
2682        assert_eq!(flag, Some(Value::String("stop".into())));
2683    }
2684
2685    #[tokio::test]
2686    async fn test_while_numeric_comparison() {
2687        let kernel = Kernel::transient().expect("failed to create kernel");
2688
2689        // Test > comparison (shell-compatible [[ ]] with -gt)
2690        kernel.execute("N=5").await.expect("set failed");
2691
2692        // Note: Put echo last so we can check the output
2693        let result = kernel
2694            .execute(r#"
2695                while [[ ${N} -gt 3 ]]; do
2696                    N=3
2697                    echo "N was greater"
2698                done
2699            "#)
2700            .await
2701            .expect("while with > failed");
2702
2703        assert!(result.ok());
2704        assert!(result.out.contains("N was greater"), "should have run once: {}", result.out);
2705    }
2706
2707    #[tokio::test]
2708    async fn test_break_in_while_loop() {
2709        let kernel = Kernel::transient().expect("failed to create kernel");
2710
2711        let result = kernel
2712            .execute(r#"
2713                I=0
2714                while true; do
2715                    I=1
2716                    echo "before break"
2717                    break
2718                    echo "after break"
2719                done
2720            "#)
2721            .await
2722            .expect("while with break failed");
2723
2724        assert!(result.ok());
2725        assert!(result.out.contains("before break"), "should see before break: {}", result.out);
2726        assert!(!result.out.contains("after break"), "should not see after break: {}", result.out);
2727
2728        // Verify we exited the loop
2729        let i = kernel.get_var("I").await;
2730        assert_eq!(i, Some(Value::Int(1)));
2731    }
2732
2733    #[tokio::test]
2734    async fn test_continue_in_while_loop() {
2735        let kernel = Kernel::transient().expect("failed to create kernel");
2736
2737        // Test continue in a while loop where variables persist
2738        // We use string state transition: "start" -> "middle" -> "end"
2739        // continue on "middle" should skip to next iteration
2740        // Shell-compatible: use [[ ]] for comparisons
2741        let result = kernel
2742            .execute(r#"
2743                STATE="start"
2744                AFTER_CONTINUE="no"
2745                while [[ ${STATE} != "done" ]]; do
2746                    if [[ ${STATE} == "start" ]]; then
2747                        STATE="middle"
2748                        continue
2749                        AFTER_CONTINUE="yes"
2750                    fi
2751                    if [[ ${STATE} == "middle" ]]; then
2752                        STATE="done"
2753                    fi
2754                done
2755            "#)
2756            .await
2757            .expect("while with continue failed");
2758
2759        assert!(result.ok());
2760
2761        // STATE should be "done" (we completed the loop)
2762        let state = kernel.get_var("STATE").await;
2763        assert_eq!(state, Some(Value::String("done".into())));
2764
2765        // AFTER_CONTINUE should still be "no" (continue skipped the assignment)
2766        let after = kernel.get_var("AFTER_CONTINUE").await;
2767        assert_eq!(after, Some(Value::String("no".into())));
2768    }
2769
2770    #[tokio::test]
2771    async fn test_break_with_level() {
2772        let kernel = Kernel::transient().expect("failed to create kernel");
2773
2774        // Nested loop with break 2 to exit both loops
2775        // We verify by checking OUTER value:
2776        // - If break 2 works, OUTER stays at 1 (set before for loop)
2777        // - If break 2 fails, OUTER becomes 2 (set after for loop)
2778        let result = kernel
2779            .execute(r#"
2780                OUTER=0
2781                while true; do
2782                    OUTER=1
2783                    for X in "1 2"; do
2784                        break 2
2785                    done
2786                    OUTER=2
2787                done
2788            "#)
2789            .await
2790            .expect("nested break failed");
2791
2792        assert!(result.ok());
2793
2794        // OUTER should be 1 (set before for loop), not 2 (would be set after for loop)
2795        let outer = kernel.get_var("OUTER").await;
2796        assert_eq!(outer, Some(Value::Int(1)), "break 2 should have skipped OUTER=2");
2797    }
2798
2799    #[tokio::test]
2800    async fn test_return_from_tool() {
2801        let kernel = Kernel::transient().expect("failed to create kernel");
2802
2803        // Define a function that returns early
2804        kernel
2805            .execute(r#"early_return() {
2806                if [[ $1 == 1 ]]; then
2807                    return 42
2808                fi
2809                echo "not returned"
2810            }"#)
2811            .await
2812            .expect("function definition failed");
2813
2814        // Call with arg=1 should return with exit code 42
2815        // (POSIX shell behavior: return N sets exit code, doesn't output N)
2816        let result = kernel
2817            .execute("early_return 1")
2818            .await
2819            .expect("function call failed");
2820
2821        // Exit code should be 42 (non-zero, so not ok())
2822        assert_eq!(result.code, 42);
2823        // Output should be empty (we returned before echo)
2824        assert!(result.out.is_empty());
2825    }
2826
2827    #[tokio::test]
2828    async fn test_return_without_value() {
2829        let kernel = Kernel::transient().expect("failed to create kernel");
2830
2831        // Define a function that returns without a value
2832        kernel
2833            .execute(r#"early_exit() {
2834                if [[ $1 == "stop" ]]; then
2835                    return
2836                fi
2837                echo "continued"
2838            }"#)
2839            .await
2840            .expect("function definition failed");
2841
2842        // Call with arg="stop" should return early
2843        let result = kernel
2844            .execute(r#"early_exit "stop""#)
2845            .await
2846            .expect("function call failed");
2847
2848        assert!(result.ok());
2849        assert!(result.out.is_empty() || result.out.trim().is_empty());
2850    }
2851
2852    #[tokio::test]
2853    async fn test_exit_stops_execution() {
2854        let kernel = Kernel::transient().expect("failed to create kernel");
2855
2856        // exit should stop further execution
2857        kernel
2858            .execute(r#"
2859                BEFORE="yes"
2860                exit 0
2861                AFTER="yes"
2862            "#)
2863            .await
2864            .expect("execution failed");
2865
2866        // BEFORE should be set, AFTER should not
2867        let before = kernel.get_var("BEFORE").await;
2868        assert_eq!(before, Some(Value::String("yes".into())));
2869
2870        let after = kernel.get_var("AFTER").await;
2871        assert!(after.is_none(), "AFTER should not be set after exit");
2872    }
2873
2874    #[tokio::test]
2875    async fn test_exit_with_code() {
2876        let kernel = Kernel::transient().expect("failed to create kernel");
2877
2878        // exit with code should propagate the exit code
2879        let result = kernel
2880            .execute("exit 42")
2881            .await
2882            .expect("exit failed");
2883
2884        // The exit code should be in the output
2885        assert_eq!(result.out, "42");
2886    }
2887
2888    #[tokio::test]
2889    async fn test_set_e_stops_on_failure() {
2890        let kernel = Kernel::transient().expect("failed to create kernel");
2891
2892        // Enable error-exit mode
2893        kernel.execute("set -e").await.expect("set -e failed");
2894
2895        // Run a sequence where the middle command fails
2896        kernel
2897            .execute(r#"
2898                STEP1="done"
2899                false
2900                STEP2="done"
2901            "#)
2902            .await
2903            .expect("execution failed");
2904
2905        // STEP1 should be set, but STEP2 should NOT be set (exit on false)
2906        let step1 = kernel.get_var("STEP1").await;
2907        assert_eq!(step1, Some(Value::String("done".into())));
2908
2909        let step2 = kernel.get_var("STEP2").await;
2910        assert!(step2.is_none(), "STEP2 should not be set after false with set -e");
2911    }
2912
2913    #[tokio::test]
2914    async fn test_set_plus_e_disables_error_exit() {
2915        let kernel = Kernel::transient().expect("failed to create kernel");
2916
2917        // Enable then disable error-exit mode
2918        kernel.execute("set -e").await.expect("set -e failed");
2919        kernel.execute("set +e").await.expect("set +e failed");
2920
2921        // Now failure should NOT stop execution
2922        kernel
2923            .execute(r#"
2924                STEP1="done"
2925                false
2926                STEP2="done"
2927            "#)
2928            .await
2929            .expect("execution failed");
2930
2931        // Both should be set since +e disables error exit
2932        let step1 = kernel.get_var("STEP1").await;
2933        assert_eq!(step1, Some(Value::String("done".into())));
2934
2935        let step2 = kernel.get_var("STEP2").await;
2936        assert_eq!(step2, Some(Value::String("done".into())));
2937    }
2938
2939    #[tokio::test]
2940    async fn test_set_ignores_unknown_options() {
2941        let kernel = Kernel::transient().expect("failed to create kernel");
2942
2943        // Bash idiom: set -euo pipefail (we support -e, ignore the rest)
2944        let result = kernel
2945            .execute("set -e -u -o pipefail")
2946            .await
2947            .expect("set with unknown options failed");
2948
2949        assert!(result.ok(), "set should succeed with unknown options");
2950
2951        // -e should still be enabled
2952        kernel
2953            .execute(r#"
2954                BEFORE="yes"
2955                false
2956                AFTER="yes"
2957            "#)
2958            .await
2959            .ok();
2960
2961        let after = kernel.get_var("AFTER").await;
2962        assert!(after.is_none(), "-e should be enabled despite unknown options");
2963    }
2964
2965    #[tokio::test]
2966    async fn test_set_no_args_shows_settings() {
2967        let kernel = Kernel::transient().expect("failed to create kernel");
2968
2969        // Enable -e
2970        kernel.execute("set -e").await.expect("set -e failed");
2971
2972        // Call set with no args to see settings
2973        let result = kernel.execute("set").await.expect("set failed");
2974
2975        assert!(result.ok());
2976        assert!(result.out.contains("set -e"), "should show -e is enabled: {}", result.out);
2977    }
2978
2979    #[tokio::test]
2980    async fn test_set_e_in_pipeline() {
2981        let kernel = Kernel::transient().expect("failed to create kernel");
2982
2983        kernel.execute("set -e").await.expect("set -e failed");
2984
2985        // Pipeline failure should trigger exit
2986        kernel
2987            .execute(r#"
2988                BEFORE="yes"
2989                false | cat
2990                AFTER="yes"
2991            "#)
2992            .await
2993            .ok();
2994
2995        let before = kernel.get_var("BEFORE").await;
2996        assert_eq!(before, Some(Value::String("yes".into())));
2997
2998        // AFTER should not be set if pipeline failure triggers exit
2999        // Note: The exit code of a pipeline is the exit code of the last command
3000        // So `false | cat` returns 0 (cat succeeds). This is bash-compatible behavior.
3001        // To test pipeline failure, we need the last command to fail.
3002    }
3003
3004    #[tokio::test]
3005    async fn test_set_e_with_and_chain() {
3006        let kernel = Kernel::transient().expect("failed to create kernel");
3007
3008        kernel.execute("set -e").await.expect("set -e failed");
3009
3010        // Commands in && chain should not trigger -e on the first failure
3011        // because && explicitly handles the error
3012        kernel
3013            .execute(r#"
3014                RESULT="initial"
3015                false && RESULT="chained"
3016                RESULT="continued"
3017            "#)
3018            .await
3019            .ok();
3020
3021        // In bash, commands in && don't trigger -e. The chain handles the failure.
3022        // Our implementation may differ - let's verify current behavior.
3023        let result = kernel.get_var("RESULT").await;
3024        // If we follow bash semantics, RESULT should be "continued"
3025        // If we trigger -e on the false, RESULT stays "initial"
3026        assert!(result.is_some(), "RESULT should be set");
3027    }
3028
3029    // ═══════════════════════════════════════════════════════════════════════════
3030    // Source Tests
3031    // ═══════════════════════════════════════════════════════════════════════════
3032
3033    #[tokio::test]
3034    async fn test_source_sets_variables() {
3035        let kernel = Kernel::transient().expect("failed to create kernel");
3036
3037        // Write a script to the VFS
3038        kernel
3039            .execute(r#"write "/test.kai" 'FOO="bar"'"#)
3040            .await
3041            .expect("write failed");
3042
3043        // Source the script
3044        let result = kernel
3045            .execute(r#"source "/test.kai""#)
3046            .await
3047            .expect("source failed");
3048
3049        assert!(result.ok(), "source should succeed");
3050
3051        // Variable should be set in current scope
3052        let foo = kernel.get_var("FOO").await;
3053        assert_eq!(foo, Some(Value::String("bar".into())));
3054    }
3055
3056    #[tokio::test]
3057    async fn test_source_with_dot_alias() {
3058        let kernel = Kernel::transient().expect("failed to create kernel");
3059
3060        // Write a script to the VFS
3061        kernel
3062            .execute(r#"write "/vars.kai" 'X=42'"#)
3063            .await
3064            .expect("write failed");
3065
3066        // Source using . alias
3067        let result = kernel
3068            .execute(r#". "/vars.kai""#)
3069            .await
3070            .expect(". failed");
3071
3072        assert!(result.ok(), ". should succeed");
3073
3074        // Variable should be set in current scope
3075        let x = kernel.get_var("X").await;
3076        assert_eq!(x, Some(Value::Int(42)));
3077    }
3078
3079    #[tokio::test]
3080    async fn test_source_not_found() {
3081        let kernel = Kernel::transient().expect("failed to create kernel");
3082
3083        // Try to source a non-existent file
3084        let result = kernel
3085            .execute(r#"source "/nonexistent.kai""#)
3086            .await
3087            .expect("source should not fail with error");
3088
3089        assert!(!result.ok(), "source of non-existent file should fail");
3090        assert!(result.err.contains("nonexistent.kai"), "error should mention filename");
3091    }
3092
3093    #[tokio::test]
3094    async fn test_source_missing_filename() {
3095        let kernel = Kernel::transient().expect("failed to create kernel");
3096
3097        // Call source with no arguments
3098        let result = kernel
3099            .execute("source")
3100            .await
3101            .expect("source should not fail with error");
3102
3103        assert!(!result.ok(), "source without filename should fail");
3104        assert!(result.err.contains("missing filename"), "error should mention missing filename");
3105    }
3106
3107    #[tokio::test]
3108    async fn test_source_executes_multiple_statements() {
3109        let kernel = Kernel::transient().expect("failed to create kernel");
3110
3111        // Write a script with multiple statements
3112        kernel
3113            .execute(r#"write "/multi.kai" 'A=1
3114B=2
3115C=3'"#)
3116            .await
3117            .expect("write failed");
3118
3119        // Source it
3120        kernel
3121            .execute(r#"source "/multi.kai""#)
3122            .await
3123            .expect("source failed");
3124
3125        // All variables should be set
3126        assert_eq!(kernel.get_var("A").await, Some(Value::Int(1)));
3127        assert_eq!(kernel.get_var("B").await, Some(Value::Int(2)));
3128        assert_eq!(kernel.get_var("C").await, Some(Value::Int(3)));
3129    }
3130
3131    #[tokio::test]
3132    async fn test_source_can_define_functions() {
3133        let kernel = Kernel::transient().expect("failed to create kernel");
3134
3135        // Write a script that defines a function
3136        kernel
3137            .execute(r#"write "/functions.kai" 'greet() {
3138    echo "Hello, $1!"
3139}'"#)
3140            .await
3141            .expect("write failed");
3142
3143        // Source it
3144        kernel
3145            .execute(r#"source "/functions.kai""#)
3146            .await
3147            .expect("source failed");
3148
3149        // Use the defined function
3150        let result = kernel
3151            .execute(r#"greet "World""#)
3152            .await
3153            .expect("greet failed");
3154
3155        assert!(result.ok());
3156        assert!(result.out.contains("Hello, World!"));
3157    }
3158
3159    #[tokio::test]
3160    async fn test_source_inherits_error_exit() {
3161        let kernel = Kernel::transient().expect("failed to create kernel");
3162
3163        // Enable error exit
3164        kernel.execute("set -e").await.expect("set -e failed");
3165
3166        // Write a script that has a failure
3167        kernel
3168            .execute(r#"write "/fail.kai" 'BEFORE="yes"
3169false
3170AFTER="yes"'"#)
3171            .await
3172            .expect("write failed");
3173
3174        // Source it (should exit on false due to set -e)
3175        kernel
3176            .execute(r#"source "/fail.kai""#)
3177            .await
3178            .ok();
3179
3180        // BEFORE should be set, AFTER should NOT be set due to error exit
3181        let before = kernel.get_var("BEFORE").await;
3182        assert_eq!(before, Some(Value::String("yes".into())));
3183
3184        // Note: This test depends on whether error exit is checked within source
3185        // Currently our implementation checks per-statement in the main kernel
3186    }
3187
3188    // ═══════════════════════════════════════════════════════════════════════════
3189    // Case Statement Tests
3190    // ═══════════════════════════════════════════════════════════════════════════
3191
3192    #[tokio::test]
3193    async fn test_case_simple_match() {
3194        let kernel = Kernel::transient().expect("failed to create kernel");
3195
3196        let result = kernel
3197            .execute(r#"
3198                case "hello" in
3199                    hello) echo "matched hello" ;;
3200                    world) echo "matched world" ;;
3201                esac
3202            "#)
3203            .await
3204            .expect("case failed");
3205
3206        assert!(result.ok());
3207        assert_eq!(result.out.trim(), "matched hello");
3208    }
3209
3210    #[tokio::test]
3211    async fn test_case_wildcard_match() {
3212        let kernel = Kernel::transient().expect("failed to create kernel");
3213
3214        let result = kernel
3215            .execute(r#"
3216                case "main.rs" in
3217                    "*.py") echo "Python" ;;
3218                    "*.rs") echo "Rust" ;;
3219                    "*") echo "Unknown" ;;
3220                esac
3221            "#)
3222            .await
3223            .expect("case failed");
3224
3225        assert!(result.ok());
3226        assert_eq!(result.out.trim(), "Rust");
3227    }
3228
3229    #[tokio::test]
3230    async fn test_case_default_match() {
3231        let kernel = Kernel::transient().expect("failed to create kernel");
3232
3233        let result = kernel
3234            .execute(r#"
3235                case "unknown.xyz" in
3236                    "*.py") echo "Python" ;;
3237                    "*.rs") echo "Rust" ;;
3238                    "*") echo "Default" ;;
3239                esac
3240            "#)
3241            .await
3242            .expect("case failed");
3243
3244        assert!(result.ok());
3245        assert_eq!(result.out.trim(), "Default");
3246    }
3247
3248    #[tokio::test]
3249    async fn test_case_no_match() {
3250        let kernel = Kernel::transient().expect("failed to create kernel");
3251
3252        // Case with no default branch and no match
3253        let result = kernel
3254            .execute(r#"
3255                case "nope" in
3256                    "yes") echo "yes" ;;
3257                    "no") echo "no" ;;
3258                esac
3259            "#)
3260            .await
3261            .expect("case failed");
3262
3263        assert!(result.ok());
3264        assert!(result.out.is_empty(), "no match should produce empty output");
3265    }
3266
3267    #[tokio::test]
3268    async fn test_case_with_variable() {
3269        let kernel = Kernel::transient().expect("failed to create kernel");
3270
3271        kernel.execute(r#"LANG="rust""#).await.expect("set failed");
3272
3273        let result = kernel
3274            .execute(r#"
3275                case ${LANG} in
3276                    python) echo "snake" ;;
3277                    rust) echo "crab" ;;
3278                    go) echo "gopher" ;;
3279                esac
3280            "#)
3281            .await
3282            .expect("case failed");
3283
3284        assert!(result.ok());
3285        assert_eq!(result.out.trim(), "crab");
3286    }
3287
3288    #[tokio::test]
3289    async fn test_case_multiple_patterns() {
3290        let kernel = Kernel::transient().expect("failed to create kernel");
3291
3292        let result = kernel
3293            .execute(r#"
3294                case "yes" in
3295                    "y"|"yes"|"Y"|"YES") echo "affirmative" ;;
3296                    "n"|"no"|"N"|"NO") echo "negative" ;;
3297                esac
3298            "#)
3299            .await
3300            .expect("case failed");
3301
3302        assert!(result.ok());
3303        assert_eq!(result.out.trim(), "affirmative");
3304    }
3305
3306    #[tokio::test]
3307    async fn test_case_glob_question_mark() {
3308        let kernel = Kernel::transient().expect("failed to create kernel");
3309
3310        let result = kernel
3311            .execute(r#"
3312                case "test1" in
3313                    "test?") echo "matched test?" ;;
3314                    "*") echo "default" ;;
3315                esac
3316            "#)
3317            .await
3318            .expect("case failed");
3319
3320        assert!(result.ok());
3321        assert_eq!(result.out.trim(), "matched test?");
3322    }
3323
3324    #[tokio::test]
3325    async fn test_case_char_class() {
3326        let kernel = Kernel::transient().expect("failed to create kernel");
3327
3328        let result = kernel
3329            .execute(r#"
3330                case "Yes" in
3331                    "[Yy]*") echo "yes-like" ;;
3332                    "[Nn]*") echo "no-like" ;;
3333                esac
3334            "#)
3335            .await
3336            .expect("case failed");
3337
3338        assert!(result.ok());
3339        assert_eq!(result.out.trim(), "yes-like");
3340    }
3341
3342    // ═══════════════════════════════════════════════════════════════════════════
3343    // Cat Stdin Tests
3344    // ═══════════════════════════════════════════════════════════════════════════
3345
3346    #[tokio::test]
3347    async fn test_cat_from_pipeline() {
3348        let kernel = Kernel::transient().expect("failed to create kernel");
3349
3350        let result = kernel
3351            .execute(r#"echo "piped text" | cat"#)
3352            .await
3353            .expect("cat pipeline failed");
3354
3355        assert!(result.ok(), "cat failed: {}", result.err);
3356        assert_eq!(result.out.trim(), "piped text");
3357    }
3358
3359    #[tokio::test]
3360    async fn test_cat_from_pipeline_multiline() {
3361        let kernel = Kernel::transient().expect("failed to create kernel");
3362
3363        let result = kernel
3364            .execute(r#"echo "line1\nline2" | cat -n"#)
3365            .await
3366            .expect("cat pipeline failed");
3367
3368        assert!(result.ok(), "cat failed: {}", result.err);
3369        assert!(result.out.contains("1\t"), "output: {}", result.out);
3370    }
3371
3372    // ═══════════════════════════════════════════════════════════════════════════
3373    // Heredoc Tests
3374    // ═══════════════════════════════════════════════════════════════════════════
3375
3376    #[tokio::test]
3377    async fn test_heredoc_basic() {
3378        let kernel = Kernel::transient().expect("failed to create kernel");
3379
3380        let result = kernel
3381            .execute("cat <<EOF\nhello\nEOF")
3382            .await
3383            .expect("heredoc failed");
3384
3385        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
3386        assert_eq!(result.out.trim(), "hello");
3387    }
3388
3389    #[tokio::test]
3390    async fn test_arithmetic_in_string() {
3391        let kernel = Kernel::transient().expect("failed to create kernel");
3392
3393        let result = kernel
3394            .execute(r#"echo "result: $((1 + 2))""#)
3395            .await
3396            .expect("arithmetic in string failed");
3397
3398        assert!(result.ok(), "echo failed: {}", result.err);
3399        assert_eq!(result.out.trim(), "result: 3");
3400    }
3401
3402    #[tokio::test]
3403    async fn test_heredoc_multiline() {
3404        let kernel = Kernel::transient().expect("failed to create kernel");
3405
3406        let result = kernel
3407            .execute("cat <<EOF\nline1\nline2\nline3\nEOF")
3408            .await
3409            .expect("heredoc failed");
3410
3411        assert!(result.ok(), "cat with heredoc failed: {}", result.err);
3412        assert!(result.out.contains("line1"), "output: {}", result.out);
3413        assert!(result.out.contains("line2"), "output: {}", result.out);
3414        assert!(result.out.contains("line3"), "output: {}", result.out);
3415    }
3416
3417    // ═══════════════════════════════════════════════════════════════════════════
3418    // Read Builtin Tests
3419    // ═══════════════════════════════════════════════════════════════════════════
3420
3421    #[tokio::test]
3422    async fn test_read_from_pipeline() {
3423        let kernel = Kernel::transient().expect("failed to create kernel");
3424
3425        // Pipe input to read
3426        let result = kernel
3427            .execute(r#"echo "Alice" | read NAME; echo "Hello, ${NAME}""#)
3428            .await
3429            .expect("read pipeline failed");
3430
3431        assert!(result.ok(), "read failed: {}", result.err);
3432        assert!(result.out.contains("Hello, Alice"), "output: {}", result.out);
3433    }
3434
3435    #[tokio::test]
3436    async fn test_read_multiple_vars_from_pipeline() {
3437        let kernel = Kernel::transient().expect("failed to create kernel");
3438
3439        let result = kernel
3440            .execute(r#"echo "John Doe 42" | read FIRST LAST AGE; echo "${FIRST} is ${AGE}""#)
3441            .await
3442            .expect("read pipeline failed");
3443
3444        assert!(result.ok(), "read failed: {}", result.err);
3445        assert!(result.out.contains("John is 42"), "output: {}", result.out);
3446    }
3447
3448    // ═══════════════════════════════════════════════════════════════════════════
3449    // Shell-Style Function Tests
3450    // ═══════════════════════════════════════════════════════════════════════════
3451
3452    #[tokio::test]
3453    async fn test_posix_function_with_positional_params() {
3454        let kernel = Kernel::transient().expect("failed to create kernel");
3455
3456        // Define POSIX-style function
3457        kernel
3458            .execute(r#"greet() { echo "Hello, $1!" }"#)
3459            .await
3460            .expect("function definition failed");
3461
3462        // Call the function
3463        let result = kernel
3464            .execute(r#"greet "Amy""#)
3465            .await
3466            .expect("function call failed");
3467
3468        assert!(result.ok(), "greet failed: {}", result.err);
3469        assert_eq!(result.out.trim(), "Hello, Amy!");
3470    }
3471
3472    #[tokio::test]
3473    async fn test_posix_function_multiple_args() {
3474        let kernel = Kernel::transient().expect("failed to create kernel");
3475
3476        // Define function using $1 and $2
3477        kernel
3478            .execute(r#"add_greeting() { echo "$1 $2!" }"#)
3479            .await
3480            .expect("function definition failed");
3481
3482        // Call the function
3483        let result = kernel
3484            .execute(r#"add_greeting "Hello" "World""#)
3485            .await
3486            .expect("function call failed");
3487
3488        assert!(result.ok(), "function failed: {}", result.err);
3489        assert_eq!(result.out.trim(), "Hello World!");
3490    }
3491
3492    #[tokio::test]
3493    async fn test_bash_function_with_positional_params() {
3494        let kernel = Kernel::transient().expect("failed to create kernel");
3495
3496        // Define bash-style function (function keyword, no parens)
3497        kernel
3498            .execute(r#"function greet { echo "Hi $1" }"#)
3499            .await
3500            .expect("function definition failed");
3501
3502        // Call the function
3503        let result = kernel
3504            .execute(r#"greet "Bob""#)
3505            .await
3506            .expect("function call failed");
3507
3508        assert!(result.ok(), "greet failed: {}", result.err);
3509        assert_eq!(result.out.trim(), "Hi Bob");
3510    }
3511
3512    #[tokio::test]
3513    async fn test_shell_function_with_all_args() {
3514        let kernel = Kernel::transient().expect("failed to create kernel");
3515
3516        // Define function using $@ (all args)
3517        kernel
3518            .execute(r#"echo_all() { echo "args: $@" }"#)
3519            .await
3520            .expect("function definition failed");
3521
3522        // Call with multiple args
3523        let result = kernel
3524            .execute(r#"echo_all "a" "b" "c""#)
3525            .await
3526            .expect("function call failed");
3527
3528        assert!(result.ok(), "function failed: {}", result.err);
3529        assert_eq!(result.out.trim(), "args: a b c");
3530    }
3531
3532    #[tokio::test]
3533    async fn test_shell_function_with_arg_count() {
3534        let kernel = Kernel::transient().expect("failed to create kernel");
3535
3536        // Define function using $# (arg count)
3537        kernel
3538            .execute(r#"count_args() { echo "count: $#" }"#)
3539            .await
3540            .expect("function definition failed");
3541
3542        // Call with three args
3543        let result = kernel
3544            .execute(r#"count_args "x" "y" "z""#)
3545            .await
3546            .expect("function call failed");
3547
3548        assert!(result.ok(), "function failed: {}", result.err);
3549        assert_eq!(result.out.trim(), "count: 3");
3550    }
3551
3552    #[tokio::test]
3553    async fn test_shell_function_shared_scope() {
3554        let kernel = Kernel::transient().expect("failed to create kernel");
3555
3556        // Set a variable in parent scope
3557        kernel
3558            .execute(r#"PARENT_VAR="visible""#)
3559            .await
3560            .expect("set failed");
3561
3562        // Define shell function that reads and writes parent variable
3563        kernel
3564            .execute(r#"modify_parent() {
3565                echo "saw: ${PARENT_VAR}"
3566                PARENT_VAR="changed by function"
3567            }"#)
3568            .await
3569            .expect("function definition failed");
3570
3571        // Call the function - it SHOULD see PARENT_VAR (bash-compatible shared scope)
3572        let result = kernel.execute("modify_parent").await.expect("function failed");
3573
3574        assert!(
3575            result.out.contains("visible"),
3576            "Shell function should access parent scope, got: {}",
3577            result.out
3578        );
3579
3580        // Parent variable should be modified
3581        let var = kernel.get_var("PARENT_VAR").await;
3582        assert_eq!(
3583            var,
3584            Some(Value::String("changed by function".into())),
3585            "Shell function should modify parent scope"
3586        );
3587    }
3588
3589    // ═══════════════════════════════════════════════════════════════════════════
3590    // Script Execution via PATH Tests
3591    // ═══════════════════════════════════════════════════════════════════════════
3592
3593    #[tokio::test]
3594    async fn test_script_execution_from_path() {
3595        let kernel = Kernel::transient().expect("failed to create kernel");
3596
3597        // Create /bin directory and script
3598        kernel.execute(r#"mkdir "/bin""#).await.ok();
3599        kernel
3600            .execute(r#"write "/bin/hello.kai" 'echo "Hello from script!"'"#)
3601            .await
3602            .expect("write script failed");
3603
3604        // Set PATH to /bin
3605        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
3606
3607        // Call script by name (without .kai extension)
3608        let result = kernel
3609            .execute("hello")
3610            .await
3611            .expect("script execution failed");
3612
3613        assert!(result.ok(), "script failed: {}", result.err);
3614        assert_eq!(result.out.trim(), "Hello from script!");
3615    }
3616
3617    #[tokio::test]
3618    async fn test_script_with_args() {
3619        let kernel = Kernel::transient().expect("failed to create kernel");
3620
3621        // Create script that uses positional params
3622        kernel.execute(r#"mkdir "/bin""#).await.ok();
3623        kernel
3624            .execute(r#"write "/bin/greet.kai" 'echo "Hello, $1!"'"#)
3625            .await
3626            .expect("write script failed");
3627
3628        // Set PATH
3629        kernel.execute(r#"PATH="/bin""#).await.expect("set PATH failed");
3630
3631        // Call script with arg
3632        let result = kernel
3633            .execute(r#"greet "World""#)
3634            .await
3635            .expect("script execution failed");
3636
3637        assert!(result.ok(), "script failed: {}", result.err);
3638        assert_eq!(result.out.trim(), "Hello, World!");
3639    }
3640
3641    #[tokio::test]
3642    async fn test_script_not_found() {
3643        let kernel = Kernel::transient().expect("failed to create kernel");
3644
3645        // Set empty PATH
3646        kernel.execute(r#"PATH="/nonexistent""#).await.expect("set PATH failed");
3647
3648        // Call non-existent script
3649        let result = kernel
3650            .execute("noscript")
3651            .await
3652            .expect("execution failed");
3653
3654        assert!(!result.ok(), "should fail with command not found");
3655        assert_eq!(result.code, 127);
3656        assert!(result.err.contains("command not found"));
3657    }
3658
3659    #[tokio::test]
3660    async fn test_script_path_search_order() {
3661        let kernel = Kernel::transient().expect("failed to create kernel");
3662
3663        // Create two directories with same-named script
3664        // Note: using "myscript" not "test" to avoid conflict with test builtin
3665        kernel.execute(r#"mkdir "/first""#).await.ok();
3666        kernel.execute(r#"mkdir "/second""#).await.ok();
3667        kernel
3668            .execute(r#"write "/first/myscript.kai" 'echo "from first"'"#)
3669            .await
3670            .expect("write failed");
3671        kernel
3672            .execute(r#"write "/second/myscript.kai" 'echo "from second"'"#)
3673            .await
3674            .expect("write failed");
3675
3676        // Set PATH with first before second
3677        kernel.execute(r#"PATH="/first:/second""#).await.expect("set PATH failed");
3678
3679        // Should find first one
3680        let result = kernel
3681            .execute("myscript")
3682            .await
3683            .expect("script execution failed");
3684
3685        assert!(result.ok(), "script failed: {}", result.err);
3686        assert_eq!(result.out.trim(), "from first");
3687    }
3688
3689    // ═══════════════════════════════════════════════════════════════════════════
3690    // Special Variable Tests ($?, $$, unset vars)
3691    // ═══════════════════════════════════════════════════════════════════════════
3692
3693    #[tokio::test]
3694    async fn test_last_exit_code_success() {
3695        let kernel = Kernel::transient().expect("failed to create kernel");
3696
3697        // true exits with 0
3698        let result = kernel.execute("true; echo $?").await.expect("execution failed");
3699        assert!(result.out.contains("0"), "expected 0, got: {}", result.out);
3700    }
3701
3702    #[tokio::test]
3703    async fn test_last_exit_code_failure() {
3704        let kernel = Kernel::transient().expect("failed to create kernel");
3705
3706        // false exits with 1
3707        let result = kernel.execute("false; echo $?").await.expect("execution failed");
3708        assert!(result.out.contains("1"), "expected 1, got: {}", result.out);
3709    }
3710
3711    #[tokio::test]
3712    async fn test_current_pid() {
3713        let kernel = Kernel::transient().expect("failed to create kernel");
3714
3715        let result = kernel.execute("echo $$").await.expect("execution failed");
3716        // PID should be a positive number
3717        let pid: u32 = result.out.trim().parse().expect("PID should be a number");
3718        assert!(pid > 0, "PID should be positive");
3719    }
3720
3721    #[tokio::test]
3722    async fn test_unset_variable_expands_to_empty() {
3723        let kernel = Kernel::transient().expect("failed to create kernel");
3724
3725        // Unset variable in interpolation should be empty
3726        let result = kernel.execute(r#"echo "prefix:${UNSET_VAR}:suffix""#).await.expect("execution failed");
3727        assert_eq!(result.out.trim(), "prefix::suffix");
3728    }
3729
3730    #[tokio::test]
3731    async fn test_eq_ne_operators() {
3732        let kernel = Kernel::transient().expect("failed to create kernel");
3733
3734        // Test -eq operator
3735        let result = kernel.execute(r#"if [[ 5 -eq 5 ]]; then echo "eq works"; fi"#).await.expect("execution failed");
3736        assert_eq!(result.out.trim(), "eq works");
3737
3738        // Test -ne operator
3739        let result = kernel.execute(r#"if [[ 5 -ne 3 ]]; then echo "ne works"; fi"#).await.expect("execution failed");
3740        assert_eq!(result.out.trim(), "ne works");
3741
3742        // Test -eq with different values
3743        let result = kernel.execute(r#"if [[ 5 -eq 3 ]]; then echo "wrong"; else echo "correct"; fi"#).await.expect("execution failed");
3744        assert_eq!(result.out.trim(), "correct");
3745    }
3746
3747    #[tokio::test]
3748    async fn test_escaped_dollar_in_string() {
3749        let kernel = Kernel::transient().expect("failed to create kernel");
3750
3751        // \$ should produce literal $
3752        let result = kernel.execute(r#"echo "\$100""#).await.expect("execution failed");
3753        assert_eq!(result.out.trim(), "$100");
3754    }
3755
3756    #[tokio::test]
3757    async fn test_special_vars_in_interpolation() {
3758        let kernel = Kernel::transient().expect("failed to create kernel");
3759
3760        // Test $? in string interpolation
3761        let result = kernel.execute(r#"true; echo "exit: $?""#).await.expect("execution failed");
3762        assert_eq!(result.out.trim(), "exit: 0");
3763
3764        // Test $$ in string interpolation
3765        let result = kernel.execute(r#"echo "pid: $$""#).await.expect("execution failed");
3766        assert!(result.out.starts_with("pid: "), "unexpected output: {}", result.out);
3767        let pid_part = result.out.trim().strip_prefix("pid: ").unwrap();
3768        let _pid: u32 = pid_part.parse().expect("PID in string should be a number");
3769    }
3770
3771    // ═══════════════════════════════════════════════════════════════════════════
3772    // Command Substitution Tests
3773    // ═══════════════════════════════════════════════════════════════════════════
3774
3775    #[tokio::test]
3776    async fn test_command_subst_assignment() {
3777        let kernel = Kernel::transient().expect("failed to create kernel");
3778
3779        // Command substitution in assignment
3780        let result = kernel.execute(r#"X=$(echo hello); echo "$X""#).await.expect("execution failed");
3781        assert_eq!(result.out.trim(), "hello");
3782    }
3783
3784    #[tokio::test]
3785    async fn test_command_subst_with_args() {
3786        let kernel = Kernel::transient().expect("failed to create kernel");
3787
3788        // Command substitution with string argument
3789        let result = kernel.execute(r#"X=$(echo "a b c"); echo "$X""#).await.expect("execution failed");
3790        assert_eq!(result.out.trim(), "a b c");
3791    }
3792
3793    #[tokio::test]
3794    async fn test_command_subst_nested_vars() {
3795        let kernel = Kernel::transient().expect("failed to create kernel");
3796
3797        // Variables inside command substitution
3798        let result = kernel.execute(r#"Y=world; X=$(echo "hello $Y"); echo "$X""#).await.expect("execution failed");
3799        assert_eq!(result.out.trim(), "hello world");
3800    }
3801
3802    #[tokio::test]
3803    async fn test_background_job_basic() {
3804        use std::time::Duration;
3805
3806        let kernel = Kernel::new(KernelConfig::isolated()).expect("failed to create kernel");
3807
3808        // Run a simple background command
3809        let result = kernel.execute("echo hello &").await.expect("execution failed");
3810        assert!(result.ok(), "background command should succeed: {}", result.err);
3811        assert!(result.out.contains("[1]"), "should return job ID: {}", result.out);
3812
3813        // Give the job time to complete
3814        tokio::time::sleep(Duration::from_millis(100)).await;
3815
3816        // Check job status
3817        let status = kernel.execute("cat /v/jobs/1/status").await.expect("status check failed");
3818        assert!(status.ok(), "status should succeed: {}", status.err);
3819        assert!(
3820            status.out.contains("done:") || status.out.contains("running"),
3821            "should have valid status: {}",
3822            status.out
3823        );
3824
3825        // Check stdout
3826        let stdout = kernel.execute("cat /v/jobs/1/stdout").await.expect("stdout check failed");
3827        assert!(stdout.ok());
3828        assert!(stdout.out.contains("hello"));
3829    }
3830
3831}