diff options
| author | t <t@tjp.lol> | 2026-08-20 12:49:36 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-08-20 13:39:46 -0600 |
| commit | a08d854d52cf5492a7763bccc1ce475dcc057cba (patch) | |
| tree | ccc2933bba649e9cd6d0fbe0cfc2426f9ae2d886 | |
| parent | d1306506aa7f504b0e91c9c6ed7314afbf99978e (diff) | |
Replace lyaml with api7-lua-tinyyaml and prepare the 0.1.0 releasev0.1.0
lyaml binds the system libyaml, which luarocks does not vendor, so
installing the rock failed wherever that library was absent, taking the
whole extension down on a stock macOS machine. api7-lua-tinyyaml is pure
Lua and matches lyaml on every quoting, escaping and block-scalar form
profiles use.
What the subset costs: anchors and aliases are not resolved, and plain
multi-line scalars raise. The raise surfaces through the existing parse
warning, but an unresolved alias came back as the literal "*alias"
string and would have become a profile name, so frontmatter.parse now
detects those keys, drops them, and warns.
Point source.url at code.tjp.lol with a v0.1.0 tag, replacing a GitHub
URL that never existed. Add a LICENSE file and mise release tasks, and
fix the README dependency-install command, which called a luarocks.cmd
function that does not exist.
| -rw-r--r-- | LICENSE | 18 | ||||
| -rw-r--r-- | README.md | 23 | ||||
| -rw-r--r-- | mise.toml | 83 | ||||
| -rw-r--r-- | panto-subagents-0.1.0-1.rockspec | 24 | ||||
| -rw-r--r-- | spec/run.lua | 2 | ||||
| -rw-r--r-- | spec/test_frontmatter.lua | 44 | ||||
| -rw-r--r-- | spec/test_init.lua | 6 | ||||
| -rw-r--r-- | spec/test_profiles.lua | 6 | ||||
| -rw-r--r-- | subagents/frontmatter.lua | 44 |
9 files changed, 195 insertions, 55 deletions
@@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 Travis Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. @@ -16,18 +16,26 @@ For a local checkout, use `paths = ["/path/to/panto-subagents"]` instead. A in panto's own rocks tree first: ```sh -panto lua -e 'require("luarocks.cmd").run("install", "toml2lua")' -panto lua -e 'require("luarocks.cmd").run("install", "lyaml")' +TREE="$(echo ~/.local/share/panto/rocks/lua-*)" +luarocks --lua-version 5.4 --tree "$TREE" install toml2lua +luarocks --lua-version 5.4 --tree "$TREE" install api7-lua-tinyyaml ``` +(Every rock here is pure Lua, so any LuaRocks targeting Lua 5.4 can populate +panto's tree. A `rocks` entry needs none of this — panto installs dependencies +with the rock.) + ### Dependencies Panto installs the rock's dependencies with it: -- **lyaml** parses profile frontmatter. It binds the system libyaml, which - LuaRocks does not vendor — install it first (`brew install libyaml`, or - `apt install libyaml-dev`), otherwise the rock fails to build and panto - quietly starts without the `subagents.*` tools. +- **api7-lua-tinyyaml** parses profile frontmatter. Pure Lua, nothing to + install alongside it. It covers a subset of YAML — every quoting, escaping + and block-scalar form profiles use, but not anchors/aliases (flagged with a + warning rather than silently misread) and not plain multi-line scalars + (a parse warning). Full-spec `lyaml` was the alternative, rejected because + binding the system libyaml makes the rock fail to build wherever that + library is absent. - **toml2lua** reads the layered `config.toml` at activation and TOML workflows afterwards. Pure Lua, nothing to install; without it activation fails. - **luv** backs profile and workflow discovery. Panto already ships it. @@ -151,8 +159,7 @@ mise run deps # install the rocks into ./.rocks (gitignored) mise run check # run the specs against that tree ``` -`mise run deps` passes Homebrew's libyaml prefix to lyaml when brew is -available. Without mise, install the same rocks with any LuaRocks targeting Lua +Without mise, install the same rocks with any LuaRocks targeting Lua 5.4 and run `lua spec/run.lua` from the repo root. `panto lua spec/run.lua` runs the suite inside panto's own interpreter and rocks tree, skipping whatever cases that tree has no rock for. @@ -5,21 +5,15 @@ lua = "5.4" # puts that tree on package.path and runs the specs. Both pin --lua-version 5.4 # because a stray system luarocks may default to another Lua. # -# lyaml is a C binding over libyaml and needs the system library present: -# `brew install libyaml` on macOS, `apt install libyaml-dev` on Debian. The -# YAML_DIR below points luarocks at Homebrew's prefix when brew is available and -# otherwise lets luarocks search the usual system paths. +# Every rock below is pure Lua, so there is no system library to install first +# and nothing to point luarocks at. [tasks.deps] description = "Install the Lua rocks the extension and specs need into ./.rocks" dir = "{{config_root}}" run = """ set -e -if brew --prefix libyaml >/dev/null 2>&1; then - luarocks --lua-version 5.4 --tree .rocks install lyaml YAML_DIR="$(brew --prefix libyaml)" -else - luarocks --lua-version 5.4 --tree .rocks install lyaml -fi +luarocks --lua-version 5.4 --tree .rocks install api7-lua-tinyyaml luarocks --lua-version 5.4 --tree .rocks install toml2lua luarocks --lua-version 5.4 --tree .rocks install luv luarocks --lua-version 5.4 --tree .rocks install dkjson @@ -57,3 +51,74 @@ set -e eval "$(luarocks --lua-version 5.4 --tree .rocks path)" lua spec/run.lua """ +[tasks.version] +description = "Rewrite the rockspec for a new version (then commit it)" +dir = "{{config_root}}" +run = """ +set -eu +NEW="{{arg(name='version')}}" +SPEC="$(ls -1 ./*.rockspec)" +case "$SPEC" in *" +"*) echo "expected exactly one rockspec, found:" >&2; echo "$SPEC" >&2; exit 1;; esac + +PKG="$(sed -n 's/^package = "\\(.*\\)"/\\1/p' "$SPEC")" +NEWSPEC="./$PKG-$NEW-1.rockspec" + +# Write through a temp file: without it, a re-run at the same version would +# have the redirect truncate the file sed is still reading. +TMP="$(mktemp)" +sed -e "s/^version = \\".*\\"/version = \\"$NEW-1\\"/" \\ + -e "s/^\\( *\\)tag = \\"v[^\\"]*\\"/\\1tag = \\"v$NEW\\"/" "$SPEC" > "$TMP" +mv "$TMP" "$NEWSPEC" +[ "$SPEC" = "$NEWSPEC" ] || rm -f "$SPEC" + +luarocks lint "$NEWSPEC" +echo "wrote $NEWSPEC -- commit it, then: mise run release" +""" + +[tasks.release] +description = "Tag the committed version, push the tag, upload to luarocks.org" +depends = ["check"] +dir = "{{config_root}}" +run = """ +set -eu +SPEC="$(ls -1 ./*.rockspec)" +case "$SPEC" in *" +"*) echo "expected exactly one rockspec, found:" >&2; echo "$SPEC" >&2; exit 1;; esac + +VER="$(sed -n 's/^version = "\\(.*\\)-[0-9]*"/\\1/p' "$SPEC")" +[ -n "$VER" ] || { echo "could not read version from $SPEC" >&2; exit 1; } +TAG="v$VER" + +# jj's working copy is itself a commit, so @ is normally the empty commit a +# `jj new` just made and the release content sits in @-. Override with +# REV=@ (or any revset) when that is not the case. +REV="${REV:-@-}" + +# The tag is what `luarocks upload` clones to build the .src.rock, so the +# rockspec being published has to already be in the revision being tagged. +# Uncommitted, they diverge silently: the upload would ship a rockspec that +# says $VER over sources from a commit that never had it. +if ! jj file show -r "$REV" "$SPEC" 2>/dev/null | diff -q - "$SPEC" >/dev/null 2>&1; then + echo "$SPEC at $REV differs from the working copy -- commit it first" >&2 + exit 1 +fi + +if jj tag list | grep -q "^$TAG:"; then + echo "tag $TAG already exists -- bump the version first" >&2 + exit 1 +fi + +# jj 0.42 creates tags natively and exports them to git in a colocated repo, +# but `jj git push` only pushes bookmarks -- tags still need git. +jj tag set "$TAG" -r "$REV" +jj tag list | grep "^$TAG:" +for remote in $(git remote); do + git push "$remote" "$TAG" +done + +# Reads the API key from ~/.luarocks/upload_config.lua. Packs a .src.rock by +# cloning source.url at the tag just pushed, so the push has to come first. +luarocks upload "$SPEC" +""" + diff --git a/panto-subagents-0.1.0-1.rockspec b/panto-subagents-0.1.0-1.rockspec index 5ae55ff..3a6293a 100644 --- a/panto-subagents-0.1.0-1.rockspec +++ b/panto-subagents-0.1.0-1.rockspec @@ -8,10 +8,19 @@ -- checkout gets from `paths = ["/path/to/panto-subagents"]`, so both load paths -- resolve identical requires. -- --- Dependencies. `lyaml` parses profile frontmatter and is a C binding over the --- system libyaml, which luarocks does not vendor: a machine without it needs --- `brew install libyaml` (or `apt install libyaml-dev`) first, and may need --- `YAML_DIR=...` passed to luarocks. `toml2lua` (module name `toml`) reads the +-- Dependencies. `api7-lua-tinyyaml` (module name `tinyyaml`) parses profile +-- frontmatter. It is pure Lua, so it installs anywhere without a system +-- library: the obvious alternative, `lyaml`, is a C binding over libyaml that +-- luarocks does not vendor, which would put `brew install libyaml` (or +-- `apt install libyaml-dev`, plus sometimes `YAML_DIR=...`) between a user and +-- a working extension. The cost is that tinyyaml parses a subset: it matches +-- lyaml on every quoting, escaping and block-scalar form profiles use, but it +-- does not resolve anchors/aliases (subagents/frontmatter.lua detects and +-- warns rather than accepting the raw `*alias` token) and it raises on plain +-- multi-line scalars, which surfaces as a parse warning. The api7 fork is +-- preferred over peposso's original: it fixes `'it''s here'` truncating to +-- `it`, and it ships a .src.rock instead of resolving a `git://` URL. +-- `toml2lua` (module name `toml`) reads the -- layered `config.toml` at activation and the TOML workflows afterwards, so it -- is required, not optional; it is pure Lua and needs nothing from the system. `luv` backs the recursive discovery walk and the -- wake pipes child jobs are polled on, and ships with panto as a pinned @@ -31,7 +40,8 @@ package = "panto-subagents" version = "0.1.0-1" source = { - url = "git+https://github.com/travisp/panto-subagents", + url = "git+https://code.tjp.lol/public/panto-subagents.git", + tag = "v0.1.0", } description = { @@ -44,14 +54,14 @@ description = { one-shot structured workers, and TOML dependency graphs exposed as /workflow:<name> commands. ]], - homepage = "https://github.com/travisp/panto-subagents", + homepage = "https://code.tjp.lol/public/panto-subagents", license = "MIT", labels = { "ai", "llm", "agent", "panto", "pantograph", "extension" }, } dependencies = { "lua >= 5.4, < 5.5", - "lyaml >= 6.2, < 7.0", + "api7-lua-tinyyaml >= 0.4.4, < 0.5", "toml2lua >= 3.0, < 4.0", "luv >= 1.48", } diff --git a/spec/run.lua b/spec/run.lua index ee9f432..5476e7b 100644 --- a/spec/run.lua +++ b/spec/run.lua @@ -2,7 +2,7 @@ -- -- Expected environment. Plain Lua 5.4 with the repo root on package.path, which -- this file arranges from `arg[0]`, plus the rocks the extension depends on --- (lyaml, toml2lua, luv) and dkjson for the specs' JSON decoding. `mise run +-- (api7-lua-tinyyaml, toml2lua, luv) and dkjson for the specs' JSON decoding. `mise run -- check` puts the ./.rocks tree on LUA_PATH/LUA_CPATH first and is the intended -- entry point; a bare `lua spec/run.lua` also works if those rocks are on the -- default path. `panto lua spec/run.lua` works too — panto's own rocks tree diff --git a/spec/test_frontmatter.lua b/spec/test_frontmatter.lua index 3f0c4c5..f729254 100644 --- a/spec/test_frontmatter.lua +++ b/spec/test_frontmatter.lua @@ -2,18 +2,18 @@ -- -- The body is the part a broken header must never cost the user, so every -- degraded case is checked for "body preserved verbatim" as well as for the --- warning. Cases that actually parse YAML need lyaml and skip without it. +-- warning. Cases that actually parse YAML need tinyyaml and skip without it. local frontmatter = require("subagents.frontmatter") -local function lyaml_or_skip() - return pcall(require, "lyaml") +local function yaml_or_skip() + return pcall(require, "tinyyaml") end return { { "fenced header parses and keeps the body verbatim", function() - if not lyaml_or_skip() then - return "skip", "lyaml is not installed" + if not yaml_or_skip() then + return "skip", "tinyyaml is not installed" end local body = "You are a reviewer.\n\n indented line \n" local data, parsed_body, warning = frontmatter.parse( @@ -27,8 +27,8 @@ return { end }, { "CRLF fences are tolerated", function() - if not lyaml_or_skip() then - return "skip", "lyaml is not installed" + if not yaml_or_skip() then + return "skip", "tinyyaml is not installed" end local data, body, warning = frontmatter.parse("---\r\nname: crlf\r\n---\r\nbody\r\n") assert(warning == nil, tostring(warning)) @@ -59,26 +59,32 @@ return { assert(warning == nil, tostring(warning)) end }, - { "broken YAML warns and keeps the body", function() - if not lyaml_or_skip() then - return "skip", "lyaml is not installed" + { "a header the parser rejects warns and keeps the body", function() + if not yaml_or_skip() then + return "skip", "tinyyaml is not installed" end - local data, body, warning = frontmatter.parse("---\na: [unclosed\n---\nbody\n") - assert(data == nil, "a broken header must not produce metadata") + local data, body, warning = frontmatter.parse("---\njust a string\n---\nbody\n") + assert(data == nil, "a rejected header must not produce metadata") assert(body == "body\n", string.format("%q", body)) assert(type(warning) == "string" and warning:find("did not parse", 1, true), "expected a parse warning, got " .. tostring(warning)) end }, - { "a non-mapping document warns and keeps the body", function() - if not lyaml_or_skip() then - return "skip", "lyaml is not installed" + { "an unresolved alias is dropped rather than read literally", function() + if not yaml_or_skip() then + return "skip", "tinyyaml is not installed" end - local data, body, warning = frontmatter.parse("---\njust a string\n---\nbody\n") - assert(data == nil, "a scalar header must not produce metadata") + -- tinyyaml does not resolve aliases: without the guard in parse(), the + -- `*d` token would arrive as the literal string "*d" and become this + -- profile's name. + local data, body, warning = frontmatter.parse( + "---\ndefs: &d my-agent\nname: *d\n---\nbody\n") + assert(type(data) == "table", "the mapping itself should survive") + assert(data.name == nil, "an alias must not become a value, got " .. tostring(data.name)) + assert(data.defs == nil, "an anchor must not become a value, got " .. tostring(data.defs)) assert(body == "body\n", string.format("%q", body)) - assert(type(warning) == "string" and warning:find("not a mapping", 1, true), - "expected a mapping warning, got " .. tostring(warning)) + assert(type(warning) == "string" and warning:find("anchors/aliases", 1, true), + "expected an alias warning, got " .. tostring(warning)) end }, { "empty input is empty output", function() diff --git a/spec/test_init.lua b/spec/test_init.lua index 5d2f8d9..eb8777a 100644 --- a/spec/test_init.lua +++ b/spec/test_init.lua @@ -5,7 +5,7 @@ -- lifecycle subscription is invisible until a user notices the tools are gone or -- a cancelled turn leaves children running. Discovery is pointed at a temporary -- config layer, so the assertions do not depend on this machine's ~/.config; the --- first case skips when luv or lyaml is missing because the profile it looks for +-- first case skips when luv or tinyyaml is missing because the profile it looks for -- could not be read without them. local fake = require("spec.fake_ext") @@ -55,8 +55,8 @@ return { if not ok_uv then return "skip", "luv is not installed" end - if not pcall(require, "lyaml") then - return "skip", "lyaml is not installed" + if not pcall(require, "tinyyaml") then + return "skip", "tinyyaml is not installed" end local tmp = assert(uv.fs_mkdtemp("/tmp/panto-subagents-init-XXXXXX")) diff --git a/spec/test_profiles.lua b/spec/test_profiles.lua index f2a54a2..680679f 100644 --- a/spec/test_profiles.lua +++ b/spec/test_profiles.lua @@ -4,7 +4,7 @@ -- real config, by passing explicit roots to `discover` — the same seam the -- extension uses for the user and project layers, in the same order. -- --- Needs luv (the recursive walk) and lyaml (the frontmatter); without either the +-- Needs luv (the recursive walk) and tinyyaml (the frontmatter); without either the -- whole file skips rather than asserting on a degraded parse. local profiles = require("subagents.profiles") @@ -22,8 +22,8 @@ local function discover_fixture() if not ok_uv then return nil, "luv is not installed" end - if not pcall(require, "lyaml") then - return nil, "lyaml is not installed" + if not pcall(require, "tinyyaml") then + return nil, "tinyyaml is not installed" end local tmp = assert(uv.fs_mkdtemp("/tmp/panto-subagents-profiles-XXXXXX")) diff --git a/subagents/frontmatter.lua b/subagents/frontmatter.lua index 94fe2ac..8740870 100644 --- a/subagents/frontmatter.lua +++ b/subagents/frontmatter.lua @@ -13,8 +13,9 @@ -- (a lone `---` at the top of a prose file is a horizontal rule, not a -- broken header, so this case is deliberately silent) -- * empty fenced block -> empty mapping, no warning --- * lyaml missing or erroring -> body after the fence, warning returned +-- * parser missing or erroring-> body after the fence, warning returned -- * YAML document not a map -> body after the fence, warning returned +-- * unresolved anchor/alias -> that key dropped, warning returned -- -- In the warning cases the fenced block is dropped rather than folded back -- into the body: an unparseable header is noise the child agent should not be @@ -27,6 +28,34 @@ local function trim(s) return (s:gsub("^%s+", ""):gsub("%s+$", "")) end +-- tinyyaml parses a subset of YAML: it does not resolve anchors (`&a`) or +-- aliases (`*a`). Rather than failing on them it hands back the raw token as +-- a plain string, so `name: *base` arrives here as the literal "*base" and +-- would sail through any `type(v) == "string"` check to become a profile's +-- actual name. Detect that shape and drop those keys so the caller falls back +-- to its defaults instead of adopting a bogus value. +-- +-- The match is deliberately narrow -- a whole value that is exactly `*word`, +-- or one opening with `&word ` -- so ordinary prose containing `*` or `&` +-- (`'fetch & parse'`, `'a *b* c'`) is left alone. +local function strip_unresolved_aliases(data) + local hits = {} + for k, v in pairs(data) do + if type(v) == "string" + and (v:match("^%*[%w_%-]+$") or v:match("^&[%w_%-]+%s")) then + hits[#hits + 1] = tostring(k) + end + end + if #hits == 0 then + return nil + end + table.sort(hits) + for _, k in ipairs(hits) do + data[k] = nil + end + return table.concat(hits, ", ") +end + -- Iterate lines, yielding the line plus its start offset and the offset just -- past its newline, so the caller can slice the original text exactly. local function lines(text) @@ -79,17 +108,22 @@ function M.parse(text) return {}, body, nil end - local ok_lyaml, lyaml = pcall(require, "lyaml") - if not ok_lyaml then - return nil, body, "lyaml is not installed; ignoring the YAML frontmatter" + local ok_yaml, yaml = pcall(require, "tinyyaml") + if not ok_yaml then + return nil, body, "tinyyaml is not installed; ignoring the YAML frontmatter" end - local ok, data = pcall(lyaml.load, block) + local ok, data = pcall(yaml.parse, block) if not ok then return nil, body, "YAML frontmatter did not parse: " .. tostring(data) end if type(data) ~= "table" then return nil, body, "YAML frontmatter is not a mapping; ignoring it" end + local aliased = strip_unresolved_aliases(data) + if aliased then + return data, body, + "YAML anchors/aliases are not supported; ignoring: " .. aliased + end return data, body, nil end |
