@pwguler/pi-loop
A pi extension for loop.
Package details
Install @pwguler/pi-loop from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@pwguler/pi-loop- Package
@pwguler/pi-loop- Version
0.1.1- Published
- Sep 6, 2026
- Downloads
- 261/mo · 261/wk
- Author
- pwguler
- License
- MIT
- Types
- extension
- Size
- 41.8 KB
- Dependencies
- 0 dependencies · 5 peers
Pi manifest JSON
{
"extensions": [
"./extensions/pi-loop/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-loop
A pi extension for loop.
Install
From npm:
pi install npm:@pwguler/pi-loop
Or straight from GitHub (no npm account needed):
pi install git:github.com/pwguler/pi-loop
Pin a release so updates don't move under you:
pi install git:github.com/pwguler/pi-loop@v0.1.0
To update a git-installed package later, install the next tag the same way. Start or reload pi, then /loop is available. To try it without installing, use pi -e git:github.com/pwguler/pi-loop.
For development, install the checkout by path; the code on disk is the code that runs and pi update --all does not touch it:
pi install ./pi-loop
Usage
/loop picker: one row per loop; Enter opens the loop, Esc closes
/loop list the same picker
/loop [flags] <text with an interval phrase> [flags]
/loop stop <name>
/loop pause <name>
/loop resume <name>
In the picker, Enter on a loop opens its detail panel (interval, prompt, next, fires, status, bounds, last error) drawn like pi's own dialogs, with single keys along the bottom:
p pause x stop escape/ctrl+c back (p resume when the loop is paused)
x asks first. Each key runs the typed command, so the state file, the notice, and the footer update the same way, and the list comes back showing the new state. Without a UI (pi -p, RPC), both forms print one text line per loop instead.
The interval phrase is cut out of the text; what remains is the prompt.
/loop 5m check the build
/loop every 30 minutes review the open PRs
/loop check the build hourly
/loop check the build, every 2 hours, and report
/loop run the smoke test every 1h30m --name smoke --max 6
/loop every 50 min @prompt.md
- Interval forms:
5m,5 min,2 hours,1d,hourly,daily,every hour,each day,1h30m,1 hour 30 minutes. Minimum1m. - At the head or the tail of the text any form counts. In the middle only an
every/eachphrase counts, sowait 5 minutes then retryis prompt text, not an interval. Two phrases reject; say one. - Flags:
--name <n>,--max <n>,--until <ISO|HH:mm>. Head or tail, never inside the prompt. - The loop fires once on create, then every interval, counted from the fire itself. A tick that comes due while the agent is busy fires once when the agent settles.
@pathreads the file at every fire, relative to the cwd. A missing or empty file skips that fire and shows the error in/loop list; the loop stays alive.--namedefaults toloop-<k>with the lowest freek.--max nremoves the loop after its n-th fire.--untilremoves it at that time;HH:mmmeans the next such local time.- Pause keeps the counter. Resume counts the next due from the resume moment.
Status line
While loops exist, one line in pi's footer:
1 active · next loop-1 02:42 steady: counts and the earliest due active loop
2 active, 1 paused · due fast fast is overdue and the agent is busy; it fires on settle
2 active · fired fast #6 for 5s after a fire, then back to next
1 paused everything paused
2 loops · owned by pid 4242 this session is not the owner
3 active · next b 10:05 · 1 error some loop has a last error; /loop list has the message
The count carries the state in the theme's colors: success steady, warning due, accent fired, dim all paused, muted non-owner. The loop name is accent, the error suffix error, separators and times dim. No loops, no line. The footer is written through ctx.ui.setStatus only, and only when the text changes.
Each fire is one user message:
[loop <name> #<fires> <YYYY-MM-DD HH:mm>]
<prompt text>
That is the stored text and what the model sees. On screen the whole message is drawn as one heading line, <name> #<fires> · <YYYY-MM-DD HH:mm>, through pi's display-only Markdown transformer; the prompt text is not repeated in the transcript. The loop's detail shows it.
State
<cwd>/.pi-loop/loops.json every loop: name, intervalMs, prompt source, dueAt, fires, paused, bounds, last error
<cwd>/.pi-loop/owner.json {pid, sessionId, claimedAt}: the one session in this cwd that fires
Only the owner fires. A second pi session in the same cwd lists the loops as owned by pid <n> and fires nothing. When the owner pid is dead, the next session takes over. Owner shutdown removes owner.json. A restart in the same cwd resumes every non-stopped loop; a loop whose due time passed while pi was down fires once at the first idle moment, then continues on its interval from that fire.
Loop state is never read from the conversation. Compaction, /tree, and forks do not change a counter or a due time. Sessions without a UI (pi -p, RPC) never claim ownership and never fire.
Verification
bun test # behavior against a mock pi host: firing, persistence, ownership, bounds, footer, picker
bun run typecheck # tsc; also proves pi's ExtensionAPI satisfies the host surface used
grep -rnE 'cache_control|"ttl"|\bttl\s*[:=]|pi\.on\("context"|systemPrompt' extensions ; test $? -eq 1
grep -rn 'sendUserMessage\|sendMessage' extensions | grep -v sendUserMessage ; test $? -eq 1
The two greps hold the line the extension exists for: no cache markers, no TTL, no context hook, no system prompt change, and sendUserMessage as the only path that adds text to the conversation.
License
MIT