-- Self-check for the panto-agent suite. Run with: -- -- panto lua panto-agent/_selfcheck.lua -- -- Uses a stub `panto` module, so it exercises the pure extension logic -- (file selection, symlink resolution, message format) without a session. local uv = require("luv") -- Stub panto module: capture agent:add_system_message calls and `on` -- subscriptions (ext.agent stands in for the session agent). Must be -- seeded before rules.lua is required. local captured = {} local handlers = {} local registered = {} package.loaded["panto"] = { ext = { agent = { add_system_message = function(_, text) captured[#captured + 1] = text end, }, on = function(event, handler) handlers[event] = handler end, register_tool = function(t) registered[#registered + 1] = t end, json = { -- Just enough JSON for the inputs this selfcheck fabricates -- (a flat object with a "path" string, no escapes). decode = function(s) return { path = s:match('"path":"(.-)"') } end, }, }, } -- Make the suite requirable relative to this file, wherever cwd is. local here = debug.getinfo(1, "S").source:match("^@(.*)/[^/]+$") or "." package.path = here .. "/panto-agent/?.lua;" .. here .. "/panto-agent/?/init.lua;" .. package.path local rules = require("rules") assert(rules.name == "agent.rules", "entry name") -- Harness pump: panto runs entrypoints inside a coroutine with the uv -- loop driven; this standalone script has to provide the same -- environment itself. local function run(fn) local results local co = coroutine.create(function() results = table.pack(fn()) end) local ok, err = coroutine.resume(co) if not ok then error(err, 0) end while coroutine.status(co) ~= "dead" do uv.run("once") end return table.unpack(results, 1, results.n) end local function write(path, text) local f = assert(io.open(path, "w")) f:write(text) f:close() end local tmp = assert(uv.fs_mkdtemp("/tmp/panto-agent-selfcheck-XXXXXX")) local function dir(name) local d = tmp .. "/" .. name assert(uv.fs_mkdir(d, 493)) -- 0755 return d end -- both: AGENTS.md wins, CLAUDE.md ignored local both = dir("both") write(both .. "/AGENTS.md", "agents rules") write(both .. "/CLAUDE.md", "claude rules") -- claude-only: falls back to CLAUDE.md local claude_only = dir("claude-only") write(claude_only .. "/CLAUDE.md", "claude only") -- empty: contributes nothing local empty = dir("empty") -- linked: AGENTS.md is a symlink; resolved path reported local target_dir = dir("target") write(target_dir .. "/real-rules.md", "linked rules") local linked = dir("linked") assert(uv.fs_symlink(target_dir .. "/real-rules.md", linked .. "/AGENTS.md")) -- activate(): subscribes to session_start; each firing resets the -- session-global `seen` set and injects one message per file, using the -- event's cwd as the project root. Activate first so firing session_start -- doubles as the seen-reset between the scenarios below. rules.layer_dirs = function(cwd) return { cwd } end rules.activate() local on_session_start = handlers["session_start"] assert(type(on_session_start) == "function", "session_start handler registered") assert(#captured == 0, "nothing injected before session_start fires") -- collect() awaits libuv callbacks, so it runs under the harness pump. local got = run(function() return rules.collect({ both, claude_only, empty, linked }) end) assert(#got == 3, "expected 3 collected, got " .. #got) assert(got[1].path == uv.fs_realpath(both .. "/AGENTS.md"), "AGENTS.md over CLAUDE.md") assert(got[1].text == "agents rules") assert(got[2].path == uv.fs_realpath(claude_only .. "/CLAUDE.md"), "CLAUDE.md fallback") assert(got[2].text == "claude only") assert(got[3].path == uv.fs_realpath(target_dir .. "/real-rules.md"), "symlink resolved") assert(got[3].text == "linked rules") -- Session-global dedupe: a repeat collect of already-seen files is empty. local again = run(function() return rules.collect({ both }) end) assert(#again == 0, "seen files are not collected twice in a session") -- A session boundary resets `seen` (empty dir: injects nothing). run(function() on_session_start({ cwd = empty }) end) assert(#captured == 0, "empty root injects nothing") -- Two dirs reaching the same real file within one collect: used once. local dedup = run(function() return rules.collect({ linked, target_dir }) end) assert(#dedup == 1, "same real file deduped") run(function() on_session_start({ cwd = both }) end) assert(#captured == 1, "one system message") local want = "context from " .. uv.fs_realpath(both .. "/AGENTS.md") .. ":\n\nagents rules" assert(captured[1] == want, "message format:\n" .. captured[1]) -- A second session boundary (e.g. /new) re-injects for the new cwd. run(function() on_session_start({ cwd = claude_only }) end) assert(#captured == 2 and captured[2]:find("claude only"), "re-injects on next session") -- tool_result handler: rules files between a read and the session root -- ride on the result via the writable `output` field, and the component -- wrapper lists them. Tree: proj/ (root, covered by the system prompt), -- proj/sub (AGENTS.md), proj/sub/deep (CLAUDE.md only). local proj = dir("proj") write(proj .. "/AGENTS.md", "root rules") assert(uv.fs_mkdir(proj .. "/sub", 493)) write(proj .. "/sub/AGENTS.md", "sub rules") assert(uv.fs_mkdir(proj .. "/sub/deep", 493)) write(proj .. "/sub/deep/CLAUDE.md", "deep rules") write(proj .. "/sub/deep/file.txt", "hello") run(function() on_session_start({ cwd = proj }) end) local on_tool_result = handlers["tool_result"] assert(type(on_tool_result) == "function", "tool_result handler registered") -- Fabricate a `tool_result` event: a plain table stands in for the real -- event userdata (assignment and method calls look the same). local fake_comp = { render = function(_, _) return { "read line" } end } local function read_event(path) local ev = { tool_name = "std.read", input = '{"path":"' .. path .. '"}', output = "FILE", } ev.get_component = function() return fake_comp end ev.set_component = function(_, t) ev.component = t end return ev end local ev = read_event(proj .. "/sub/deep/file.txt") run(function() on_tool_result(ev) end) local sub_real = uv.fs_realpath(proj .. "/sub/AGENTS.md") local deep_real = uv.fs_realpath(proj .. "/sub/deep/CLAUDE.md") assert(ev.output == "FILE" .. "\n\n---\ncontext from " .. sub_real .. ":\n\nsub rules" .. "\n\n---\ncontext from " .. deep_real .. ":\n\ndeep rules", "walk order + format:\n" .. ev.output) -- The component wrapper passes the original lines through and lists the -- attached files below them. local lines = ev.component.render(ev.component, 80) assert(lines[1] == "read line", "wrapper renders through the original") assert(lines[2] == "+ rules: " .. sub_real and lines[3] == "+ rules: " .. deep_real, "wrapper lists attached files") -- Same read again: everything already seen, result and component untouched. local ev2 = read_event(proj .. "/sub/deep/file.txt") run(function() on_tool_result(ev2) end) assert(ev2.output == "FILE" and ev2.component == nil, "already-seen rules are not re-injected") -- Reading a directory directly starts the walk at that directory. assert(uv.fs_mkdir(proj .. "/other", 493)) write(proj .. "/other/AGENTS.md", "other rules") local ev3 = read_event(proj .. "/other") run(function() on_tool_result(ev3) end) assert(ev3.output:find("other rules", 1, true), "directory read includes its own rules") -- Reads outside the session root add nothing; other tools are ignored. local ev4 = read_event(both .. "/AGENTS.md") run(function() on_tool_result(ev4) end) assert(ev4.output == "FILE", "outside-root reads are untouched") local ev5 = read_event(proj .. "/sub/deep/file.txt") ev5.tool_name = "std.shell" run(function() on_tool_result(ev5) end) assert(ev5.output == "FILE", "non-read tools are ignored") -- agent.skill: layered discovery, frontmatter-name shadowing, enum/desc -- registration, frontmatter-stripped load with a path preamble. local skill = require("skill") assert(skill.name == "agent.skill", "skill entry name") local function write_skill(layer, dirname, name, desc, body) local d = layer .. "/" .. dirname assert(uv.fs_mkdir(d, 493)) write(d .. "/SKILL.md", "---\nname: " .. name .. "\ndescription: " .. desc .. "\nmetadata:\n author: selfcheck\n---\n" .. body) return d end local sk_low = dir("skills-low") local sk_high = dir("skills-high") -- alpha in both layers; the high layer's directory name differs from the -- frontmatter name, which is the identity that shadows. write_skill(sk_low, "alpha", "alpha", "low alpha", "low alpha body\n") local alpha_high = write_skill(sk_high, "alpha-dir", "alpha", '"high alpha"', "high alpha body\n") write_skill(sk_low, "beta", "beta", "beta desc", "beta body\n") -- Not skills: no SKILL.md; SKILL.md without frontmatter. assert(uv.fs_mkdir(sk_low .. "/noskill", 493)) assert(uv.fs_mkdir(sk_low .. "/badfm", 493)) write(sk_low .. "/badfm/SKILL.md", "just markdown, no frontmatter\n") skill.skill_dirs = function() return { sk_low, sk_high } end run(function() skill.activate() end) assert(#registered == 1, "one tool registered") local tool = registered[1] assert(tool.name == "agent.skill", "tool name") local enum = tool.schema.properties.skill.enum assert(#enum == 2 and enum[1] == "alpha" and enum[2] == "beta", "enum is the surviving names, sorted") assert(tool.description:find("- alpha: high alpha", 1, true), "shadowing layer's description wins") assert(tool.description:find("- beta: beta desc", 1, true), "description lists every skill") local alpha_real = uv.fs_realpath(alpha_high) local out = run(function() return tool.handler({ skill = "alpha" }) end) assert(out:find("loaded from " .. alpha_real .. "/SKILL.md", 1, true), "preamble names the real path") assert(out:find("resolve it against " .. alpha_real .. "/", 1, true), "preamble covers relative paths") assert(out:find("high alpha body", 1, true), "higher layer shadows lower") assert(not out:find("name: alpha", 1, true), "frontmatter stripped") local miss = run(function() return tool.handler({ skill = "nope" }) end) assert(miss:find("unknown skill: nope", 1, true) and miss:find("alpha, beta", 1, true), "unknown skill lists the available ones") -- No skills anywhere: the tool is not registered at all. skill.skill_dirs = function() return { empty } end run(function() skill.activate() end) assert(#registered == 1, "no skills, no tool") print("panto-agent selfcheck: OK")