@bramburn/pi-loop

A pi extension for cron/event-based agent re-wake loops and background process monitoring.

Packages

Package details

extension

Install @bramburn/pi-loop from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@bramburn/pi-loop
Package
@bramburn/pi-loop
Version
2.6.3
Published
Aug 19, 2026
Downloads
2,375/mo · 766/wk
Author
bramburn
License
MIT
Types
extension
Size
1.9 MB
Dependencies
2 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./src/index.ts"
  ]
}

Security note

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

README

Install

pi install npm:@bramburn/pi-loop

Quick start

LoopCreate trigger="5m" prompt="Check if the build passed"
LoopCreate trigger="tool_execution_start" prompt="Log the tool being used" triggerType="event"
LoopList
/loop           (then "View loops" → "x Delete" to delete a loop)

Widget (v2.0)

pi-loop renders an above-editor widget showing every loop, monitor, and task at a glance:

  pi-loop · 3 loops · 1 monitor · 3 tasks
    ├─ * #1 [active] check deploy status (cron: */5 * * * *) → firing (2s ago)
    ├─ * #2 [active] tail logs (event: tool_execution_start)
    ├─ * #3 [active] weekly report (cron: 0 9 * * 1 · auto-task)
    ├─ > #5 [running] npm test --watch (42 lines, 3m)
  └─ 3 tasks: active: wire validator into tests

Render snapshots (text-based, generated from renderWidgetLines):

When a loop fires, the row shows → firing (Ns ago) for 5 seconds, refreshing every second. Press Ctrl+Shift+L for a scrollable loop list overlay. Press Escape during a long-running fire to skip or cancel.

Commands

/loop [interval] [prompt] — interactive loop creation.

/loop                         # menu
/loop 5m check the deploy     # 5-minute cron loop

/loop-resume <id> — re-arm a stored loop by ID and re-add it to the trigger system. Use this after a session/process restart when a stored event/hybrid loop's trigger subscription was lost. Idempotent: re-arming an already-active loop just refreshes the trigger.

/loop-settings — open the unified settings TUI editor (loopScope, taskScope, debug, autoClear, sortOrder, hiddenAt, maxVisible, showAll, taskThreshold). Includes a Shared loops sub-screen for promoting loops to the cross-repo shared store and adopting shared loops into the current project.

/loop-resume 5        # re-arm loop #5 by id
/loop-resume          # open a single-select picker of all stored loops

/loop-resume (no args) — open a simple picker listing every stored loop as * #N [status] prompt (trigger). Pick a row to re-arm it, or < Back to exit without changing anything. Each terminal reads and writes its own .pi/loops/bindings-<sessionId>.json so parallel sessions do not interfere.

/loop-fire [id] — fire a stored loop's prompt as a fresh user message into the chat. Use it to manually trigger a loop's prompt out-of-band without waiting for its trigger to fire. No args opens a picker over all stored loops (active and paused); with an id, that loop fires directly. Sends entry.prompt only (no [pi-loop] wrapper, no loop:fire event, no fireCount bump). When the agent is idle, the message triggers a turn immediately; when the agent is busy, the message is queued with deliverAs: "followUp".

/loop-fire            # picker over all stored loops
/loop-fire 5          # fire loop #5's prompt directly

/loop-subagent <interval> <prompt> [flags] — create a sub-agent loop. Each fire spawns a fresh child pi process with its own context window; the parent only sees a one-line summary.

/loop-subagent 30m "check upstream pi-loop releases" \
  --goal "find a release newer than 2.5.0 and report the diff" \
  --success-criteria "found a newer release" \
  --failure-criteria "404 or network error" \
  --max-tokens 50000 --max-iterations 5

# Optional flags: --goal, --success-criteria, --failure-criteria,
#                 --state-file, --model, --max-tokens, --max-iterations,
#                 --iteration-timeout

Tools

Tool What it does
LoopCreate Schedule a prompt on a cron timer, a pi event, or both with debounce
LoopUpdate Update progress for a dynamic goal loop (self-paced mode)
LoopList Show active loops with IDs, triggers, and next-fire times
LoopPause Pause a loop without removing it (preserves history, trigger, ID)
LoopResume Resume a paused loop (re-adds the trigger; does not touch session bindings)
LoopInspect Read the latest iteration summary (status, tokens, cost, preview) for a loop — used by the agent to read its own sub-agent runs without opening files

Note: Loop deletion is intentionally not an LLM-callable tool. Use /loop → View loops → x Delete to delete a loop. The LLM can pause, resume, and update loops, but only the user can delete. | MonitorCreate | (retired — see Retired tools) | | MonitorList | (retired) | | MonitorStop | (retired) | | TaskCreate | (retired — see Retired tools) | | TaskList | (retired) | | TaskUpdate | (retired) | | TaskDelete | (retired) |

Trigger types: cron (5m, 1h, 0 9 * * 1-5), event (any pi event source), or hybrid (both, debounced).

Tasks

With pi-tasks

Works with @tintinweb/pi-tasks. Pass autoTask: true on LoopCreate and each loop fire auto-creates a tracked task. Detection happens over pi's event bus — no manual wiring.

Without pi-tasks

If pi-tasks does not respond during startup detection, pi-loop registers a native fallback task system for the session:

  • session- or project-scoped task files under .pi/tasks/ per settings.taskScope
  • TaskCreate, TaskList, TaskUpdate, TaskDelete
  • /tasks interactive viewer
  • above-editor widget task tracking (replaces the v1.x status-line task summary)

This fallback is session-sticky: pi-loop decides once at startup whether pi-tasks or native tasks own task management for that session.

Sub-agent execution mode (v2.5)

When a loop's isolation is set to "sub-agent", each fire spawns a fresh child pi process with its own context window, runs the prompt in isolation, and returns only a one-line summary to the parent. This is the right shape when a recurring check is too long, too noisy, or too stateful to share the parent's context.

Use it for: upstream monitoring, periodic refactors, scheduled test runs with result evaluation, any "run a task and tell me one line" loop where the parent should not see the full transcript.

Quick start

/loop-subagent 30m "check upstream pi-loop releases" \
  --goal "find a release newer than 2.5.0 and report the diff" \
  --success-criteria "found a newer release" \
  --failure-criteria "404 or network error" \
  --max-tokens 50000 --max-iterations 5

The child writes its session file, stdout/stderr, and a result.md to <loopScope>/sub-agent-results/<loopId>/iter-<N>/. The parent loop entry accumulates cumulativeTokens and cumulativeCostUsd across iterations and is auto-paused after 3 consecutive failures.

What the parent sees

Each fire returns a single tiered line such as:

[pi-loop sub-agent] #3 iter-7 SUCCESS · "found 2.5.3, 2 new commits, 1 breaking" · 12,348 tok · $0.018

Use LoopInspect({ loopId, iterId? }) to read the structured summary (status, tokens, cost, preview, error). For the full result.md and the child's session file, the inspector returns the on-disk paths so the agent can open them directly.

How it differs from a regular loop

In-process loop (isolation: "in-process") Sub-agent loop (isolation: "sub-agent")
Where it runs Parent's turn Fresh child pi process
Context cost per fire Full turn + tool calls in parent context One summary line in parent context
Per-iteration artefacts entry.dynamic.state in .pi/loops/ <loopScope>/sub-agent-results/<id>/iter-N/ (session file, result.md, stdout/stderr)
Cost / token tracking None Per-iteration + cumulative on the loop entry
Self-evaluation LoopUpdate({ status, state, metrics, doneCriteria }) Regex match against result.md (success / failure criteria)
Failure handling maxFires budget Auto-pause after 3 consecutive failures

Fields

LoopCreate / LoopUpdate field Effect
isolation "in-process" (default) or "sub-agent"
goal Long-form description of the loop's purpose (helps the child stay on-task)
successCriteria Regex matched against result.md; fires match → SUCCEEDED
failureCriteria Regex matched against result.md; fires match → FAILED (wins over success)
stateFile Optional path the child reads/writes across iterations
subAgent.model Per-loop model override
subAgent.maxTokens Hard cap on tokens per iteration
subAgent.maxIterations Hard cap on iterations before the loop is auto-paused
subAgent.iterationTimeoutMs Per-iteration wall-clock cap (default from settings)

Session-wide defaults live under the subAgent block in .pi/pi-loop-settings.json (see Configuration).

Restart safety

Sub-agent iterations are durable on disk. After a parent restart, result-watcher walks the on-disk result directories and finalises any in-flight iterations as orphaned so the next scheduler tick can proceed without losing state.

Status line

pi-loop keeps a compact persistent status line in the TUI.

When active work exists, it shows a single focus-friendly line such as:

1 loop · 1 monitor
2 tasks | active: Fix deploy polling
1 loop · 2 monitors · 3 tasks | next: Update README

When no loops, monitors, or native tasks are active, the status line clears completely.

Only task counts and the single active/next task are shown there so attention stays on what is currently happening. Use LoopList, MonitorList, and /tasks for detail.

Configuration

All configuration lives in .pi/pi-loop-settings.json — see userflow/settings-v2.md for the full schema and migration guide. The v2.0 release removes the v1.x PI_LOOP_* environment variables (see CHANGELOG.md for the clean break). To change loopScope, taskScope, debug logging, auto-clear behaviour, sort order, or backlog threshold, run /loop-settings (no environment variables needed).

subAgent block (v2.5+, sub-agent execution mode)

Session-wide defaults for sub-agent loops. Edit the JSON directly (the cyclic TUI editor shows the block as a read-only summary).

Field Effect Default
defaultIsolation Default isolation for loops created without an explicit value "in-process"
activeIterationsMax Concurrency cap (in-flight sub-agent iterations in this session) 4
defaultIterationTimeoutMs Per-iteration wall-clock cap when a loop doesn't override 600000 (10 min)
defaultIterationTokenBudget Default per-iteration soft token budget ({ in, out } in tokens) { in: 30000, out: 6000 }
piBinary Path / name of the pi binary the child process spawns "pi"
envOverrides Extra env vars to inject into every child {}
registerBackgroundWorkProvider Register the runtime as a background-work provider on the host true
honorCapabilityCeiling If true, the child inherits the parent's capability ceiling true
criticalInterruptsAll If true, a critical-priority fire can preempt in-flight iterations false
showCostInStatusLine Show accumulated sub-agent cost in the widget status line true
useLlmEvaluator Use the LLM-evaluator (in addition to the regex evaluator) when a result.md is present false

A one-shot migration (src/migration/v2-to-v2.5.ts) inserts the default subAgent block into existing settings files on first v2.5+ load — idempotent.

Sentry Effect Default
SENTRY_DSN Enable anonymous crash + log reporting (Sentry). Set to your project DSN to opt in. unset → telemetry disabled
SENTRY_ENVIRONMENT Environment tag for events (e.g. production, development) development
SENTRY_TRACES_SAMPLE_RATE Performance transaction sample rate (0.01.0) 0.1
SENTRY_CAPTURE_LOGS Pipe debug() output into Sentry logs true
SENTRY_DEBUG Verbose Sentry SDK debug logging to stderr false

In project scope (default), loop and task files are saved to .pi/loops/loops.json and .pi/tasks/tasks.json so they survive across chat sessions and process restarts in the same repository — mirroring pi-goal-x's .pi/goals/ pattern. In memory scope nothing persists to disk.

Recommended scope policy

loopScope: project is the default and best balance for normal use.

  • project is the default: loops and tasks persist across sessions and process restarts in the same repo, so a 5m cron loop survives closing and reopening pi.
  • session is best when you want each pi session isolated (e.g. concurrent worktrees, throwaway explorations). Loops disappear when the session ID changes.
  • memory is best for disposable scratch work, tests, or situations where you explicitly do not want any persisted loop/task state.

Re-arming loops after a restart

Cron loops re-arm themselves automatically only if they are bound to this session (see Per-Session Bindings below). Event/hybrid loops do not auto-re-arm their trigger subscriptions — use /loop-resume <id> to re-bind them.

Per-session bindings (multi-terminal parallelism)

If you run two or three pi terminals in the same repo and want each one to fire a different subset of loops, use the bindings mechanism:

  • Each terminal has its own .pi/loops/bindings-<sessionId>.json file listing the loop IDs it has chosen to arm.
  • A fresh session (no bindings file yet) starts with zero loops armed (strict isolation). Run /loop-resume <id> to bind loops for this terminal.
  • Terminal A binding loop #5 does not cause Terminal B to fire #5, because each session reads only its own bindings file and its trigger subscriptions are process-local.

This is a deliberate behavior change from previous versions, where every session armed every active loop on start.

Crash analytics (opt-in)

pi-loop integrates with Sentry for anonymous crash analytics and structured log capture. Telemetry is strictly opt-in — leaving SENTRY_DSN unset (the default) makes every Sentry callsite a no-op.

To enable reporting:

  1. Apply for Sentry for Open Source and create a project for @bramburn/pi-loop. Once approved, you'll receive a DSN like https://publickey@o1234567.ingest.us.sentry.io/1234567.
  2. Set SENTRY_DSN in your shell environment (or add it to .env — see .env.example):
    export SENTRY_DSN=https://publickey@o1234567.ingest.us.sentry.io/1234567
    
  3. Restart pi. The extension will initialize Sentry on load and start capturing:
    • Unhandled exceptions and unhandled promise rejections
    • Tool errors via the wrapToolExecute wrapper around every pi.registerTool call
    • Breadcrumbs on session_switch, loop_fire, and tool entry
    • Optional debug() log output (controlled by SENTRY_CAPTURE_LOGS)

All events are passed through a beforeSend hook that strips:

  • Absolute filesystem paths (Windows + Unix user dirs)
  • process.env.* references
  • Sentry DSN literals
  • Values under sensitive keys (prompt, message, text, body, content, description)
  • Stack-frame filename/abs_path fields

The DSN itself is a public client identifier (it's shipped to browsers in Sentry's own SDK) — it only grants permission to send events, not to read them. Even so, the DSN is never committed to this repository. See .env.example for the full telemetry env-var matrix.

For the wider design rationale see docs/SENTRY.md.

Retired tools

The Loop family is now active (see Status and Quick start). The following tools and commands remain present in source but not registered in src/index.ts to keep the extension footprint minimal:

File What's in it
src/tools/monitor-tools.ts MonitorCreate, MonitorList, MonitorStop, MonitorDelete
src/tools/native-task-tools.ts TaskCreate, TaskList, TaskGet, TaskClaim, TaskHeartbeat, TaskUpdate, TaskDelete, TaskPrune
src/tools/workflow-tools.ts Workflow step-execution tools
src/commands/monitors-command.ts /monitors command
src/commands/tasks-command.ts /tasks command

The infrastructure that would back these tools is still in place: src/monitor-manager.ts, src/task-store.ts, src/runtime/task-*.ts coordinators. To re-enable any of them, add the matching register*() call to src/index.ts and provide the runtime stubs that the registered tools depend on.

Limits

25 active loops, 25 running monitors. Recurring loops expire after 7 days.

Development

npm run typecheck
npm run lint
npm test
npm run build

See CONTRIBUTING.md.

License

MIT — LICENSE