From f72b5793a5c7e7891e4904be73c0bc6bc038fa18 Mon Sep 17 00:00:00 2001 From: T Date: Wed, 27 May 2026 16:34:32 -0600 Subject: system agent definition scaffolding --- agent/README.md | 28 ++++++++ build.zig | 42 ++++++++++++ build/gen_agent_embed.zig | 126 +++++++++++++++++++++++++++++++++++ src/extension_loader.zig | 165 +++++++++++++++++++++++++++++++++++++++++++--- src/luarocks_runtime.zig | 37 +++++++++++ src/main.zig | 10 +-- src/panto_home.zig | 11 ++++ 7 files changed, 406 insertions(+), 13 deletions(-) create mode 100644 agent/README.md create mode 100644 build/gen_agent_embed.zig diff --git a/agent/README.md b/agent/README.md new file mode 100644 index 0000000..00e6e60 --- /dev/null +++ b/agent/README.md @@ -0,0 +1,28 @@ +# panto system agent tree + +Contents of this directory are embedded into the `panto` binary at +build time and staged onto disk at bootstrap into +`$PANTO_HOME/agent/` (i.e. `$XDG_DATA_HOME/panto/agent/`, or +`~/.local/share/panto/agent/` if `XDG_DATA_HOME` is unset). + +The staged tree is the "system" layer of panto's extension/tool +search path — sitting underneath the user layer +(`$XDG_CONFIG_HOME/panto/`) and the project layer +(`./.panto/`). Project shadows user shadows system. + +Files are written with `writeIfDifferent` semantics: on each +bootstrap, embedded contents are compared against what's on disk and +only rewritten when they differ. This keeps mtimes stable across +reruns. + +The staged tree is **panto's territory** — local edits will be +overwritten on the next bootstrap. To customize a system tool, +override it at the user or project layer (a file with the same +basename under `$XDG_CONFIG_HOME/panto/tools/` or `./.panto/tools/` +shadows the system copy). + +## Layout + + agent/ + tools/ single-file tools, one per `.lua` + extensions/ (future) multi-tool extensions diff --git a/build.zig b/build.zig index 4e4e862..bcfe1be 100644 --- a/build.zig +++ b/build.zig @@ -41,6 +41,11 @@ pub fn build(b: *std.Build) void { // for luarocks to find when compiling C rocks. const lua_headers_embed_path = generateLuaHeadersEmbed(b, lua_src); + // And the in-repo `agent/` tree: bundled into the binary and + // staged at $PANTO_HOME/agent/ on first run. This is panto's + // "system" extension/tool layer (read/write/edit/bash etc.). + const agent_embed_path = generateAgentEmbed(b); + // Constants module: makes Zig know the Lua and luarocks versions // without duplicating literals everywhere. const versions_mod = b.addOptions(); @@ -64,6 +69,9 @@ pub fn build(b: *std.Build) void { exe_mod.addImport("embedded_lua_headers", b.createModule(.{ .root_source_file = lua_headers_embed_path, })); + exe_mod.addImport("embedded_agent", b.createModule(.{ + .root_source_file = agent_embed_path, + })); // Link Lua. We can't use a static library here because the // standard archive-linking behavior drops object files whose // symbols nothing in our code references — e.g. `lua_settable`, @@ -121,6 +129,9 @@ pub fn build(b: *std.Build) void { test_mod.addImport("embedded_lua_headers", b.createModule(.{ .root_source_file = lua_headers_embed_path, })); + test_mod.addImport("embedded_agent", b.createModule(.{ + .root_source_file = agent_embed_path, + })); addLuaSources(test_mod, lua_src, lua_anchor_path); test_mod.addIncludePath(lua_src.path("src")); test_mod.linkLibrary(lua_repl); @@ -300,6 +311,37 @@ fn generateLuarocksEmbed( return out_zig; } +/// Codegen step: walk the in-repo `agent/` tree and emit a Zig module +/// embedding every file. Bootstrap stages the result under +/// `$PANTO_HOME/agent/` on first run, where the runtime's extension +/// loader finds it as the "system" layer (below user and project). +/// +/// Mirrors `generateLuarocksEmbed`'s addCopyDirectory trick so the +/// emitted `@embedFile("agent_src/")` references resolve relative +/// to the generated zig file. +fn generateAgentEmbed(b: *std.Build) std.Build.LazyPath { + const tool = b.addExecutable(.{ + .name = "gen-agent-embed", + .root_module = b.createModule(.{ + .root_source_file = b.path("build/gen_agent_embed.zig"), + .target = b.graph.host, + .optimize = .Debug, + }), + }); + + const run = b.addRunArtifact(tool); + // Marker subpath: each emitted `@embedFile` reference is + // `agent_src/`, and we stage the tree under that name. + run.addArg("agent_src"); + run.addDirectoryArg(b.path("agent")); + const gen_file = run.addOutputFileArg("embedded_agent.zig"); + + const wf = b.addWriteFiles(); + const out_zig = wf.addCopyFile(gen_file, "embedded_agent.zig"); + _ = wf.addCopyDirectory(b.path("agent"), "agent_src", .{}); + return out_zig; +} + /// Codegen step: emit a Zig module that exposes every Lua public header /// from the Lua source tarball as embedded bytes. The bootstrap stages /// these under `$PANTO_HOME/rocks/lua-X.Y.Z/include/` so luarocks can diff --git a/build/gen_agent_embed.zig b/build/gen_agent_embed.zig new file mode 100644 index 0000000..b705863 --- /dev/null +++ b/build/gen_agent_embed.zig @@ -0,0 +1,126 @@ +//! Build-time codegen: walk the in-repo `agent/` tree and emit a Zig +//! module that exposes every file as an `@embedFile` entry. +//! +//! Invoked from `build.zig`: +//! +//! gen-agent-embed +//! +//! `` is the repo's `agent/` directory. `` is +//! the subpath the generated zig file's neighboring `addCopyDirectory` +//! stages the tree under; every `@embedFile` reference uses this +//! prefix so Zig can resolve it relative to the generated module's +//! directory. +//! +//! The generated file looks like: +//! +//! pub const Entry = struct { +//! /// Path relative to the agent tree root (e.g. "tools/read.lua"). +//! path: []const u8, +//! /// File contents. +//! contents: []const u8, +//! }; +//! pub const files: []const Entry = &.{ +//! .{ .path = "tools/read.lua", .contents = @embedFile("...") }, +//! ... +//! }; +//! +//! Bootstrap walks `files` and stages each entry at +//! `$PANTO_HOME/agent/` using the same `writeIfDifferent` +//! discipline as `stageLuaHeaders`. + +const std = @import("std"); + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + var args = init.minimal.args.iterate(); + defer args.deinit(); + _ = args.next(); // skip program name + const embed_prefix = args.next() orelse return error.MissingEmbedPrefix; + const src_dir_arg = args.next() orelse return error.MissingSrcDir; + const out_path_arg = args.next() orelse return error.MissingOutPath; + + var entries: std.array_list.Managed(Entry) = .init(arena); + try collect(arena, io, src_dir_arg, embed_prefix, &entries); + + // Sort for deterministic output across builds. + std.mem.sort(Entry, entries.items, {}, Entry.lessThan); + + var out_file = try std.Io.Dir.cwd().createFile(io, out_path_arg, .{}); + defer out_file.close(io); + var buf: [4096]u8 = undefined; + var writer = out_file.writer(io, &buf); + const w = &writer.interface; + + try w.writeAll( + \\//! Auto-generated. Do not edit. See build/gen_agent_embed.zig. + \\ + \\pub const Entry = struct { + \\ /// Path relative to the agent tree root (e.g. "tools/read.lua"). + \\ path: []const u8, + \\ /// File contents (raw bytes). + \\ contents: []const u8, + \\}; + \\ + \\pub const files: []const Entry = &.{ + \\ + ); + + for (entries.items) |e| { + try w.print( + " .{{ .path = \"{s}\", .contents = @embedFile(\"{s}\") }},\n", + .{ e.rel_path, e.embed_path }, + ); + } + + try w.writeAll("};\n"); + try w.flush(); +} + +const Entry = struct { + /// Path inside the agent tree, forward-slash separated. + rel_path: []const u8, + /// Path the generated `@embedFile` references (under embed prefix). + embed_path: []const u8, + + fn lessThan(_: void, a: Entry, b: Entry) bool { + return std.mem.lessThan(u8, a.rel_path, b.rel_path); + } +}; + +/// Walk `` recursively, collecting every file. Dotfiles +/// (entries whose basename starts with `.`) are skipped at every +/// level so we don't accidentally embed editor backups, `.DS_Store`, +/// or VCS metadata. +fn collect( + arena: std.mem.Allocator, + io: std.Io, + root_abs: []const u8, + embed_prefix: []const u8, + out: *std.array_list.Managed(Entry), +) !void { + var dir = std.Io.Dir.cwd().openDir(io, root_abs, .{ .iterate = true }) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; + defer dir.close(io); + + var walker = try dir.walk(arena); + defer walker.deinit(); + + while (try walker.next(io)) |entry| { + if (entry.kind != .file) continue; + if (entry.basename.len == 0 or entry.basename[0] == '.') continue; + // Skip anything along the path that looks like a dotfile dir. + if (std.mem.indexOf(u8, entry.path, "/.") != null) continue; + + const rel_path = try arena.dupe(u8, entry.path); + const embed_path = try std.fs.path.join(arena, &.{ embed_prefix, entry.path }); + + try out.append(.{ + .rel_path = rel_path, + .embed_path = embed_path, + }); + } +} diff --git a/src/extension_loader.zig b/src/extension_loader.zig index 8a871c9..2fd86ae 100644 --- a/src/extension_loader.zig +++ b/src/extension_loader.zig @@ -1,17 +1,26 @@ //! Extension and tool discovery: walk well-known directories, locate Lua //! files, and load each into a long-lived `LuaRuntime`. //! -//! Two parallel namespaces are scanned, each at user and project scope: +//! Two parallel namespaces are scanned, each at three scopes — system, +//! user, and project. Project shadows user shadows system. //! //! Extensions (full-featured; call `panto.register_tool` from a script //! that may register many tools): -//! 1. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/extensions/` ("user") -//! 2. `./.panto/extensions/` ("project") +//! 1. `$PANTO_HOME/agent/extensions/` ("system") +//! 2. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/extensions/` ("user") +//! 3. `./.panto/extensions/` ("project") //! //! Tools (ergonomic single-tool form; the script returns one table //! shaped like the argument to `panto.register_tool`): -//! 1. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/tools/` ("user") -//! 2. `./.panto/tools/` ("project") +//! 1. `$PANTO_HOME/agent/tools/` ("system") +//! 2. `${XDG_CONFIG_HOME:-$HOME/.config}/panto/tools/` ("user") +//! 3. `./.panto/tools/` ("project") +//! +//! The `system` layer is populated at bootstrap from files embedded +//! into the panto binary (see `build/gen_agent_embed.zig` and +//! `luarocks_runtime.stageAgentTree`). It is panto's "batteries" tier: +//! ships in the binary, but every entry is individually shadowable +//! from the user or project layer. //! //! Layout per directory (identical for extensions and tools): //! - `.lua` -- single-file entry; the logical name is the @@ -24,10 +33,11 @@ //! Conflict rules: //! - Within one directory, two entries with the same logical name //! are an error. -//! - Project shadows user *within the same kind* (extension or tool). -//! Extensions and tools live in distinct namespaces for shadowing; -//! the registered *tool names* still share one global namespace -//! across both, and collisions there are still an error. +//! - Project shadows user shadows system *within the same kind* +//! (extension or tool). Extensions and tools live in distinct +//! namespaces for shadowing; the registered *tool names* still +//! share one global namespace across both, and collisions there +//! are still an error. //! //! Symlinks: followed normally. Dotfiles: skipped. @@ -60,11 +70,15 @@ const Found = struct { }; pub const Source = enum { + /// Staged at bootstrap from files embedded in the panto binary. + /// Lowest priority — shadowed by user and project. + system, user, project, pub fn label(self: Source) []const u8 { return switch (self) { + .system => "system", .user => "user", .project => "project", }; @@ -87,14 +101,31 @@ pub const Kind = enum { /// paths into `runtime`. Returns the number of registered tools added /// to the runtime by this call. /// +/// `system_agent_dir`, when non-null, is the path under which embedded +/// system tools/extensions have been staged — typically +/// `$PANTO_HOME/agent/`. Pass `null` to skip the system layer entirely +/// (mostly useful for tests). +/// /// `environ_map` is consulted for `HOME` and `XDG_CONFIG_HOME`. The /// project directories are always `cwd()/.panto/{extensions,tools}`. pub fn discoverAndLoad( allocator: Allocator, io: Io, environ_map: *const std.process.Environ.Map, + system_agent_dir: ?[]const u8, runtime: *LuaRuntime, ) !usize { + const sys_ext = if (system_agent_dir) |d| + try std.fs.path.join(allocator, &.{ d, kindSubdir(.extension) }) + else + null; + defer if (sys_ext) |d| allocator.free(d); + const sys_tool = if (system_agent_dir) |d| + try std.fs.path.join(allocator, &.{ d, kindSubdir(.tool) }) + else + null; + defer if (sys_tool) |d| allocator.free(d); + const user_ext = try userKindDir(allocator, environ_map, .extension); defer if (user_ext) |d| allocator.free(d); const user_tool = try userKindDir(allocator, environ_map, .tool); @@ -110,8 +141,10 @@ pub fn discoverAndLoad( io, runtime, .{ + .system_extensions = sys_ext, .user_extensions = user_ext, .project_extensions = project_ext, + .system_tools = sys_tool, .user_tools = user_tool, .project_tools = project_tool, }, @@ -120,9 +153,15 @@ pub fn discoverAndLoad( /// Set of search paths consumed by `loadFromDirs`. Any field may be /// null; missing directories on disk are silently skipped. +/// +/// Scan order is system → user → project. `applyShadowing` keeps the +/// *last* occurrence of each (kind, name), so project entries win, +/// then user, then system. pub const DirSet = struct { + system_extensions: ?[]const u8 = null, user_extensions: ?[]const u8 = null, project_extensions: ?[]const u8 = null, + system_tools: ?[]const u8 = null, user_tools: ?[]const u8 = null, project_tools: ?[]const u8 = null, }; @@ -141,8 +180,10 @@ pub fn loadFromDirs( found.deinit(); } + if (dirs.system_extensions) |d| try scanDir(allocator, io, d, .system, .extension, &found); if (dirs.user_extensions) |d| try scanDir(allocator, io, d, .user, .extension, &found); if (dirs.project_extensions) |d| try scanDir(allocator, io, d, .project, .extension, &found); + if (dirs.system_tools) |d| try scanDir(allocator, io, d, .system, .tool, &found); if (dirs.user_tools) |d| try scanDir(allocator, io, d, .user, .tool, &found); if (dirs.project_tools) |d| try scanDir(allocator, io, d, .project, .tool, &found); @@ -708,6 +749,112 @@ test "loadFromDirs: project tool shadows user tool of the same name" { try testing.expectEqualStrings("PROJECT", results[0].ok); } +test "loadFromDirs: project tool shadows user shadows system" { + var tmp = testing.tmpDir(.{ .iterate = true }); + defer tmp.cleanup(); + + try makeDir(tmp.dir, "system_tools"); + try makeDir(tmp.dir, "user_tools"); + try makeDir(tmp.dir, "project_tools"); + + try writeFile(tmp.dir, "system_tools/greet.lua", + \\return { + \\ name = "greet", description = "system version", + \\ schema = { type = "object" }, + \\ handler = function(input) return "SYSTEM" end, + \\} + ); + try writeFile(tmp.dir, "user_tools/greet.lua", + \\return { + \\ name = "greet", description = "user version", + \\ schema = { type = "object" }, + \\ handler = function(input) return "USER" end, + \\} + ); + try writeFile(tmp.dir, "project_tools/greet.lua", + \\return { + \\ name = "greet", description = "project version", + \\ schema = { type = "object" }, + \\ handler = function(input) return "PROJECT" end, + \\} + ); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const sys_len = try tmp.dir.realPathFile(testing.io, "system_tools", &path_buf); + const sys_path = try testing.allocator.dupe(u8, path_buf[0..sys_len]); + defer testing.allocator.free(sys_path); + const user_len = try tmp.dir.realPathFile(testing.io, "user_tools", &path_buf); + const user_path = try testing.allocator.dupe(u8, path_buf[0..user_len]); + defer testing.allocator.free(user_path); + const proj_len = try tmp.dir.realPathFile(testing.io, "project_tools", &path_buf); + const proj_path = try testing.allocator.dupe(u8, path_buf[0..proj_len]); + defer testing.allocator.free(proj_path); + + var rt = try LuaRuntime.create(testing.allocator); + defer rt.deinit(); + + const n_tools = try loadFromDirs(testing.allocator, testing.io, rt, .{ + .system_tools = sys_path, + .user_tools = user_path, + .project_tools = proj_path, + }); + try testing.expectEqual(@as(usize, 1), n_tools); + + var src = rt.toolSource(); + const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }}; + var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; + try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); + defer testing.allocator.free(results[0].ok); + try testing.expectEqualStrings("PROJECT", results[0].ok); +} + +test "loadFromDirs: user tool shadows system tool when no project entry" { + var tmp = testing.tmpDir(.{ .iterate = true }); + defer tmp.cleanup(); + + try makeDir(tmp.dir, "system_tools"); + try makeDir(tmp.dir, "user_tools"); + + try writeFile(tmp.dir, "system_tools/greet.lua", + \\return { + \\ name = "greet", description = "system", + \\ schema = { type = "object" }, + \\ handler = function(input) return "SYSTEM" end, + \\} + ); + try writeFile(tmp.dir, "user_tools/greet.lua", + \\return { + \\ name = "greet", description = "user", + \\ schema = { type = "object" }, + \\ handler = function(input) return "USER" end, + \\} + ); + + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const sys_len = try tmp.dir.realPathFile(testing.io, "system_tools", &path_buf); + const sys_path = try testing.allocator.dupe(u8, path_buf[0..sys_len]); + defer testing.allocator.free(sys_path); + const user_len = try tmp.dir.realPathFile(testing.io, "user_tools", &path_buf); + const user_path = try testing.allocator.dupe(u8, path_buf[0..user_len]); + defer testing.allocator.free(user_path); + + var rt = try LuaRuntime.create(testing.allocator); + defer rt.deinit(); + + const n_tools = try loadFromDirs(testing.allocator, testing.io, rt, .{ + .system_tools = sys_path, + .user_tools = user_path, + }); + try testing.expectEqual(@as(usize, 1), n_tools); + + var src = rt.toolSource(); + const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }}; + var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; + try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); + defer testing.allocator.free(results[0].ok); + try testing.expectEqualStrings("USER", results[0].ok); +} + test "loadFromDirs: extension and tool share a *file* name independently" { // The shadow-key is (kind, name) so an extension named `foo` and a // tool named `foo` coexist — as long as their *registered* tool names diff --git a/src/luarocks_runtime.zig b/src/luarocks_runtime.zig index b67a8ad..0114b64 100644 --- a/src/luarocks_runtime.zig +++ b/src/luarocks_runtime.zig @@ -37,6 +37,7 @@ const manifest = @import("manifest.zig"); const panto_home = @import("panto_home.zig"); const embedded_luarocks = @import("embedded_luarocks"); const embedded_lua_headers = @import("embedded_lua_headers"); +const embedded_agent = @import("embedded_agent"); const lua_bridge = @import("lua_bridge.zig"); const c = lua_bridge.c; @@ -135,6 +136,7 @@ pub fn bootstrap( try panto_home.ensureDirsExist(layout, io); try stageLuaHeaders(allocator, io, layout); + try stageAgentTree(allocator, io, layout); const lua_wrapper_path = try writeLuaWrapper(allocator, io, layout, panto_executable_path); defer allocator.free(lua_wrapper_path); try writeLuarocksConfig(allocator, io, layout, lua_wrapper_path); @@ -199,6 +201,41 @@ fn stageLuaHeaders( } } +// --------------------------------------------------------------------------- +// Stage the embedded `agent/` tree +// --------------------------------------------------------------------------- + +/// Materialize the binary-embedded `agent/` tree under +/// `$PANTO_HOME/agent/`. Each entry's `path` is relative to the agent +/// root and may include subdirectories (`tools/read.lua` etc.); the +/// parent directory is created lazily as we encounter files inside it. +/// +/// Uses the same `writeIfDifferent` discipline as `stageLuaHeaders`: +/// unchanged files keep their on-disk mtime so downstream consumers +/// (the extension loader's stat-cached searches, future-rocks builds +/// against staged headers) don't see spurious invalidations. +fn stageAgentTree( + allocator: Allocator, + io: Io, + layout: panto_home.Layout, +) !void { + var root = try Io.Dir.cwd().openDir(io, layout.agent_dir, .{}); + defer root.close(io); + + for (embedded_agent.files) |entry| { + // Ensure the file's parent directory exists. `path` is + // forward-slash separated (the generator normalizes that), + // so we can split off the dirname directly. + if (std.fs.path.dirname(entry.path)) |parent| { + root.createDirPath(io, parent) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + } + try writeIfDifferent(allocator, io, root, entry.path, entry.contents); + } +} + /// Write `contents` into `dir/name` only if the existing file differs. /// Creates the file if absent. fn writeIfDifferent( diff --git a/src/main.zig b/src/main.zig index 3bc24e3..f95812d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -399,14 +399,16 @@ pub fn main(init: std.process.Init) !void { // chance to register tools that might want to yield. try rt.installScheduler(); - // Discover Lua extensions from $XDG_CONFIG_HOME/panto/extensions (or - // $HOME/.config/panto/extensions) and ./.panto/extensions. Project - // entries shadow user entries with the same name; tool-name collisions - // between extensions abort startup. + // Discover Lua extensions across three layers — system + // ($PANTO_HOME/agent), user ($XDG_CONFIG_HOME/panto or + // $HOME/.config/panto), and project (./.panto). Project shadows + // user shadows system; tool-name collisions across surviving + // entries abort startup. const n_ext_tools = extension_loader.discoverAndLoad( alloc, io, init.environ_map, + luarocks_rt.layout.agent_dir, rt, ) catch |err| { std.log.err("extension discovery failed: {t}", .{err}); diff --git a/src/panto_home.zig b/src/panto_home.zig index b7c1799..353e54b 100644 --- a/src/panto_home.zig +++ b/src/panto_home.zig @@ -30,6 +30,11 @@ pub const Layout = struct { allocator: Allocator, /// `$PANTO_HOME` itself. home: []u8, + /// `$PANTO_HOME/agent/` — the "system" extension/tool tree, + /// populated at bootstrap from files embedded into the panto + /// binary. Searched after user/project layers for tools and + /// extensions; project shadows user shadows system. + agent_dir: []u8, /// `$PANTO_HOME/rocks/lua-/` — the versioned tree. tree: []u8, /// `/include/` — where Lua headers are staged. @@ -52,6 +57,7 @@ pub const Layout = struct { pub fn deinit(self: Layout) void { const a = self.allocator; a.free(self.home); + a.free(self.agent_dir); a.free(self.tree); a.free(self.include_dir); a.free(self.share_lua_dir); @@ -76,6 +82,9 @@ pub fn resolve( const home = try resolveHome(allocator, environ_map); errdefer allocator.free(home); + const agent_dir = try std.fs.path.join(allocator, &.{ home, "agent" }); + errdefer allocator.free(agent_dir); + // `/rocks/lua-` const tree_subdir = try std.fmt.allocPrint( allocator, @@ -111,6 +120,7 @@ pub fn resolve( return .{ .allocator = allocator, .home = home, + .agent_dir = agent_dir, .tree = tree, .include_dir = include_dir, .share_lua_dir = share_lua_dir, @@ -143,6 +153,7 @@ fn resolveHome( /// — existing directories are left alone. pub fn ensureDirsExist(layout: Layout, io: Io) !void { try makePathRecursive(io, layout.home); + try makePathRecursive(io, layout.agent_dir); try makePathRecursive(io, layout.tree); try makePathRecursive(io, layout.include_dir); try makePathRecursive(io, layout.share_lua_dir); -- cgit v1.3