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