@agimon-ai/po-runner

Supervised shell execution, background process control, and run logs for Pi coding agents.

Packages

Package details

package

Install @agimon-ai/po-runner from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@agimon-ai/po-runner
Package
@agimon-ai/po-runner
Version
0.0.0-alpha.107
Published
Nov 4, 2026
Downloads
18K/mo · 7,962/wk
Author
agiflow-ai
License
BUSL-1.1
Types
package
Size
2.5 MB
Dependencies
10 dependencies · 4 peers

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

README

@agimon-ai/po-runner

Supervised shell execution, sequential checks, background process control, complete session-temp logs.

Part of the Po distribution.

Runner provides the native bash tool in sessions that load it. Short commands return inline; long commands can be promoted to a background runner instead of blocking the agent. In native server sessions, an agent-watched runner retains background-work ownership until its completion wake-up is admitted. Delayed or retried handoffs must not let a workflow step finish early.

Alpha: tool and process contracts may change between releases.

Requirements

  • Node.js 24 or newer
  • /bin/bash
  • macOS or Linux on arm64 or x64 for bundled RMUX and RTK support

Linux native loading also depends on a compatible system loader and libc. The bundled binaries are glibc-linked, so a musl host such as Alpine cannot run them.

RTK is optional. Eligible commands return bounded raw output with an "RTK is unavailable" note when its binary is missing or cannot start. A synchronous spawn failure or an ENOENT, EACCES, or ENOEXEC spawn error disables further attempts for that RtkProcessor instance. Missing-path resolution and ordinary processing failures are not latched.

Panes come from the first backend that answers: bundled RMUX, then rmux on PATH, then tmux on PATH. Only when none is available does a non-interactive command fall back to a supervised subprocess, and interactive commands need one of them. A tmux-backed run is supervised by the same host process as an RMUX one, so exit metadata and logs behave the same; its session names carry a po-tmux- prefix rather than po-runner-.

Install

po init include Runner in default.packages in .po/modes.yaml. Move it to a named layer to enable it only in selected major modes.

Use the bash tool

{
  "command": ["pnpm test"],
  "timeout": 60
}

Commands crossing the promotion threshold move to a supervised runner. The default threshold is 60 seconds. Set background: true to detach immediately, or interactive: true only when a command needs terminal input.

Always pass command as an array, including for one command. Dependent commands run in separate fresh shells, sequentially and fail-fast:

{
  "command": ["pnpm build", "pnpm typecheck", "pnpm lint", "pnpm test"],
  "name": "verification",
  "timeout": 120
}

With interactive: true, only the last command gets a pseudo terminal. Runner returns control when that stage starts; inspect its stage runner ID and logs, then use that ID for input, not the sequence ID. Each command gets its own timeout and complete log. The result lists all attempted log paths and only the last executed command's trimmed output. Shell state such as cd and export does not carry between entries. Keep repository mutations separate from verification. A promoted array remains one supervised sequence, and its next command waits for the current process to actually exit. Native sessions receive one aggregate completion; remote MCP callers poll.

Runner requires a session identity and host-admitted PO_SESSION_TEMP_DIR. Processes, controls, and logs remain addressable after transcript compaction because identity is outside model context.

RTK result processing

Runner keeps the complete original log, then uses the matching RTK stdin filter only for conservatively recognized single commands. Current safe mappings cover cargo test, pytest, plain unified git diff, structured grep or rg, go test -json, and JSON-formatted ruff check. Compound commands, pipelines, redirects, substitutions, incompatible formats, unsupported commands, and logs above RTK's 10 MiB stdin limit return bounded raw output instead. An eligible RTK failure also returns bounded raw output with a warning and does not change the command exit result.

Inspect and control runs

When code mode is available, use its typed runner namespace rather than shelling out to the CLI:

// discover({ namespace: 'runner' }) shows current signatures.
return await runner.list();
return await runner.status({ id: 'returned-id' });
return await runner.logs({ id: 'returned-id', lines: 100 });
return await runner.stop({ id: 'unused-id', reason: 'finished' });

runner.input sends interactive input, and runner.stopAll stops this session's active work. Reads return bounded snapshots, not live streams. Sequence IDs report ordered stages and the guarded final result; stage IDs address the actual process logs. Check status before retrying, and distinguish a requested cancellation from confirmed termination.

Without code mode, the po-runner CLI supports:

po-runner list
po-runner status <runner-id>
po-runner logs <runner-id>
po-runner input <runner-id> --text "y" --enter
po-runner stop <runner-id>
po-runner stop-all

The input command requires a running interactive process backed by RMUX or tmux.

Stop runs that are no longer required.

Storage and limits

All runner files live beneath the session's host-admitted temporary directory: command logs, process metadata, sequence manifests, cancellation files, and supervision transport files. Runner does not use the artifact folder or a durable agent-directory fallback for session-owned files.

Session removal, restart, or termination stops its runner processes and removes its runner subtree and registry entries. An external observer handles owner hard-kill cleanup. Compaction and ordinary activity changes do not remove evidence. Save important deliverables to the session artifact folder.

Do not pipe or redirect command output to a file, head, or tail. Runner automatically saves full logs and bounds returned output. Inspect saved logs when an excerpt is truncated instead of rerunning the command. Result limits and the promotion threshold remain environment-configurable.

Result excerpts

A result that exceeds the ceiling is returned as an excerpt: roughly the first fifth of the budget holds the opening lines, the rest holds the closing ones, and a marker between them states how many lines were dropped.

The budget depends on the exit status. A command that failed gets 8 KiB and 120 lines, because its output is the evidence. A command that succeeded gets 2 KiB, because exiting 0 has already reported the outcome and the rest is mostly progress lines and repeated warnings:

Log excerpt (71 of 604 lines):
$ pnpm build
Compiling 1200 modules...
  compiled src/module-0.ts
… [5 lines elided] …
  compiled src/module-599.ts
Build succeeded in 12.4s
exit status 0
Full log: /path/to/run.log (17.0 KB, 604 lines); inspect with po-runner logs run-a

Three ceilings apply and whichever binds first wins: bytes, lines, and tokens. Tokens are there because bytes do not track context cost. Measured against gpt-tokenizer, real text runs from about 1.4 characters per token for base64 to 4.7 for English prose, so 8 KiB of one costs several times what 8 KiB of the other does. The defaults are 2,048 tokens on failure and 512 on success.

Token counts are estimated rather than tokenized: truncation runs on every command and is synchronous, so loading a vocabulary would charge every session for a table most never need. The estimator models how BPE behaves, with punctuation forcing boundaries, natural words cheap, and letter-digit runs expensive. Its worst error against gpt-tokenizer is a factor of about 1.46 either way, which is why the byte ceiling stays behind it as the hard bound.

PO_RUNNER_RESULT_MAX_BYTES, PO_RUNNER_RESULT_MAX_TOKENS, and their PO_RUNNER_SUCCESS_* counterparts move these for one invocation. A pragma overrides all of them, which is how one noisy successful command can still ask for the wider result. Raising only maxResultBytes will not help when the token ceiling is the one binding.

Error lines found in the dropped span are rescued into that marker's block, up to ten entries. Matching looks for a severity word in the first 60 characters, so [widget] build: error TS2322 and a line behind a timestamp both count, while 0 errors, no failures, and --error-format do not. Byte-identical lines collapse to one entry with an occurrence count. Lines that share a shape but differ in detail keep every variant, joined inside brackets so nothing actionable is lost:

ERROR in [src/widget-100.ts:100:7|src/widget-200.ts:200:7] - Type mismatch
error: connection refused (×40)

The complete raw log on disk is never truncated.

Configuration

Four layers, most specific first: a per-command pragma, then an environment variable, then project configuration, then the built-in default. Project configuration is the project's baseline, so an environment variable set for one invocation still wins over it.

One command that needs a wider result can say so on its first line. The pragma is a shell comment, so it stays harmless to the command itself:

# @po: {"maxResultBytes": 32768, "maxResultTokens": 8192, "maxResultLines": 300}
pnpm build

Project settings follow Pi's convention for extension configuration: one JSON file per extension under the config directory, at .pi/po-runner.json. It is read only when the project is trusted, and an unrecognized or out-of-range key is reported rather than silently ignored.

{
  "maxResultBytes": 16384,
  "maxResultLines": 200,
  "successMaxResultBytes": 2048,
  "maxResultTokens": 2048,
  "successMaxResultTokens": 512,
  "headRatio": 0.2,
  "errorPatterns": ["^FEHLER", "^\\s*FAILED\\b"],
  "errorMaxEntries": 10,
  "errorMaxVariantsJoined": 6,
  "errorBudgetRatio": 0.2
}

errorPatterns extends the built-in matcher rather than replacing it, which is what makes output it does not recognize reachable. Ceilings apply to every numeric key so a checked-in file cannot flood the model's context.

Commands inherit the Po process environment and the operating system user's privileges. Logs may contain prompts, source, command output, credentials, or other secrets; restrict access and retention accordingly.

Host-wide resource budget

The headless Po server shares one heavy-job queue across its sessions. Known noninteractive build, test, lint, and typecheck commands acquire a slot before launching. The default is two concurrent jobs; set PO_RUNNER_MAX_HEAVY_JOBS to a positive integer in the server's environment to change it. The waiting queue is bounded to 128 jobs. Cancellation removes queued work without launching it.

A background promotion keeps its slot until the process exits. Closing one session does not release another session's slot. Interactive commands and ordinary inspection commands bypass this queue. This is admission for recognized command forms, not an operating-system CPU quota or a parser for arbitrary shell scripts.

For noninteractive native, RMUX, and tmux launches, unset child environment values default to NX_NATIVE_COMMAND_RUNNER=false, NX_TUI=false, NX_PARALLEL=2, and VITEST_MAX_WORKERS=2. Explicit environment values still win; interactive launches keep their original settings.

The server records changed queue counts as po_server.execution_budget events with the host PID, running jobs, queued jobs, and limit. These are not per-session memory measurements.

Native artifacts

Runner selects the matching optional dependencies automatically:

Host RMUX package RTK package
macOS arm64 @agimon-ai/po-runner-rmux-darwin-arm64 @agimon-ai/po-runner-rtk-darwin-arm64
macOS x64 @agimon-ai/po-runner-rmux-darwin-x64 @agimon-ai/po-runner-rtk-darwin-x64
Linux arm64 @agimon-ai/po-runner-rmux-linux-arm64 @agimon-ai/po-runner-rtk-linux-arm64
Linux x64 @agimon-ai/po-runner-rmux-linux-x64 @agimon-ai/po-runner-rtk-linux-x64

Do not install platform packages directly. They contain native executables and export only package metadata. RMUX is dual-licensed under MIT or Apache-2.0. RTK v0.45.0 is licensed under Apache-2.0.

Public API

import {
  createRunnerDependencies,
  DEFAULT_BG_THRESHOLD_MS,
  DEFAULT_LOG_MAX_BYTES,
  DEFAULT_LOG_TTL_MS,
  RmuxBackend,
  rtkPackageForTarget,
} from '@agimon-ai/po-runner';

Declared subpaths expose process supervision, configuration, tools, and response envelopes for host authors.

Development

pnpm build
pnpm typecheck
pnpm test
pnpm lint

Maintained by Agimon.

License

Business Source License 1.1: free for personal use, organisations under USD 1M revenue, non-profits and education. See LICENSE and https://agimon.ai/licensing.

Extension lifecycle and source layout

Public capabilities use flat kebab-case subpaths such as runner-dependencies, rmux-backend, runner-paths, bash-tool, and response-envelope. Flat src/exports/ files only forward these capabilities. Executable bin/* paths remain unchanged. The former nested service aliases and container entry are removed.

Native session ownership

The Po server plugin owns runner startup, history retention, background completion notifications, and successful-compaction recovery. The standalone po-runner command remains available. Runner project settings retain the existing headless bridge's trusted-project assumption, validation, and environment-over-project precedence. Optional runner activity toggles do not restart the retained supervisor.

Package launches and trusted owner routing

readPoRunnerLaunch(context) from @agimon-ai/po-runner/launch returns the po/runner-launch service, whose start, get, list, stop, logs and onChange always act on the calling session. Its optional forOwner() takes no argument and returns the same six operations against the session's trusted owner: the workflow run's ownerSessionId when the session was bootstrapped as a workflow step, otherwise the session itself. Callers cannot name an owner. Check typeof launch.forOwner === 'function' before use, because builds that predate it lack it.

Routed starts never wake the owner's agent (wakeOnExit is forced off) and register with the owner's supervision, so a runner survives its workflow child and stops with the owner. Releasing the child cancels only its own pending starts before handoff and its own subscriptions. A missing or other-build owner fails with owner unavailable before anything spawns. Closing owners also refuse launches, with a disposed or aborted error if still registered. There is no fallback to the calling session. The bash tool, the runner namespace and the top-level launch methods stay session-local.