@trevonistrevon/pi-loop 0.6.1 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.2](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.1...pi-loop-v0.6.2) (2026-07-17)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **deps:** require patched pi runtime ([459a44a](https://github.com/trvon/pi-loop/commit/459a44af75dce174e53d70ef0427ca773845a912))
9
+ * **loop:** start dynamic goals immediately ([790959c](https://github.com/trvon/pi-loop/commit/790959c02d3c2b301526f780122aacf533429bc5))
10
+ * **monitor:** wake after timeouts ([478535b](https://github.com/trvon/pi-loop/commit/478535b3c837e74074dc022ea35ec3ba9b18f533))
11
+
3
12
  ## [0.6.1](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.0...pi-loop-v0.6.1) (2026-07-16)
4
13
 
5
14
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <h1 align="center">@trevonistrevon/pi-loop</h1>
3
- <h6 align="center">Cron, event, and dynamic goal loops for the pi coding agent. Background monitors, scheduled re-wakes, pi-tasks integration, and native task fallback.</h6>
3
+ <h6 align="center">Scheduled and event-driven agent re-wakes for pi, with dynamic goals and background process monitoring.</h6>
4
4
  </p>
5
5
 
6
6
  ## Install
@@ -11,177 +11,50 @@ pi install npm:@trevonistrevon/pi-loop
11
11
 
12
12
  ## Quick start
13
13
 
14
- ```text
15
- LoopCreate trigger="5m" prompt="Check if the build passed"
16
- LoopCreate trigger="tool_execution_start" prompt="Log the tool being used" triggerType="event"
17
- LoopList
18
- LoopDelete id="1"
19
- ```
14
+ Create scheduled, event-driven, or self-paced loops:
20
15
 
21
16
  ```text
22
- MonitorCreate command="tail -n0 -f build.log" description="Watch build"
23
- MonitorCreate command="python train.py" onDone="Analyze results and report best loss"
24
- MonitorList
25
- MonitorStop monitorId="1"
17
+ /loop 5m check the deploy
18
+ /loop event tasks:created process the backlog
19
+ /loop finish the release
26
20
  ```
27
21
 
28
- When `pi-tasks` is not installed, `pi-loop` also exposes native task tools after startup detection:
22
+ Or use the tools directly:
29
23
 
30
24
  ```text
31
- TaskCreate subject="Fix deploy polling" description="Switch deploy check to event-driven loop"
32
- TaskList
33
- TaskUpdate id="1" status="in_progress"
34
- TaskDelete id="1"
25
+ LoopCreate trigger="5m" prompt="Check if the build passed" maxFires=12
26
+ LoopCreate trigger="tool_execution_start" prompt="Log the tool" triggerType="event" recurring=true
27
+ LoopList
28
+ LoopDelete id="1"
35
29
  ```
36
30
 
37
- ## Commands
38
-
39
- `/loop` creates scheduled, event-triggered, or self-paced dynamic goal loops.
31
+ Run work in the background and wake the agent when it succeeds, fails, or times out:
40
32
 
41
33
  ```text
42
- /loop # menu
43
- /loop 5m check the deploy # 5-minute cron loop
44
- /loop event tasks:created process backlog # event loop
45
- /loop finish the release # dynamic goal loop
34
+ MonitorCreate command="python train.py" onDone="Analyze results and report best loss"
35
+ MonitorList
36
+ MonitorStop monitorId="1"
46
37
  ```
47
38
 
48
- Dynamic goal loops wake immediately when the agent is idle. After each iteration, the agent calls `LoopUpdate` with one of:
49
-
50
- - `status="continue"` to save progress and wake again when idle
51
- - `status="continue" nextInterval="3m"` to schedule a timed next wake
52
- - `status="paused"` when blocked
53
- - `status="completed"` to finish and delete the loop
39
+ ## What it provides
54
40
 
55
- Paused dynamic loops can be resumed from the `/loop` menu. Dynamic loops recover their wake after a process restart or session switch if an in-memory notification was lost.
41
+ - Cron, event, hybrid, and dynamic goal loops
42
+ - Idle-safe agent re-wakes with dynamic-loop restart/session-switch recovery
43
+ - Background command monitoring with buffered output and `onDone` wakes
44
+ - Optional `pi-tasks` integration and a native task fallback
45
+ - Session-isolated persistence and a compact TUI status line
56
46
 
57
- `/tasks` interactive native task viewer/manager, only registered when `pi-tasks` is absent.
47
+ ## Commands and tools
58
48
 
59
- ```text
60
- /tasks # open native task viewer
61
- /tasks Write README updates # quick-create native task
62
- ```
63
-
64
- ## Tools
65
-
66
- | Tool | What it does |
49
+ | Surface | Purpose |
67
50
  |---|---|
68
- | `LoopCreate` | Schedule a prompt on a cron timer, a pi event, or both with debounce |
69
- | `LoopList` | Show active loops with IDs, triggers, and next-fire times |
70
- | `LoopUpdate` | Continue, pause, or complete a dynamic goal loop and save its progress |
71
- | `LoopDelete` | Delete or pause a loop |
72
- | `MonitorCreate` | Run a background command, stream output as `monitor:output` events. Use `onDone` for auto-notify on completion |
73
- | `MonitorList` | Show monitors with status, uptime, and output line count |
74
- | `MonitorStop` | Stop a monitor (SIGTERM → 5s → SIGKILL) |
75
- | `TaskCreate` | Create a native fallback task when `pi-tasks` is absent |
76
- | `TaskList` | List native fallback tasks |
77
- | `TaskUpdate` | Update native fallback task status/details |
78
- | `TaskDelete` | Delete a native fallback task |
79
-
80
- Trigger types: `cron` (`5m`, `1h`, `0 9 * * 1-5`), `event` (any pi event source), `hybrid` (both, debounced), or `dynamic` (self-paced goal loops created with `/loop <goal>`).
81
-
82
- ## Tasks
83
-
84
- ### With `pi-tasks`
85
-
86
- Works with [@tintinweb/pi-tasks](https://github.com/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.
87
-
88
- ### Without `pi-tasks`
89
-
90
- If `pi-tasks` does not respond during startup detection, `pi-loop` registers a native fallback task system for the session:
91
-
92
- - session- or project-scoped task files under `.pi/tasks/` depending on `PI_LOOP_SCOPE`
93
- - `TaskCreate`, `TaskList`, `TaskUpdate`, `TaskDelete`
94
- - `/tasks` interactive viewer
95
- - compact status-line task tracking
96
- - native task RPC replies on `tasks:rpc:ping`, `tasks:rpc:create`, `tasks:rpc:pending`, `tasks:rpc:clean`, and `tasks:rpc:update`
97
-
98
- This fallback is session-sticky: `pi-loop` decides once at startup whether `pi-tasks` or native tasks own task management for that session.
99
-
100
- ### Task and backlog events
101
-
102
- `pi-loop` emits native task lifecycle events that other extensions can consume directly:
103
-
104
- - `tasks:created`
105
- - `tasks:started`
106
- - `tasks:completed`
107
- - `tasks:reopened`
108
- - `tasks:updated`
109
- - `tasks:deleted`
110
-
111
- Payloads carry `previousStatus`. Transition events (`tasks:started` /
112
- `tasks:completed` / `tasks:reopened`) report the status before the transition;
113
- `tasks:updated` (a details edit) reports the status current at edit time — so a
114
- combined status+details update never fabricates a second transition. (Changed
115
- in 0.6.0: the tool path previously reused the pre-transition status.)
116
- - `tasks:backlog_empty` — emitted when a task-backlog worker observes zero pending tasks and is about to auto-delete
117
- - `loops:autodeleted` — emitted for each loop that `pi-loop` auto-deletes, including backlog workers removed because the task queue drained
118
-
119
- ### Cross-extension task RPC
120
-
121
- Other extensions can create and manage native tasks without importing `pi-loop` internals. `pi-loop` answers `ping` over `pi.events` from extension init; the other verbs unlock once the pi-tasks detection probe settles (a few seconds at most, immediate when no provider replies race it). The whole server stands down (silent no-op) once an external `pi-tasks` is detected:
122
-
123
- | Channel | Request | Reply |
124
- |---|---|---|
125
- | `tasks:rpc:ping` | `{}` | `{ version, provider }` |
126
- | `tasks:rpc:pending` | `{}` | `{ pending }` |
127
- | `tasks:rpc:create` | `{ subject, description, metadata? }` | `{ id, task }` |
128
- | `tasks:rpc:clean` | `{}` | `{ pruned }` |
129
- | `tasks:rpc:update` | `{ id, status?, subject?, description? }` | `{ task }` |
130
-
131
- Every request/reply pair follows the same contract: emit `{ requestId, ...params }` on the channel, receive an envelope on `<channel>:reply:<requestId>` — `{ success: true, data }` or `{ success: false, error }`.
132
-
133
- `@trevonistrevon/pi-loop/api` exports typed channel constants and a client helper so consumers don't hand-roll the envelope:
134
-
135
- ```ts
136
- import { TASKS_RPC, rpcCall } from "@trevonistrevon/pi-loop/api";
137
-
138
- const { id, task } = await rpcCall(pi.events, TASKS_RPC.create, {
139
- subject: "Fix deploy polling",
140
- description: "Switch deploy check to event-driven loop",
141
- });
142
- ```
143
-
144
- `rpcCall` rejects on failure or timeout instead of returning a sentinel; wrap it in `try/catch` if you want fallback behavior. Import only from `@trevonistrevon/pi-loop/api` — the package's `exports` map blocks deep `src/` imports.
145
-
146
- ## Status line
147
-
148
- `pi-loop` keeps a compact persistent status line in the TUI.
149
-
150
- When active work exists, it shows a single focus-friendly line such as:
151
-
152
- ```text
153
- 1 loop · 1 monitor
154
- 2 tasks | active: Fix deploy polling
155
- 1 loop · 2 monitors · 3 tasks | next: Update README
156
- ```
157
-
158
- When no loops, monitors, or native tasks are active, the status line clears completely.
159
-
160
- 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.
161
-
162
- ## Configuration
163
-
164
- | Variable | Effect | Default |
165
- |---|---|---|
166
- | `PI_LOOP` | Store path override. `off` to disable, absolute or project-relative path | unset → derived from `PI_LOOP_SCOPE` |
167
- | `PI_LOOP_SCOPE` | `memory` (ephemeral), `session` (per-session file), `project` (shared) | `session` |
168
- | `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
169
-
170
- In `session` scope (default), loop and task files are saved per session ID (e.g. `.pi/loops/loops-<sessionId>.json` and `.pi/tasks/tasks-<sessionId>.json`) so concurrent sessions and worktree agents do not share state. In `memory` scope nothing persists to disk.
171
-
172
- ### Recommended scope policy
173
-
174
- Keep `PI_LOOP_SCOPE=session` as the default.
175
-
176
- - `session` is the best balance for normal use: it preserves loops/tasks across a session restart while isolating concurrent sessions and worktrees.
177
- - `memory` is best for disposable scratch work, tests, or situations where you explicitly do not want any persisted loop/task state.
178
- - `project` should be opt-in for intentionally shared automation, because it allows multiple sessions in the same repo to see the same persisted state.
179
-
180
-
181
-
182
- ## Limits
51
+ | `/loop` | Create or manage scheduled, event, and dynamic goal loops |
52
+ | `/tasks` | Manage native fallback tasks when `pi-tasks` is absent |
53
+ | `LoopCreate`, `LoopList`, `LoopUpdate`, `LoopDelete` | Create and control loops |
54
+ | `MonitorCreate`, `MonitorList`, `MonitorStop` | Run and inspect background commands |
55
+ | `TaskCreate`, `TaskList`, `TaskUpdate`, `TaskDelete` | Native fallback task management |
183
56
 
184
- 25 active loops, 25 running monitors. Recurring loops expire after 7 days.
57
+ See the [usage guide](./docs/USAGE_GUIDE.md) for trigger types, dynamic loop lifecycle, monitor behavior, task integration, configuration, events, and the public RPC API.
185
58
 
186
59
  ## Development
187
60
 
@@ -24,6 +24,7 @@ export interface LoopCommandOptions {
24
24
  getStore: () => LoopStoreLike;
25
25
  getTriggerSystem: () => TriggerSystemLike;
26
26
  updateWidget: () => void;
27
+ onDynamicLoopActivated?: (entry: LoopEntry) => void;
27
28
  }
28
29
  export declare function registerLoopCommand(options: LoopCommandOptions): void;
29
30
  export {};
@@ -29,7 +29,7 @@ function parseLoopCommandRoute(input) {
29
29
  return { type: "dynamic", goal: trimmed };
30
30
  }
31
31
  export function registerLoopCommand(options) {
32
- const { pi, getStore, getTriggerSystem, updateWidget } = options;
32
+ const { pi, getStore, getTriggerSystem, updateWidget, onDynamicLoopActivated } = options;
33
33
  function createCronLoop(ui, interval, prompt, notifyEvery) {
34
34
  let entry;
35
35
  try {
@@ -82,6 +82,7 @@ export function registerLoopCommand(options) {
82
82
  getTriggerSystem().add(entry);
83
83
  updateWidget();
84
84
  ui.notify(`Dynamic loop #${entry.id} created — ${goal.slice(0, 50)}`, "info");
85
+ onDynamicLoopActivated?.(entry);
85
86
  }
86
87
  async function viewLoops(ui) {
87
88
  const loops = getStore().list();
@@ -127,6 +128,8 @@ export function registerLoopCommand(options) {
127
128
  getTriggerSystem().add(resumed);
128
129
  updateWidget();
129
130
  ui.notify(`Loop #${entry.id} resumed`, "info");
131
+ if (resumed.trigger.type === "dynamic")
132
+ onDynamicLoopActivated?.(resumed);
130
133
  }
131
134
  }
132
135
  }
package/dist/index.js CHANGED
@@ -331,6 +331,9 @@ export default function (pi) {
331
331
  updateWidget: () => {
332
332
  widget.update();
333
333
  },
334
+ onDynamicLoopActivated: (entry) => {
335
+ onLoopFire(entry);
336
+ },
334
337
  });
335
338
  // ── Native task tools (only when pi-tasks is absent) ──
336
339
  // Only tool/command registration stays deferred: the tool names collide with
@@ -22,7 +22,7 @@ export declare class MonitorManager {
22
22
  create(command: string, description?: string, timeout?: number): MonitorEntry;
23
23
  get(id: string): MonitorEntry | undefined;
24
24
  list(): MonitorEntry[];
25
- stop(id: string): Promise<boolean>;
25
+ stop(id: string, reason?: "manual" | "timeout"): Promise<boolean>;
26
26
  onComplete(id: string, callback: () => void): boolean;
27
27
  getProcess(id: string): MonitorProcess | undefined;
28
28
  }
@@ -190,7 +190,7 @@ export class MonitorManager {
190
190
  if (timeout > 0) {
191
191
  setTimeout(() => {
192
192
  if (bp.entry.status === "running") {
193
- this.stop(id);
193
+ void this.stop(id, "timeout");
194
194
  }
195
195
  }, timeout);
196
196
  }
@@ -206,7 +206,7 @@ export class MonitorManager {
206
206
  .map(bp => bp.entry)
207
207
  .sort((a, b) => Number(a.id) - Number(b.id));
208
208
  }
209
- async stop(id) {
209
+ async stop(id, reason = "manual") {
210
210
  const bp = this.processes.get(id);
211
211
  if (!bp || bp.entry.status !== "running")
212
212
  return false;
@@ -218,7 +218,7 @@ export class MonitorManager {
218
218
  entityId: id,
219
219
  payload: {
220
220
  id,
221
- reason: "manual",
221
+ reason,
222
222
  },
223
223
  });
224
224
  this.schedulePrune(id);
@@ -236,6 +236,15 @@ export class MonitorManager {
236
236
  resolve();
237
237
  });
238
238
  });
239
+ if (reason === "timeout") {
240
+ this.pi.events.emit("monitor:error", {
241
+ monitorId: id,
242
+ error: `Timed out after ${bp.entry.timeout}ms`,
243
+ outputLines: bp.entry.outputLines,
244
+ });
245
+ for (const callback of bp.completionCallbacks)
246
+ callback();
247
+ }
239
248
  bp.completionCallbacks = [];
240
249
  for (const resolve of bp.waiters)
241
250
  resolve();
@@ -30,7 +30,7 @@ Default to MonitorCreate for any long-running or background work:\n- Watch a CI/
30
30
 
31
31
  ## onDone — auto-notify on completion
32
32
 
33
- Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fires when the process exits. The completion wake includes the exit code and output line count.\n\nExample: MonitorCreate command="python train.py" onDone="Check training results and report best loss"\nExample: MonitorCreate command="hut builds show 1769753" onDone="Analyze the build result and report status"`,
33
+ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fires when the process exits, fails, or times out. The completion wake lets the agent inspect the final status and buffered output.\n\nExample: MonitorCreate command="python train.py" onDone="Check training results and report best loss"\nExample: MonitorCreate command="hut builds show 1769753" onDone="Analyze the build result and report status"`,
34
34
  promptGuidelines: [
35
35
  "Default to MonitorCreate for any long-running or background command — releases the agent to keep working on other tasks in parallel.",
36
36
  "When the user asks to monitor CI builds, watch a build, check a remote job, or run an experiment, use MonitorCreate instead of inline bash/curl/wait.",
@@ -0,0 +1,185 @@
1
+ # pi-loop usage guide
2
+
3
+ `pi-loop` re-wakes a pi agent from schedules and events, runs self-paced goal loops, and monitors background commands. This guide covers the operational details; see the [README](../README.md) for installation and a quick start.
4
+
5
+ ## Loops
6
+
7
+ ### Scheduled loops
8
+
9
+ Use `/loop <interval> <prompt>` or `LoopCreate`:
10
+
11
+ ```text
12
+ /loop 5m check the deploy
13
+ LoopCreate trigger="0 9 * * 1-5" prompt="Review weekday alerts" maxFires=10
14
+ ```
15
+
16
+ Intervals such as `5m`, `2h`, and `1d` are converted to cron expressions. Full five-field cron expressions are also accepted. Cron and hybrid loops track their next fire time and deliver only when the agent is idle.
17
+
18
+ Use `maxFires` for polling or other bounded work so a loop cannot run indefinitely. Recurring loops expire after seven days.
19
+
20
+ ### Event loops
21
+
22
+ Event loops react to pi events instead of polling:
23
+
24
+ ```text
25
+ /loop event tasks:created process the backlog
26
+ LoopCreate trigger="tool_execution_end" prompt="Review the completed tool call" triggerType="event" recurring=true
27
+ ```
28
+
29
+ `LoopCreate` event loops are one-shot by default. Set `recurring=true` to keep listening. Prefer events when a relevant source exists; use hybrid triggers when an event needs a scheduled safety net.
30
+
31
+ ### Hybrid loops
32
+
33
+ A hybrid trigger combines cron and event delivery with a debounce window:
34
+
35
+ ```text
36
+ LoopCreate trigger="cron:5m event:tasks:created" prompt="Process pending tasks" triggerType="hybrid" debounceMs=30000
37
+ ```
38
+
39
+ ### Dynamic goal loops
40
+
41
+ Free text passed to `/loop` creates a self-paced dynamic goal:
42
+
43
+ ```text
44
+ /loop finish the release
45
+ ```
46
+
47
+ The first wake is immediate when the agent is idle. After each iteration, the agent calls `LoopUpdate`:
48
+
49
+ ```text
50
+ LoopUpdate id="1" status="continue" state="Tests pass; reviewing package"
51
+ LoopUpdate id="1" status="continue" nextInterval="3m"
52
+ LoopUpdate id="1" status="paused" state="Waiting for credentials"
53
+ LoopUpdate id="1" status="completed"
54
+ ```
55
+
56
+ - `continue` saves progress and wakes again when idle.
57
+ - `continue` with `nextInterval` schedules a timed wake.
58
+ - `paused` preserves progress without firing.
59
+ - `completed` finishes and deletes the loop.
60
+
61
+ Paused dynamic loops can be resumed from the `/loop` menu. If an in-memory wake is lost during a restart or session switch, persisted dynamic state recovers it.
62
+
63
+ ### Inspecting and stopping loops
64
+
65
+ ```text
66
+ LoopList
67
+ LoopDelete id="1" action="pause"
68
+ LoopDelete id="1" action="delete"
69
+ ```
70
+
71
+ `LoopDelete` defaults to `action="delete"`.
72
+
73
+ ## Background monitors
74
+
75
+ `MonitorCreate` runs a shell command without blocking the agent:
76
+
77
+ ```text
78
+ MonitorCreate command="npm test" description="Run test suite" onDone="Inspect the result and fix failures"
79
+ ```
80
+
81
+ Output is buffered and emitted as `monitor:output`. A monitor finishes as one of:
82
+
83
+ - clean exit: emits `monitor:done`
84
+ - nonzero exit or spawn failure: emits `monitor:error`
85
+ - timeout: stops the process, emits `monitor:error`, and reports the timeout
86
+ - explicit `MonitorStop`: cancels the monitor without an `onDone` wake
87
+
88
+ Pass `onDone` whenever the agent should resume work after completion. Its one-shot wake fires on success, failure, or timeout. The default timeout is five minutes; use `timeout=0` to disable it.
89
+
90
+ ```text
91
+ MonitorList
92
+ MonitorStop monitorId="1"
93
+ ```
94
+
95
+ `MonitorList` includes status, exit code when available, output count, and the last five buffered lines. Finished monitors remain briefly available before pruning.
96
+
97
+ ## Task integration
98
+
99
+ ### With pi-tasks
100
+
101
+ When [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks) is available, `pi-loop` uses it over the task RPC channels. Set `autoTask=true` on `LoopCreate` to create a tracked task for each fire.
102
+
103
+ ### Native fallback
104
+
105
+ If `pi-tasks` does not answer during startup detection, `pi-loop` registers:
106
+
107
+ ```text
108
+ TaskCreate subject="Fix deploy polling" description="Replace polling with an event-driven loop"
109
+ TaskList
110
+ TaskUpdate id="1" status="in_progress"
111
+ TaskUpdate id="1" status="completed"
112
+ TaskDelete id="1"
113
+ ```
114
+
115
+ The native provider is selected for the session and exposes `/tasks`, compact status-line tracking, persisted task state, lifecycle events, and task RPC replies.
116
+
117
+ Set `taskBacklog=true` on a loop that processes existing pending tasks. Backlog workers bootstrap when tasks already exist and delete themselves when the queue drains. `autoTask` serves a different purpose: it creates a new task on each loop fire.
118
+
119
+ ## Events
120
+
121
+ Monitor events:
122
+
123
+ - `monitor:output`
124
+ - `monitor:done`
125
+ - `monitor:error`
126
+
127
+ Native task lifecycle events:
128
+
129
+ - `tasks:created`
130
+ - `tasks:started`
131
+ - `tasks:completed`
132
+ - `tasks:reopened`
133
+ - `tasks:updated`
134
+ - `tasks:deleted`
135
+ - `tasks:backlog_empty`
136
+ - `loops:autodeleted`
137
+
138
+ Task event payloads include `previousStatus`. Transition events report the status before the transition; details-only `tasks:updated` events report the status current at edit time.
139
+
140
+ ## Cross-extension task RPC
141
+
142
+ External consumers should import only from `@trevonistrevon/pi-loop/api`; deep `src/` imports are blocked by the package export map.
143
+
144
+ ```ts
145
+ import { TASKS_RPC, rpcCall } from "@trevonistrevon/pi-loop/api";
146
+
147
+ const { id, task } = await rpcCall(pi.events, TASKS_RPC.create, {
148
+ subject: "Fix deploy polling",
149
+ description: "Replace polling with an event-driven loop",
150
+ });
151
+ ```
152
+
153
+ `rpcCall` rejects on failure or timeout. The native provider supports:
154
+
155
+ | Channel | Request | Reply |
156
+ |---|---|---|
157
+ | `tasks:rpc:ping` | `{}` | `{ version, provider }` |
158
+ | `tasks:rpc:pending` | `{}` | `{ pending }` |
159
+ | `tasks:rpc:create` | `{ subject, description, metadata? }` | `{ id, task }` |
160
+ | `tasks:rpc:clean` | `{}` | `{ pruned }` |
161
+ | `tasks:rpc:update` | `{ id, status?, subject?, description? }` | `{ task }` |
162
+
163
+ Requests include `requestId`; replies arrive on `<channel>:reply:<requestId>` as `{ success: true, data }` or `{ success: false, error }`.
164
+
165
+ ## Persistence and configuration
166
+
167
+ | Variable | Effect | Default |
168
+ |---|---|---|
169
+ | `PI_LOOP` | Store path override; use `off` to disable persistence | derived from scope |
170
+ | `PI_LOOP_SCOPE` | `memory`, `session`, or `project` | `session` |
171
+ | `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
172
+
173
+ Scope behavior:
174
+
175
+ - `session`: persists loops and tasks per session ID while isolating concurrent sessions and worktrees
176
+ - `memory`: keeps all state ephemeral
177
+ - `project`: shares persisted automation across sessions in the repository
178
+
179
+ Session files live under `.pi/loops/` and `.pi/tasks/`. Keep `session` as the normal default; use `project` only when shared automation is intentional.
180
+
181
+ ## Status line and limits
182
+
183
+ The TUI status line summarizes active loops, monitors, and native tasks. Use `LoopList`, `MonitorList`, and `/tasks` for detail. The status clears when no work is active.
184
+
185
+ The runtime allows at most 25 active loops and 25 running monitors.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trevonistrevon/pi-loop",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "A pi extension for cron/event-based agent re-wake loops and background process monitoring.",
5
5
  "author": "trevonistrevon",
6
6
  "license": "MIT",
@@ -20,6 +20,7 @@
20
20
  "files": [
21
21
  "src/",
22
22
  "dist/",
23
+ "docs/USAGE_GUIDE.md",
23
24
  "README.md",
24
25
  "CHANGELOG.md",
25
26
  "LICENSE"
@@ -30,8 +31,8 @@
30
31
  "./package.json": "./package.json"
31
32
  },
32
33
  "peerDependencies": {
33
- "@earendil-works/pi-coding-agent": "^0.75.5",
34
- "@earendil-works/pi-tui": "^0.75.5"
34
+ "@earendil-works/pi-coding-agent": "^0.80.10",
35
+ "@earendil-works/pi-tui": "^0.80.10"
35
36
  },
36
37
  "dependencies": {
37
38
  "typebox": "^1.1.34"
@@ -33,6 +33,7 @@ export interface LoopCommandOptions {
33
33
  getStore: () => LoopStoreLike;
34
34
  getTriggerSystem: () => TriggerSystemLike;
35
35
  updateWidget: () => void;
36
+ onDynamicLoopActivated?: (entry: LoopEntry) => void;
36
37
  }
37
38
 
38
39
  type LoopCommandRoute =
@@ -74,7 +75,7 @@ function parseLoopCommandRoute(input: string): LoopCommandRoute {
74
75
  }
75
76
 
76
77
  export function registerLoopCommand(options: LoopCommandOptions): void {
77
- const { pi, getStore, getTriggerSystem, updateWidget } = options;
78
+ const { pi, getStore, getTriggerSystem, updateWidget, onDynamicLoopActivated } = options;
78
79
 
79
80
  function createCronLoop(ui: ExtensionUIContext, interval: string, prompt: string, notifyEvery: boolean) {
80
81
  let entry: LoopEntry | undefined;
@@ -130,6 +131,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
130
131
  getTriggerSystem().add(entry);
131
132
  updateWidget();
132
133
  ui.notify(`Dynamic loop #${entry.id} created — ${goal.slice(0, 50)}`, "info");
134
+ onDynamicLoopActivated?.(entry);
133
135
  }
134
136
 
135
137
  async function viewLoops(ui: ExtensionUIContext) {
@@ -178,6 +180,7 @@ export function registerLoopCommand(options: LoopCommandOptions): void {
178
180
  getTriggerSystem().add(resumed);
179
181
  updateWidget();
180
182
  ui.notify(`Loop #${entry.id} resumed`, "info");
183
+ if (resumed.trigger.type === "dynamic") onDynamicLoopActivated?.(resumed);
181
184
  }
182
185
  }
183
186
  }
package/src/index.ts CHANGED
@@ -368,6 +368,9 @@ export default function (pi: ExtensionAPI) {
368
368
  updateWidget: () => {
369
369
  widget.update();
370
370
  },
371
+ onDynamicLoopActivated: (entry) => {
372
+ onLoopFire(entry);
373
+ },
371
374
  });
372
375
 
373
376
  // ── Native task tools (only when pi-tasks is absent) ──
@@ -211,7 +211,7 @@ export class MonitorManager {
211
211
  if (timeout > 0) {
212
212
  setTimeout(() => {
213
213
  if (bp.entry.status === "running") {
214
- this.stop(id);
214
+ void this.stop(id, "timeout");
215
215
  }
216
216
  }, timeout);
217
217
  }
@@ -231,7 +231,7 @@ export class MonitorManager {
231
231
  .sort((a, b) => Number(a.id) - Number(b.id));
232
232
  }
233
233
 
234
- async stop(id: string): Promise<boolean> {
234
+ async stop(id: string, reason: "manual" | "timeout" = "manual"): Promise<boolean> {
235
235
  const bp = this.processes.get(id);
236
236
  if (!bp || bp.entry.status !== "running") return false;
237
237
 
@@ -243,7 +243,7 @@ export class MonitorManager {
243
243
  entityId: id,
244
244
  payload: {
245
245
  id,
246
- reason: "manual",
246
+ reason,
247
247
  },
248
248
  });
249
249
  this.schedulePrune(id);
@@ -261,6 +261,14 @@ export class MonitorManager {
261
261
  });
262
262
  });
263
263
 
264
+ if (reason === "timeout") {
265
+ this.pi.events.emit("monitor:error", {
266
+ monitorId: id,
267
+ error: `Timed out after ${bp.entry.timeout}ms`,
268
+ outputLines: bp.entry.outputLines,
269
+ });
270
+ for (const callback of bp.completionCallbacks) callback();
271
+ }
264
272
  bp.completionCallbacks = [];
265
273
  for (const resolve of bp.waiters) resolve();
266
274
  bp.waiters = [];
@@ -57,7 +57,7 @@ Default to MonitorCreate for any long-running or background work:\n- Watch a CI/
57
57
 
58
58
  ## onDone — auto-notify on completion
59
59
 
60
- Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fires when the process exits. The completion wake includes the exit code and output line count.\n\nExample: MonitorCreate command="python train.py" onDone="Check training results and report best loss"\nExample: MonitorCreate command="hut builds show 1769753" onDone="Analyze the build result and report status"`,
60
+ Pass onDone with a prompt and the monitor auto-creates a one-shot loop that fires when the process exits, fails, or times out. The completion wake lets the agent inspect the final status and buffered output.\n\nExample: MonitorCreate command="python train.py" onDone="Check training results and report best loss"\nExample: MonitorCreate command="hut builds show 1769753" onDone="Analyze the build result and report status"`,
61
61
  promptGuidelines: [
62
62
  "Default to MonitorCreate for any long-running or background command — releases the agent to keep working on other tasks in parallel.",
63
63
  "When the user asks to monitor CI builds, watch a build, check a remote job, or run an experiment, use MonitorCreate instead of inline bash/curl/wait.",