@trevonistrevon/pi-loop 0.6.0 → 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.
Files changed (108) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +28 -143
  3. package/dist/commands/loop-command.d.ts +10 -2
  4. package/dist/commands/loop-command.js +84 -51
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.js +44 -16
  7. package/dist/loop-format.d.ts +3 -0
  8. package/dist/loop-format.js +20 -0
  9. package/dist/loop-parse.d.ts +1 -0
  10. package/dist/loop-parse.js +45 -1
  11. package/dist/loop-reducer.d.ts +13 -1
  12. package/dist/loop-reducer.js +26 -0
  13. package/dist/monitor-manager.d.ts +1 -1
  14. package/dist/monitor-manager.js +13 -4
  15. package/dist/notification-reducer.d.ts +2 -0
  16. package/dist/runtime/monitor-ondone-runtime.js +1 -1
  17. package/dist/runtime/notification-runtime.d.ts +2 -1
  18. package/dist/runtime/notification-runtime.js +19 -7
  19. package/dist/runtime/task-backlog-runtime.d.ts +2 -1
  20. package/dist/runtime/task-backlog-runtime.js +13 -9
  21. package/dist/scheduler.js +25 -7
  22. package/dist/store.d.ts +9 -1
  23. package/dist/store.js +57 -0
  24. package/dist/tools/loop-tools.d.ts +8 -0
  25. package/dist/tools/loop-tools.js +104 -12
  26. package/dist/tools/monitor-tools.js +1 -1
  27. package/dist/trigger-system.js +1 -1
  28. package/dist/types.d.ts +24 -1
  29. package/docs/USAGE_GUIDE.md +185 -0
  30. package/package.json +13 -4
  31. package/src/commands/loop-command.ts +102 -54
  32. package/src/index.ts +46 -16
  33. package/src/loop-format.ts +22 -0
  34. package/src/loop-parse.ts +44 -1
  35. package/src/loop-reducer.ts +41 -1
  36. package/src/monitor-manager.ts +12 -4
  37. package/src/notification-reducer.ts +3 -0
  38. package/src/runtime/monitor-ondone-runtime.ts +1 -1
  39. package/src/runtime/notification-runtime.ts +21 -8
  40. package/src/runtime/task-backlog-runtime.ts +15 -11
  41. package/src/scheduler.ts +25 -8
  42. package/src/store.ts +59 -2
  43. package/src/tools/loop-tools.ts +128 -12
  44. package/src/tools/monitor-tools.ts +1 -1
  45. package/src/trigger-system.ts +1 -1
  46. package/src/types.ts +29 -1
  47. package/.release-please-manifest.json +0 -3
  48. package/AGENTS.md +0 -123
  49. package/CONTRIBUTING.md +0 -79
  50. package/Dockerfile +0 -17
  51. package/benchmarks/experiment-sim.js +0 -30
  52. package/coverage/base.css +0 -224
  53. package/coverage/block-navigation.js +0 -87
  54. package/coverage/coverage-final.json +0 -36
  55. package/coverage/favicon.png +0 -0
  56. package/coverage/index.html +0 -191
  57. package/coverage/prettify.css +0 -1
  58. package/coverage/prettify.js +0 -2
  59. package/coverage/sort-arrow-sprite.png +0 -0
  60. package/coverage/sorter.js +0 -210
  61. package/coverage/src/api.ts.html +0 -193
  62. package/coverage/src/commands/index.html +0 -131
  63. package/coverage/src/commands/loop-command.ts.html +0 -634
  64. package/coverage/src/commands/tasks-command.ts.html +0 -490
  65. package/coverage/src/coordinator.ts.html +0 -430
  66. package/coverage/src/index.html +0 -341
  67. package/coverage/src/index.ts.html +0 -1249
  68. package/coverage/src/loop-parse.ts.html +0 -607
  69. package/coverage/src/loop-reducer.ts.html +0 -559
  70. package/coverage/src/monitor-completion-coordinator.ts.html +0 -157
  71. package/coverage/src/monitor-manager.ts.html +0 -964
  72. package/coverage/src/monitor-reducer.ts.html +0 -583
  73. package/coverage/src/notification-reducer.ts.html +0 -550
  74. package/coverage/src/reducer-backed-store.ts.html +0 -589
  75. package/coverage/src/rpc/channels.ts.html +0 -376
  76. package/coverage/src/rpc/cross-extension-rpc.ts.html +0 -541
  77. package/coverage/src/rpc/index.html +0 -131
  78. package/coverage/src/runtime/index.html +0 -251
  79. package/coverage/src/runtime/loop-events.ts.html +0 -265
  80. package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
  81. package/coverage/src/runtime/native-task-rpc.ts.html +0 -502
  82. package/coverage/src/runtime/notification-runtime.ts.html +0 -721
  83. package/coverage/src/runtime/scope.ts.html +0 -196
  84. package/coverage/src/runtime/session-runtime.ts.html +0 -679
  85. package/coverage/src/runtime/task-backlog-runtime.ts.html +0 -616
  86. package/coverage/src/runtime/task-events.ts.html +0 -208
  87. package/coverage/src/runtime/task-mutations.ts.html +0 -394
  88. package/coverage/src/runtime/task-rpc.ts.html +0 -523
  89. package/coverage/src/scheduler.ts.html +0 -400
  90. package/coverage/src/store.ts.html +0 -667
  91. package/coverage/src/task-backlog-coordinator.ts.html +0 -181
  92. package/coverage/src/task-reducer.ts.html +0 -550
  93. package/coverage/src/task-store.ts.html +0 -526
  94. package/coverage/src/tools/index.html +0 -161
  95. package/coverage/src/tools/loop-tools.ts.html +0 -991
  96. package/coverage/src/tools/monitor-tools.ts.html +0 -538
  97. package/coverage/src/tools/native-task-tools.ts.html +0 -493
  98. package/coverage/src/tools/tool-result.ts.html +0 -97
  99. package/coverage/src/trigger-system.ts.html +0 -547
  100. package/coverage/src/ui/index.html +0 -116
  101. package/coverage/src/ui/widget.ts.html +0 -292
  102. package/docker-compose.yml +0 -12
  103. package/docs/architecture/state-machine-migration.md +0 -502
  104. package/docs/architecture/state-machine-reducer-event-model.md +0 -822
  105. package/docs/architecture/state-machine-test-matrix.md +0 -249
  106. package/docs/architecture/state-machine-transition-map.md +0 -436
  107. package/release-please-config.json +0 -9
  108. package/vitest.config.ts +0 -29
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { formatTrigger } from "../loop-format.js";
2
3
  import { parseInterval } from "../loop-parse.js";
3
4
  import { textResult } from "./tool-result.js";
4
5
  function validateTrigger(trigger) {
@@ -40,6 +41,67 @@ function formatRemaining(ms) {
40
41
  return `${Math.round(ms / 60000)}m`;
41
42
  return `${Math.round(ms / 3600000)}h`;
42
43
  }
44
+ function parseDelayMs(input) {
45
+ const match = input.trim().match(/^(\d+)\s*(s|m|h|d)$/i);
46
+ if (!match)
47
+ return undefined;
48
+ const value = Number.parseInt(match[1] ?? "", 10);
49
+ const unit = (match[2] ?? "").toLowerCase();
50
+ const multiplier = unit === "s" ? 1000 : unit === "m" ? 60000 : unit === "h" ? 3600000 : 86400000;
51
+ return value * multiplier;
52
+ }
53
+ function resolveNextWakeAt(nextInterval) {
54
+ if (!nextInterval)
55
+ return { nextWakeAt: undefined };
56
+ const parsedDelayMs = parseDelayMs(nextInterval);
57
+ if (!parsedDelayMs)
58
+ return { error: `Invalid nextInterval "${nextInterval}". Use formats like 3m, 30s, or 1h.` };
59
+ return { nextWakeAt: Date.now() + parsedDelayMs };
60
+ }
61
+ function formatDynamicUpdateResult(id, iteration, nextWakeAt) {
62
+ const mode = nextWakeAt === undefined
63
+ ? "Next wake: when idle"
64
+ : `Next wake: ${formatRemaining(Math.max(0, nextWakeAt - Date.now()))}`;
65
+ return `Dynamic loop #${id} updated\n` +
66
+ `Iteration: ${iteration ?? "?"}` +
67
+ `\n${mode}`;
68
+ }
69
+ function formatDeletionTombstone(id, tombstone) {
70
+ const detail = tombstone.pendingCount === undefined ? "" : ` (pending: ${tombstone.pendingCount})`;
71
+ return `Loop #${id} already auto-deleted: ${tombstone.reason}${detail}`;
72
+ }
73
+ function continueDynamicLoop(params, entry, store, triggerSystem) {
74
+ const { nextWakeAt, error } = resolveNextWakeAt(params.nextInterval);
75
+ if (error)
76
+ return error;
77
+ const updated = store.updateDynamic(params.id, {
78
+ prompt: params.prompt,
79
+ dynamic: {
80
+ goal: params.prompt ?? entry.dynamic.goal,
81
+ state: params.state,
82
+ metrics: params.metrics,
83
+ doneCriteria: params.doneCriteria,
84
+ iteration: (entry.dynamic.iteration ?? 0) + 1,
85
+ nextWakeAt,
86
+ awaitingUpdate: false,
87
+ lastUpdatedAt: Date.now(),
88
+ },
89
+ });
90
+ if (updated) {
91
+ triggerSystem.remove(params.id);
92
+ triggerSystem.add(updated);
93
+ }
94
+ return formatDynamicUpdateResult(params.id, updated?.dynamic?.iteration, nextWakeAt);
95
+ }
96
+ function stopDynamicLoop(params, store, triggerSystem) {
97
+ triggerSystem.remove(params.id);
98
+ if (params.status === "completed") {
99
+ store.delete(params.id);
100
+ return `Dynamic loop #${params.id} completed and deleted`;
101
+ }
102
+ store.pause(params.id);
103
+ return `Dynamic loop #${params.id} paused`;
104
+ }
43
105
  export function registerLoopTools(options) {
44
106
  const { pi, getStore, getTriggerSystem, getScheduler, getMonitorManager, updateWidget, maybeBootstrapTaskLoop, isTaskSystemReady, } = options;
45
107
  pi.registerTool({
@@ -160,11 +222,7 @@ Skip this tool when the task is a one-off check (just do it directly) or when th
160
222
  }
161
223
  const bootstrapped = await maybeBootstrapTaskLoop(entry);
162
224
  updateWidget();
163
- const triggerDesc = trigger.type === "cron"
164
- ? `schedule: ${trigger.schedule}`
165
- : trigger.type === "event"
166
- ? `event: ${trigger.source}`
167
- : `hybrid: cron ${trigger.cron} + event ${trigger.event.source}`;
225
+ const triggerDesc = formatTrigger(trigger, "create");
168
226
  return Promise.resolve(textResult(`Loop #${entry.id} created: ${entry.prompt.slice(0, 60)}\n` +
169
227
  `Trigger: ${triggerDesc}\n` +
170
228
  `Recurring: ${entry.recurring}\n` +
@@ -188,12 +246,8 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
188
246
  return Promise.resolve(textResult("No loops configured. Use LoopCreate to set up a schedule."));
189
247
  const lines = [];
190
248
  for (const entry of loops) {
191
- const triggerDesc = entry.trigger.type === "cron"
192
- ? `cron: ${entry.trigger.schedule}`
193
- : entry.trigger.type === "event"
194
- ? `event: ${entry.trigger.source}`
195
- : `hybrid: ${entry.trigger.cron} + ${entry.trigger.event.source}`;
196
- const nextFire = entry.trigger.type !== "event" ? getScheduler().nextFire(entry.id) : undefined;
249
+ const triggerDesc = formatTrigger(entry.trigger, "list");
250
+ const nextFire = entry.trigger.type === "cron" || entry.trigger.type === "hybrid" || entry.dynamic?.nextWakeAt !== undefined ? getScheduler().nextFire(entry.id) : undefined;
197
251
  const statusIcon = entry.status === "active" ? "*" : entry.status === "paused" ? "-" : "x";
198
252
  let line = `${statusIcon} #${entry.id} [${entry.status}] ${entry.prompt.slice(0, 60)}`;
199
253
  line += ` (${triggerDesc})`;
@@ -210,6 +264,37 @@ Use this before creating new loops to avoid duplicates, or to find IDs for LoopD
210
264
  return Promise.resolve(textResult(lines.join("\n")));
211
265
  },
212
266
  });
267
+ pi.registerTool({
268
+ name: "LoopUpdate",
269
+ label: "LoopUpdate",
270
+ description: `Update progress for a dynamic loop.
271
+
272
+ Use this after a dynamic loop wake. Mark status as "continue" with updated state/metrics and optional nextInterval, "completed" when the goal is done, or "paused" to stop temporarily.`,
273
+ parameters: Type.Object({
274
+ id: Type.String({ description: "Dynamic loop ID to update" }),
275
+ status: Type.String({ description: "continue, completed, or paused", enum: ["continue", "completed", "paused"] }),
276
+ state: Type.Optional(Type.String({ description: "Current progress/state summary" })),
277
+ metrics: Type.Optional(Type.String({ description: "Current metrics/check results" })),
278
+ doneCriteria: Type.Optional(Type.String({ description: "Definition of done for the dynamic loop" })),
279
+ nextInterval: Type.Optional(Type.String({ description: "When to wake next, e.g. 3m, 30s, 1h" })),
280
+ prompt: Type.Optional(Type.String({ description: "Optional updated goal/prompt text" })),
281
+ }),
282
+ execute(_toolCallId, params) {
283
+ const store = getStore();
284
+ const triggerSystem = getTriggerSystem();
285
+ const entry = store.get(params.id);
286
+ if (!entry)
287
+ return Promise.resolve(textResult(`Loop #${params.id} not found`));
288
+ if (entry.trigger.type !== "dynamic" || !entry.dynamic) {
289
+ return Promise.resolve(textResult(`Loop #${params.id} is not a dynamic loop`));
290
+ }
291
+ const message = params.status === "continue"
292
+ ? continueDynamicLoop(params, entry, store, triggerSystem)
293
+ : stopDynamicLoop(params, store, triggerSystem);
294
+ updateWidget();
295
+ return Promise.resolve(textResult(message));
296
+ },
297
+ });
213
298
  pi.registerTool({
214
299
  name: "LoopDelete",
215
300
  label: "LoopDelete",
@@ -224,8 +309,12 @@ Use "pause" to temporarily stop a loop without removing it. Use "delete" to perm
224
309
  const { id, action } = params;
225
310
  if (action === "pause") {
226
311
  const entry = getStore().pause(id);
227
- if (!entry)
312
+ if (!entry) {
313
+ const tombstone = getStore().getDeletionTombstone(id);
314
+ if (tombstone)
315
+ return Promise.resolve(textResult(formatDeletionTombstone(id, tombstone)));
228
316
  return Promise.resolve(textResult(`Loop #${id} not found`));
317
+ }
229
318
  getTriggerSystem().remove(id);
230
319
  updateWidget();
231
320
  return Promise.resolve(textResult(`Loop #${id} paused`));
@@ -235,6 +324,9 @@ Use "pause" to temporarily stop a loop without removing it. Use "delete" to perm
235
324
  updateWidget();
236
325
  if (deleted)
237
326
  return Promise.resolve(textResult(`Loop #${id} deleted`));
327
+ const tombstone = getStore().getDeletionTombstone(id);
328
+ if (tombstone)
329
+ return Promise.resolve(textResult(formatDeletionTombstone(id, tombstone)));
238
330
  return Promise.resolve(textResult(`Loop #${id} not found`));
239
331
  },
240
332
  });
@@ -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.",
@@ -28,7 +28,7 @@ export class TriggerSystem {
28
28
  this.hybridTimers.clear();
29
29
  }
30
30
  add(entry) {
31
- if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid") {
31
+ if (entry.trigger.type === "cron" || entry.trigger.type === "hybrid" || entry.trigger.type === "dynamic") {
32
32
  this.scheduler.add(entry);
33
33
  }
34
34
  if (entry.trigger.type === "event" || entry.trigger.type === "hybrid") {
package/dist/types.d.ts CHANGED
@@ -1,3 +1,12 @@
1
+ export type LoopDeletionReason = "task_backlog_empty";
2
+ export interface LoopDeletionTombstone {
3
+ id: string;
4
+ reason: LoopDeletionReason;
5
+ deletedAt: number;
6
+ prompt: string;
7
+ pendingCount?: number;
8
+ }
9
+ export type LoopDeletionTombstoneInput = Omit<LoopDeletionTombstone, "id" | "deletedAt" | "prompt">;
1
10
  export type LoopStatus = "active" | "paused";
2
11
  export interface CronTrigger {
3
12
  type: "cron";
@@ -17,7 +26,20 @@ export interface HybridTrigger {
17
26
  };
18
27
  debounceMs: number;
19
28
  }
20
- export type Trigger = CronTrigger | EventTrigger | HybridTrigger;
29
+ export interface DynamicTrigger {
30
+ type: "dynamic";
31
+ }
32
+ export type Trigger = CronTrigger | EventTrigger | HybridTrigger | DynamicTrigger;
33
+ export interface DynamicLoopState {
34
+ goal: string;
35
+ state?: string;
36
+ metrics?: string;
37
+ doneCriteria?: string;
38
+ iteration: number;
39
+ nextWakeAt?: number;
40
+ awaitingUpdate?: boolean;
41
+ lastUpdatedAt?: number;
42
+ }
21
43
  export interface LoopEntry {
22
44
  id: string;
23
45
  prompt: string;
@@ -32,6 +54,7 @@ export interface LoopEntry {
32
54
  readOnly?: boolean;
33
55
  maxFires?: number;
34
56
  fireCount?: number;
57
+ dynamic?: DynamicLoopState;
35
58
  }
36
59
  export interface LoopStoreData {
37
60
  nextId: number;
@@ -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.0",
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",
@@ -17,20 +17,29 @@
17
17
  "monitor",
18
18
  "scheduler"
19
19
  ],
20
+ "files": [
21
+ "src/",
22
+ "dist/",
23
+ "docs/USAGE_GUIDE.md",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
20
28
  "exports": {
21
29
  ".": "./src/index.ts",
22
30
  "./api": "./src/api.ts",
23
31
  "./package.json": "./package.json"
24
32
  },
25
33
  "peerDependencies": {
26
- "@earendil-works/pi-coding-agent": "^0.75.5",
27
- "@earendil-works/pi-tui": "^0.75.5"
34
+ "@earendil-works/pi-coding-agent": "^0.80.10",
35
+ "@earendil-works/pi-tui": "^0.80.10"
28
36
  },
29
37
  "dependencies": {
30
38
  "typebox": "^1.1.34"
31
39
  },
32
40
  "scripts": {
33
- "build": "tsc",
41
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
42
+ "build": "npm run clean && tsc",
34
43
  "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
35
44
  "test": "vitest run",
36
45
  "test:coverage": "vitest run --coverage",