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