summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-08-18 14:45:03 -0600
committert <t@tjp.lol>2026-08-18 14:45:08 -0600
commit2acd6b0e38e4af1fd13bad1cef8af7407b20684c (patch)
treee95c6a20a8b96bab627ef3974b7c355faba4d66a
initial commit
-rw-r--r--README.md45
-rw-r--r--_selfcheck.lua44
-rw-r--r--panto-herdr-scm-1.rockspec28
-rw-r--r--panto-herdr/init.lua53
4 files changed, 170 insertions, 0 deletions
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..18e221e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+# panto-herdr
+
+`panto-herdr` reports panto lifecycle state to [Herdr](https://herdr.dev/) when
+panto is running in a Herdr pane. It reports `panto` as the agent label and
+uses `thinking`, `responding`, `compacting`, and `tool:<name>` as short status
+labels.
+
+Install it through panto's rock loader:
+
+```toml
+[extensions]
+rocks = ["panto-herdr"]
+```
+
+Then run `panto update`. Start panto in Herdr, for example:
+
+```sh
+herdr agent start panto --cwd "$PWD" -- panto
+```
+
+The extension is inert outside Herdr: it only activates when `HERDR_PANE_ID`
+is set. It invokes the local `herdr` executable asynchronously and discards
+its JSON output, so reporting does not affect the panto terminal UI.
+
+## State mapping
+
+| panto event | Herdr state |
+| --- | --- |
+| `session_start` | `idle` |
+| `user_message`, `thinking`, `compaction`, `tool_details`, `assistant_text` | `working` |
+| `assistant_text_complete` | `idle` |
+
+Current panto extensions do not receive a turn-complete or interrupt event.
+`assistant_text_complete` is therefore the best available idle boundary; an
+assistant text block followed by a tool call can show `idle` briefly before
+the next lifecycle event restores `working`. Native session restore is also
+out of scope until panto exposes a stable session reference to extensions.
+
+## Check
+
+Run the module-level check with panto's Lua runtime:
+
+```sh
+panto lua _selfcheck.lua
+```
diff --git a/_selfcheck.lua b/_selfcheck.lua
new file mode 100644
index 0000000..b4d0396
--- /dev/null
+++ b/_selfcheck.lua
@@ -0,0 +1,44 @@
+local events = {}
+local reports = {}
+
+assert(loadfile("panto-herdr-scm-1.rockspec"))
+
+package.preload.panto = function()
+ return {
+ ext = {
+ on = function(name, handler) events[name] = handler end,
+ },
+ }
+end
+
+package.preload.luv = function()
+ return {
+ spawn = function(command, options, _)
+ reports[#reports + 1] = { command = command, args = options.args }
+ return { close = function() end }
+ end,
+ }
+end
+
+local getenv = os.getenv
+os.getenv = function(name)
+ if name == "HERDR_PANE_ID" then return "w1:p1" end
+ return getenv(name)
+end
+
+local entry = require("panto-herdr")
+entry.activate()
+events.session_start()
+events.user_message()
+events.user_message()
+events.tool_details({ tool_name = "read" })
+events.assistant_text()
+events.assistant_text_complete()
+
+assert(#reports == 5)
+assert(reports[1].command == "herdr")
+assert(table.concat(reports[1].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 1")
+assert(table.concat(reports[3].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state working --seq 3 --custom-status tool:read")
+assert(table.concat(reports[5].args, " ") == "pane report-agent w1:p1 --source panto:extension --agent panto --state idle --seq 5")
+
+print("panto-herdr self-check: ok")
diff --git a/panto-herdr-scm-1.rockspec b/panto-herdr-scm-1.rockspec
new file mode 100644
index 0000000..d974b69
--- /dev/null
+++ b/panto-herdr-scm-1.rockspec
@@ -0,0 +1,28 @@
+rockspec_format = "3.0"
+package = "panto-herdr"
+version = "scm-1"
+
+source = {
+ -- Replace when the repository has a public remote.
+ url = "git+https://example.invalid/panto-herdr.git",
+}
+
+description = {
+ summary = "Herdr lifecycle reporting for panto",
+ detailed = [[
+A panto extension that reports panto lifecycle state to Herdr when it runs
+inside a Herdr pane.
+]],
+ license = "MIT",
+}
+
+dependencies = {
+ "lua >= 5.1",
+}
+
+build = {
+ type = "builtin",
+ modules = {
+ ["panto-herdr"] = "panto-herdr/init.lua",
+ },
+}
diff --git a/panto-herdr/init.lua b/panto-herdr/init.lua
new file mode 100644
index 0000000..dffef51
--- /dev/null
+++ b/panto-herdr/init.lua
@@ -0,0 +1,53 @@
+local function tool_status(name)
+ name = name or "?"
+ return "tool:" .. name:sub(1, 27)
+end
+
+return {
+ name = "herdr",
+ activate = function()
+ local pane_id = os.getenv("HERDR_PANE_ID")
+ if not pane_id or pane_id == "" then return end
+
+ local panto = require("panto")
+ local uv = require("luv")
+ local seq = 0
+ local last_state
+ local last_status
+
+ local function report(state, status)
+ if state == last_state and status == last_status then return end
+ last_state, last_status = state, status
+ seq = seq + 1
+
+ local args = {
+ "pane", "report-agent", pane_id,
+ "--source", "panto:extension",
+ "--agent", "panto",
+ "--state", state,
+ "--seq", tostring(seq),
+ }
+ if status then
+ args[#args + 1] = "--custom-status"
+ args[#args + 1] = status
+ end
+
+ local handle
+ pcall(function()
+ handle = uv.spawn("herdr", { args = args, stdio = { nil, nil, nil } }, function()
+ if handle then handle:close() end
+ end)
+ end)
+ end
+
+ panto.ext.on("session_start", function() report("idle") end)
+ panto.ext.on("user_message", function() report("working", "thinking") end)
+ panto.ext.on("thinking", function() report("working", "thinking") end)
+ panto.ext.on("compaction", function() report("working", "compacting") end)
+ panto.ext.on("tool_details", function(event)
+ report("working", tool_status(event.tool_name))
+ end)
+ panto.ext.on("assistant_text", function() report("working", "responding") end)
+ panto.ext.on("assistant_text_complete", function() report("idle") end)
+ end,
+}