@trevonistrevon/pi-loop 0.5.9 → 0.6.1

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 (152) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +64 -5
  3. package/dist/api.d.ts +11 -0
  4. package/dist/api.js +10 -0
  5. package/dist/commands/loop-command.d.ts +9 -2
  6. package/dist/commands/loop-command.js +81 -51
  7. package/dist/commands/tasks-command.js +3 -3
  8. package/dist/coordinator.d.ts +1 -1
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.js +89 -19
  11. package/dist/loop-format.d.ts +3 -0
  12. package/dist/loop-format.js +20 -0
  13. package/dist/loop-parse.d.ts +1 -0
  14. package/dist/loop-parse.js +59 -6
  15. package/dist/loop-reducer.d.ts +13 -1
  16. package/dist/loop-reducer.js +26 -0
  17. package/dist/monitor-manager.js +1 -1
  18. package/dist/notification-reducer.d.ts +2 -0
  19. package/dist/reducer-backed-store.d.ts +1 -1
  20. package/dist/reducer-backed-store.js +1 -1
  21. package/dist/rpc/channels.d.ts +73 -0
  22. package/dist/rpc/channels.js +30 -0
  23. package/dist/rpc/cross-extension-rpc.d.ts +59 -0
  24. package/dist/rpc/cross-extension-rpc.js +117 -0
  25. package/dist/runtime/loop-events.d.ts +26 -0
  26. package/dist/runtime/loop-events.js +26 -0
  27. package/dist/runtime/monitor-ondone-runtime.js +1 -1
  28. package/dist/runtime/native-task-rpc.d.ts +22 -0
  29. package/dist/runtime/native-task-rpc.js +50 -0
  30. package/dist/runtime/notification-runtime.d.ts +2 -1
  31. package/dist/runtime/notification-runtime.js +19 -7
  32. package/dist/runtime/task-backlog-runtime.d.ts +5 -1
  33. package/dist/runtime/task-backlog-runtime.js +14 -7
  34. package/dist/runtime/task-mutations.d.ts +39 -0
  35. package/dist/runtime/task-mutations.js +68 -0
  36. package/dist/runtime/task-rpc.d.ts +4 -0
  37. package/dist/runtime/task-rpc.js +40 -58
  38. package/dist/scheduler.js +25 -7
  39. package/dist/store.d.ts +9 -1
  40. package/dist/store.js +57 -0
  41. package/dist/tools/loop-tools.d.ts +8 -0
  42. package/dist/tools/loop-tools.js +105 -15
  43. package/dist/tools/monitor-tools.js +1 -3
  44. package/dist/tools/native-task-tools.d.ts +2 -6
  45. package/dist/tools/native-task-tools.js +25 -55
  46. package/dist/tools/tool-result.d.ts +8 -0
  47. package/dist/tools/tool-result.js +4 -0
  48. package/dist/trigger-system.js +1 -1
  49. package/dist/types.d.ts +24 -1
  50. package/package.json +15 -2
  51. package/src/api.ts +36 -0
  52. package/src/commands/loop-command.ts +100 -55
  53. package/src/commands/tasks-command.ts +3 -3
  54. package/src/coordinator.ts +1 -1
  55. package/src/index.ts +92 -19
  56. package/src/loop-format.ts +22 -0
  57. package/src/loop-parse.ts +62 -8
  58. package/src/loop-reducer.ts +41 -1
  59. package/src/monitor-manager.ts +1 -1
  60. package/src/notification-reducer.ts +3 -0
  61. package/src/reducer-backed-store.ts +3 -3
  62. package/src/rpc/channels.ts +97 -0
  63. package/src/rpc/cross-extension-rpc.ts +152 -0
  64. package/src/runtime/loop-events.ts +60 -0
  65. package/src/runtime/monitor-ondone-runtime.ts +1 -1
  66. package/src/runtime/native-task-rpc.ts +139 -0
  67. package/src/runtime/notification-runtime.ts +21 -8
  68. package/src/runtime/task-backlog-runtime.ts +25 -7
  69. package/src/runtime/task-mutations.ts +103 -0
  70. package/src/runtime/task-rpc.ts +48 -54
  71. package/src/scheduler.ts +25 -8
  72. package/src/store.ts +59 -2
  73. package/src/tools/loop-tools.ts +129 -16
  74. package/src/tools/monitor-tools.ts +1 -4
  75. package/src/tools/native-task-tools.ts +31 -52
  76. package/src/tools/tool-result.ts +4 -0
  77. package/src/trigger-system.ts +5 -5
  78. package/src/types.ts +29 -1
  79. package/.release-please-manifest.json +0 -3
  80. package/AGENTS.md +0 -83
  81. package/CONTRIBUTING.md +0 -69
  82. package/Dockerfile +0 -17
  83. package/benchmarks/experiment-sim.js +0 -30
  84. package/coverage/base.css +0 -224
  85. package/coverage/block-navigation.js +0 -87
  86. package/coverage/coverage-final.json +0 -33
  87. package/coverage/favicon.png +0 -0
  88. package/coverage/index.html +0 -176
  89. package/coverage/prettify.css +0 -1
  90. package/coverage/prettify.js +0 -2
  91. package/coverage/sort-arrow-sprite.png +0 -0
  92. package/coverage/sorter.js +0 -210
  93. package/coverage/src/commands/index.html +0 -131
  94. package/coverage/src/commands/loop-command.ts.html +0 -634
  95. package/coverage/src/commands/tasks-command.ts.html +0 -490
  96. package/coverage/src/coordinator.ts.html +0 -430
  97. package/coverage/src/goal-coordinator.ts.html +0 -259
  98. package/coverage/src/goal-reducer.ts.html +0 -982
  99. package/coverage/src/goal-store.ts.html +0 -742
  100. package/coverage/src/goal-verifier.ts.html +0 -808
  101. package/coverage/src/index.html +0 -386
  102. package/coverage/src/index.ts.html +0 -1111
  103. package/coverage/src/loop-parse.ts.html +0 -574
  104. package/coverage/src/loop-reducer.ts.html +0 -559
  105. package/coverage/src/monitor-completion-coordinator.ts.html +0 -157
  106. package/coverage/src/monitor-manager.ts.html +0 -940
  107. package/coverage/src/monitor-reducer.ts.html +0 -583
  108. package/coverage/src/notification-reducer.ts.html +0 -550
  109. package/coverage/src/reducer-backed-store.ts.html +0 -589
  110. package/coverage/src/runtime/index.html +0 -206
  111. package/coverage/src/runtime/monitor-ondone-runtime.ts.html +0 -310
  112. package/coverage/src/runtime/notification-runtime.ts.html +0 -721
  113. package/coverage/src/runtime/scope.ts.html +0 -196
  114. package/coverage/src/runtime/session-runtime.ts.html +0 -679
  115. package/coverage/src/runtime/task-backlog-runtime.ts.html +0 -574
  116. package/coverage/src/runtime/task-events.ts.html +0 -208
  117. package/coverage/src/runtime/task-rpc.ts.html +0 -541
  118. package/coverage/src/scheduler.ts.html +0 -400
  119. package/coverage/src/store.ts.html +0 -667
  120. package/coverage/src/task-backlog-coordinator.ts.html +0 -181
  121. package/coverage/src/task-reducer.ts.html +0 -550
  122. package/coverage/src/task-store.ts.html +0 -526
  123. package/coverage/src/tools/index.html +0 -146
  124. package/coverage/src/tools/loop-tools.ts.html +0 -1000
  125. package/coverage/src/tools/monitor-tools.ts.html +0 -547
  126. package/coverage/src/tools/native-task-tools.ts.html +0 -556
  127. package/coverage/src/trigger-system.ts.html +0 -547
  128. package/coverage/src/ui/index.html +0 -116
  129. package/coverage/src/ui/widget.ts.html +0 -292
  130. package/dist/goal-coordinator.d.ts +0 -22
  131. package/dist/goal-coordinator.js +0 -28
  132. package/dist/goal-reducer.d.ts +0 -98
  133. package/dist/goal-reducer.js +0 -190
  134. package/dist/goal-store.d.ts +0 -22
  135. package/dist/goal-store.js +0 -188
  136. package/dist/goal-types.d.ts +0 -82
  137. package/dist/goal-types.js +0 -1
  138. package/dist/goal-verifier.d.ts +0 -20
  139. package/dist/goal-verifier.js +0 -198
  140. package/docker-compose.yml +0 -12
  141. package/docs/architecture/goal-state-schema.md +0 -505
  142. package/docs/architecture/state-machine-migration.md +0 -546
  143. package/docs/architecture/state-machine-reducer-event-model.md +0 -823
  144. package/docs/architecture/state-machine-test-matrix.md +0 -249
  145. package/docs/architecture/state-machine-transition-map.md +0 -436
  146. package/release-please-config.json +0 -9
  147. package/src/goal-coordinator.ts +0 -58
  148. package/src/goal-reducer.ts +0 -299
  149. package/src/goal-store.ts +0 -219
  150. package/src/goal-types.ts +0 -104
  151. package/src/goal-verifier.ts +0 -241
  152. package/vitest.config.ts +0 -29
package/CHANGELOG.md CHANGED
@@ -1,5 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.1](https://github.com/trvon/pi-loop/compare/pi-loop-v0.6.0...pi-loop-v0.6.1) (2026-07-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * **loop:** add dynamic goal loops ([44e578f](https://github.com/trvon/pi-loop/commit/44e578f5e7cdeaf6aee765835892dd975d2fe913))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * fast failing monitor hit error before onDone registered. Orca/agent never woke. ([56cd43d](https://github.com/trvon/pi-loop/commit/56cd43d2b41a6b0df5c10cccb11c0fa04981d83a))
14
+ * **loop:** harden dynamic goal loops ([036fbaf](https://github.com/trvon/pi-loop/commit/036fbaff9a8c7c60acefce4dec757bfc1e6ff8d0))
15
+
16
+ ## [0.6.0](https://github.com/trvon/pi-loop/compare/pi-loop-v0.5.8...pi-loop-v0.6.0) (2026-07-02)
17
+
18
+
19
+ ### ⚠ BREAKING CHANGES
20
+
21
+ * package exports map blocks deep src/ imports — consumers must use @trevonistrevon/pi-loop/api. tasks:updated now reports previousStatus at edit time, not pre-transition.
22
+
23
+ ### Features
24
+
25
+ * add maxFires for self-limiting loops, event-driven prompt steering ([52b50f0](https://github.com/trvon/pi-loop/commit/52b50f0d2b8c0b1df6aa74b09a8561807b549fe6))
26
+ * add native task fallback and compact task tracker ([6b5b6ff](https://github.com/trvon/pi-loop/commit/6b5b6fff53cc7810242e4f9081fe6d21b318c654))
27
+ * add task decomposition guidance to TaskCreate prompt ([4c51e72](https://github.com/trvon/pi-loop/commit/4c51e72a7317e1433cdb5bed6cfc9dfa521b5bfb))
28
+ * add tasks:rpc:clean RPC for sweeping done tasks ([73812ed](https://github.com/trvon/pi-loop/commit/73812ed4c3efeb08e97908e6de6435f65c768440))
29
+ * auto-create task worker loop at backlog threshold ([430663a](https://github.com/trvon/pi-loop/commit/430663a8042560688e08bd08129ea57b190ccecc))
30
+ * canonical rpc module + init-time task server ([7335eff](https://github.com/trvon/pi-loop/commit/7335eff6440452178ae9db3cb22a49098ad3953d))
31
+ * generalize task backlog loop cleanup ([9132948](https://github.com/trvon/pi-loop/commit/9132948dc9e22cda32adf6e8c5e13a7203f9c864))
32
+ * goal prompt and loading refinements ([b31df23](https://github.com/trvon/pi-loop/commit/b31df23c240f0bde7fb69fb8ea702429576276c7))
33
+ * prune completed tasks after successful git commit ([cba120b](https://github.com/trvon/pi-loop/commit/cba120be3a7fcdf71b7f7649b414577412d26a94))
34
+ * refining monitor interface ([66a6007](https://github.com/trvon/pi-loop/commit/66a60077093f40c258efeeb971f6f8db74e24f83))
35
+ * show worker-loop hint when pending tasks reach 5+ ([74dbf2c](https://github.com/trvon/pi-loop/commit/74dbf2cbf7f55a4206762eee3731798527537ab5))
36
+ * skip loop fires when autoTask loop has no pending tasks ([f2feb07](https://github.com/trvon/pi-loop/commit/f2feb079d5b520ed4c4dd0dc85d5b385efc45c45))
37
+ * **tasks:** add native rpc update and backlog signals ([31d06f6](https://github.com/trvon/pi-loop/commit/31d06f61fce1333c359f0d38def7406474144f8b))
38
+ * **tasks:** emit native task events with previousStatus tracking ([61b49ea](https://github.com/trvon/pi-loop/commit/61b49ea5703597b02754998269edfdc5b2018ebb))
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * add trigger validation, readOnly flag, and edge-case tests ([91baa07](https://github.com/trvon/pi-loop/commit/91baa07884bc20d760c7a3642c3ec462997fc0d3))
44
+ * auto-delete worker loop when task backlog clears ([502fa10](https://github.com/trvon/pi-loop/commit/502fa106b8862c06648e6be373e5980d2af18f6f))
45
+ * auto-expire monitor:done loops, buffer output, show completed monitors ([a3ec5de](https://github.com/trvon/pi-loop/commit/a3ec5de5847d67bddee9c572079b559b03e35f3f))
46
+ * deduplicate loop follow-up messages to prevent flood ([1613511](https://github.com/trvon/pi-loop/commit/1613511a2453d346edb59919beed2bdb18c05f63))
47
+ * delete done loops/monitors immediately instead of marking expired ([43f5220](https://github.com/trvon/pi-loop/commit/43f5220a3e6d807234e4bdb7611e44c0fa7468ec))
48
+ * delete event maxFires loops immediately ([12335e7](https://github.com/trvon/pi-loop/commit/12335e75dcf8d17a2c887966cae180d2b2f848da))
49
+ * deliver monitor onDone wakes without event dependency ([79d6a8b](https://github.com/trvon/pi-loop/commit/79d6a8b925aa98e8de0ea1b12addd5e0d2db0c47))
50
+ * derive jitter ceiling from cron step, delete expired event loops ([6d13935](https://github.com/trvon/pi-loop/commit/6d13935e79d2367072bec027d5ef2cb430eff44a))
51
+ * flush buffered worker wakes on agent end ([7a7dedf](https://github.com/trvon/pi-loop/commit/7a7dedf3ec7f16eb63edbfaf93e5dae9ed7a8f25))
52
+ * harden TaskUpdate prompt to prevent taskId alias errors ([5dfefd9](https://github.com/trvon/pi-loop/commit/5dfefd96bb08c29d192de472c6224ac434394b6f))
53
+ * **injection:** use before_agent_start message, not tool_result ([99a6317](https://github.com/trvon/pi-loop/commit/99a6317aabde8ce9d4f9e0c3065f42b6c6111259))
54
+ * loop trigger fix ([619f32c](https://github.com/trvon/pi-loop/commit/619f32c48134d9cc1376a89a62a7c8d729321143))
55
+ * **monitor:** make MonitorManager spawn-injectable, fix CI test timeouts ([f277646](https://github.com/trvon/pi-loop/commit/f277646b95369b43508fbbc8cc7eef5e37580781))
56
+ * **monitor:** prune stopped/timed-out monitors ([a46f6e8](https://github.com/trvon/pi-loop/commit/a46f6e8f2b99fa39bd11ff22d1a34999e1ac4434))
57
+ * only dedup recurring loop fires, always deliver one-shot events ([352e50e](https://github.com/trvon/pi-loop/commit/352e50e1de14ecd8d884b18a8e72572270ca90e9))
58
+ * **persist:** expire event loops on session start, clean stale monitors ([7f0876d](https://github.com/trvon/pi-loop/commit/7f0876d821e93e663f43ec2d5351bc9a37223b4b))
59
+ * recommend 5m default interval in LoopCreate task-continuation prompt ([1af3bcd](https://github.com/trvon/pi-loop/commit/1af3bcd35a2782f9fc333559d293c665db76c234))
60
+ * release please fix ([16e2304](https://github.com/trvon/pi-loop/commit/16e2304ea3112664c12c65b4e098e04d390f93f0))
61
+ * **reminders:** make loop reminder directive, not informational ([e21174d](https://github.com/trvon/pi-loop/commit/e21174df3d1d9403684e094f5763db33ed9cb732))
62
+ * repair native task fallback compilation ([a8cef04](https://github.com/trvon/pi-loop/commit/a8cef04eabd8a8af404d27ffa756266ee6a188ec))
63
+ * **runtime:** unref retention timer, swallow heartbeat pump errors ([602816b](https://github.com/trvon/pi-loop/commit/602816b3abb06859a0268c02119cc753de03decb))
64
+ * scope native task files by session, prevent cross-session leakage ([0436710](https://github.com/trvon/pi-loop/commit/04367102414dd56f7668e1522b91c6f089490979))
65
+ * **tasks:** gate late native RPC probes ([c97f4ad](https://github.com/trvon/pi-loop/commit/c97f4ad6800c00b9858c1a8e640f13e699e0c26a))
66
+ * **tasks:** guard native fallback registration ([7d3b74a](https://github.com/trvon/pi-loop/commit/7d3b74abfa637bbe370ae232b8067e7bc5205ea1))
67
+ * **trigger:** auto-expire non-recurring event loops ([178f9fd](https://github.com/trvon/pi-loop/commit/178f9fd6a6548a04307f3ed83935068a235c24d3))
68
+ * use pi.hasPendingMessages() instead of bespoke tracking Set ([dab60d4](https://github.com/trvon/pi-loop/commit/dab60d46340fab04ae74b80044ad03b6b0ca9fe8))
69
+
70
+
71
+ ### Performance Improvements
72
+
73
+ * **test:** replace real 6.1s waits with fake-timer advance in onDone tests ([c713ad3](https://github.com/trvon/pi-loop/commit/c713ad36f958d53e75c4c7d7fd723c06ee420543))
74
+
3
75
  ## [0.1.2]
4
76
 
5
77
  - Added `onDone` parameter to `MonitorCreate` — auto-creates a completion loop so the agent is notified when a background process finishes, no polling needed
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 and event loops for the pi coding agent. Background monitors, scheduled re-wakes, pi-tasks integration, and native task fallback.</h6>
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>
4
4
  </p>
5
5
 
6
6
  ## Install
@@ -36,13 +36,24 @@ TaskDelete id="1"
36
36
 
37
37
  ## Commands
38
38
 
39
- `/loop [interval] [prompt]` interactive loop creation.
39
+ `/loop` creates scheduled, event-triggered, or self-paced dynamic goal loops.
40
40
 
41
41
  ```text
42
- /loop # menu
43
- /loop 5m check the deploy # 5-minute cron loop
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
44
46
  ```
45
47
 
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
54
+
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.
56
+
46
57
  `/tasks` — interactive native task viewer/manager, only registered when `pi-tasks` is absent.
47
58
 
48
59
  ```text
@@ -56,6 +67,7 @@ TaskDelete id="1"
56
67
  |---|---|
57
68
  | `LoopCreate` | Schedule a prompt on a cron timer, a pi event, or both with debounce |
58
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 |
59
71
  | `LoopDelete` | Delete or pause a loop |
60
72
  | `MonitorCreate` | Run a background command, stream output as `monitor:output` events. Use `onDone` for auto-notify on completion |
61
73
  | `MonitorList` | Show monitors with status, uptime, and output line count |
@@ -65,7 +77,7 @@ TaskDelete id="1"
65
77
  | `TaskUpdate` | Update native fallback task status/details |
66
78
  | `TaskDelete` | Delete a native fallback task |
67
79
 
68
- Trigger types: `cron` (`5m`, `1h`, `0 9 * * 1-5`), `event` (any pi event source), or `hybrid` (both, debounced).
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>`).
69
81
 
70
82
  ## Tasks
71
83
 
@@ -81,9 +93,56 @@ If `pi-tasks` does not respond during startup detection, `pi-loop` registers a n
81
93
  - `TaskCreate`, `TaskList`, `TaskUpdate`, `TaskDelete`
82
94
  - `/tasks` interactive viewer
83
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`
84
97
 
85
98
  This fallback is session-sticky: `pi-loop` decides once at startup whether `pi-tasks` or native tasks own task management for that session.
86
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
+
87
146
  ## Status line
88
147
 
89
148
  `pi-loop` keeps a compact persistent status line in the TUI.
package/dist/api.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public API surface for sibling extensions (imported as
3
+ * `@trevonistrevon/pi-loop/api`). Everything else under src/ is internal —
4
+ * the package `exports` map deliberately blocks deep imports.
5
+ */
6
+ export { type CleanReply, type CreateTaskParams, type CreateTaskReply, type PendingReply, type PingReply, replyChannel, type SpawnParams, type SpawnReply, SUBAGENTS_RPC, TASK_EVENTS, TASKS_RPC, type TaskEntryWire, type TaskStatusWire, type UpdateTaskParams, type UpdateTaskReply, } from "./rpc/channels.js";
7
+ export { type HandleRpcOptions, handleRpc, PROTOCOL_VERSION, RpcError, type RpcEventBus, type RpcReply, rpcCall, rpcProbe, } from "./rpc/cross-extension-rpc.js";
8
+ export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
9
+ export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
10
+ export { TaskStore } from "./task-store.js";
11
+ export type { TaskEntry, TaskStatus, TaskStoreData } from "./task-types.js";
package/dist/api.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Public API surface for sibling extensions (imported as
3
+ * `@trevonistrevon/pi-loop/api`). Everything else under src/ is internal —
4
+ * the package `exports` map deliberately blocks deep imports.
5
+ */
6
+ export { replyChannel, SUBAGENTS_RPC, TASK_EVENTS, TASKS_RPC, } from "./rpc/channels.js";
7
+ export { handleRpc, PROTOCOL_VERSION, RpcError, rpcCall, rpcProbe, } from "./rpc/cross-extension-rpc.js";
8
+ export { NATIVE_TASKS_PROVIDER } from "./runtime/native-task-rpc.js";
9
+ export { resolveLoopStorePath, resolveTaskStorePath } from "./runtime/scope.js";
10
+ export { TaskStore } from "./task-store.js";
@@ -1,9 +1,16 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import type { LoopEntry, Trigger } from "../types.js";
2
+ import type { DynamicLoopState, LoopEntry, Trigger } from "../types.js";
3
3
  interface LoopStoreLike {
4
4
  list(): LoopEntry[];
5
5
  get(id: string): LoopEntry | undefined;
6
- create(trigger: Trigger, prompt: string, options?: Partial<LoopEntry>): LoopEntry;
6
+ create(trigger: Trigger, prompt: string, options: {
7
+ recurring: boolean;
8
+ autoTask?: boolean;
9
+ taskBacklog?: boolean;
10
+ readOnly?: boolean;
11
+ maxFires?: number;
12
+ dynamic?: Partial<DynamicLoopState>;
13
+ }): LoopEntry;
7
14
  pause(id: string): LoopEntry | undefined;
8
15
  resume(id: string): LoopEntry | undefined;
9
16
  delete(id: string): boolean;
@@ -1,30 +1,69 @@
1
- import { parseInterval } from "../loop-parse.js";
1
+ import { formatTrigger } from "../loop-format.js";
2
+ import { isValidCronExpression, parseInterval } from "../loop-parse.js";
3
+ function parseLoopCommandRoute(input) {
4
+ const trimmed = input.trim();
5
+ if (!trimmed)
6
+ return { type: "menu" };
7
+ const eventMatch = trimmed.match(/^(?:event|when)\s+(\S+)\s+(.+)$/i);
8
+ if (eventMatch?.[1] && eventMatch[2]) {
9
+ return { type: "event", source: eventMatch[1], prompt: eventMatch[2].trim() };
10
+ }
11
+ const parts = trimmed.split(/\s+/);
12
+ if (parts.length > 5) {
13
+ const interval = parts.slice(0, 5).join(" ");
14
+ const cronShaped = parts.slice(0, 5).every((part) => /^[\d*/,-]+$/.test(part));
15
+ if (cronShaped) {
16
+ if (!isValidCronExpression(interval))
17
+ return { type: "invalid-cron", interval };
18
+ return { type: "cron", interval, prompt: parts.slice(5).join(" "), notifyEvery: false };
19
+ }
20
+ }
21
+ const intervalMatch = trimmed.match(/^(\d+\s*[smhdS]\b)/i);
22
+ if (intervalMatch) {
23
+ const interval = intervalMatch[1] ?? intervalMatch[0];
24
+ const prompt = trimmed.slice(intervalMatch[0].length).trim();
25
+ if (!prompt)
26
+ return { type: "missing-interval-prompt" };
27
+ return { type: "cron", interval, prompt, notifyEvery: true };
28
+ }
29
+ return { type: "dynamic", goal: trimmed };
30
+ }
2
31
  export function registerLoopCommand(options) {
3
32
  const { pi, getStore, getTriggerSystem, updateWidget } = options;
4
- async function scheduleLoop(ui, prompt) {
5
- const p = prompt || await ui.input("Prompt (what should the agent check?)");
6
- if (!p)
7
- return;
8
- const interval = await ui.input("Interval (e.g., 5m, 2h, 1d)");
9
- if (!interval)
10
- return;
33
+ function createCronLoop(ui, interval, prompt, notifyEvery) {
34
+ let entry;
11
35
  try {
12
36
  const parsed = parseInterval(interval);
13
37
  const trigger = { type: "cron", schedule: parsed.cron };
14
- const entry = getStore().create(trigger, p, { recurring: true });
38
+ entry = getStore().create(trigger, prompt, { recurring: true });
15
39
  getTriggerSystem().add(entry);
16
40
  updateWidget();
17
- ui.notify(`Loop #${entry.id} created: every ${parsed.description}`, "info");
41
+ const cadence = notifyEvery ? `every ${parsed.description}` : parsed.description;
42
+ ui.notify(`Loop #${entry.id} created: ${cadence} — ${prompt.slice(0, 50)}`, "info");
18
43
  }
19
44
  catch (err) {
45
+ if (entry) {
46
+ getTriggerSystem().remove(entry.id);
47
+ getStore().delete(entry.id);
48
+ updateWidget();
49
+ }
20
50
  ui.notify(err.message, "error");
21
51
  }
22
52
  }
23
- async function eventLoop(ui, prompt) {
53
+ async function scheduleLoop(ui, prompt) {
54
+ const p = prompt || await ui.input("Prompt (what should the agent check?)");
55
+ if (!p)
56
+ return;
57
+ const interval = await ui.input("Interval (e.g., 5m, 2h, 1d)");
58
+ if (!interval)
59
+ return;
60
+ createCronLoop(ui, interval, p, true);
61
+ }
62
+ async function eventLoop(ui, prompt, sourceOverride) {
24
63
  const p = prompt || await ui.input("Prompt");
25
64
  if (!p)
26
65
  return;
27
- const source = await ui.input("Pi event source (e.g., tool_execution_start, before_agent_start)");
66
+ const source = sourceOverride || await ui.input("Pi event source (e.g., tool_execution_start, before_agent_start)");
28
67
  if (!source)
29
68
  return;
30
69
  const trigger = { type: "event", source };
@@ -33,6 +72,17 @@ export function registerLoopCommand(options) {
33
72
  updateWidget();
34
73
  ui.notify(`Event loop #${entry.id} created: fires on "${source}"`, "info");
35
74
  }
75
+ function dynamicLoop(ui, goal) {
76
+ const trigger = { type: "dynamic" };
77
+ const entry = getStore().create(trigger, goal, {
78
+ recurring: true,
79
+ maxFires: 20,
80
+ dynamic: { goal, iteration: 0 },
81
+ });
82
+ getTriggerSystem().add(entry);
83
+ updateWidget();
84
+ ui.notify(`Dynamic loop #${entry.id} created — ${goal.slice(0, 50)}`, "info");
85
+ }
36
86
  async function viewLoops(ui) {
37
87
  const loops = getStore().list();
38
88
  if (loops.length === 0) {
@@ -41,19 +91,14 @@ export function registerLoopCommand(options) {
41
91
  }
42
92
  const choices = loops.map((l) => {
43
93
  const icon = l.status === "active" ? "*" : l.status === "paused" ? "-" : "x";
44
- const triggerDesc = l.trigger.type === "cron"
45
- ? `cron: ${l.trigger.schedule}`
46
- : l.trigger.type === "event"
47
- ? `event: ${l.trigger.source}`
48
- : `hybrid: ${l.trigger.cron}`;
49
- return `${icon} #${l.id} [${l.status}] ${l.prompt.slice(0, 50)} (${triggerDesc})`;
94
+ return `${icon} #${l.id} [${l.status}] ${l.prompt.slice(0, 50)} (${formatTrigger(l.trigger, "command")})`;
50
95
  });
51
96
  choices.push("< Back");
52
97
  const selected = await ui.select("Loops", choices);
53
98
  if (!selected || selected === "< Back")
54
99
  return;
55
100
  const match = selected.match(/#(\d+)/);
56
- if (match) {
101
+ if (match?.[1]) {
57
102
  const entry = getStore().get(match[1]);
58
103
  if (entry) {
59
104
  const actions = ["x Delete"];
@@ -76,8 +121,10 @@ export function registerLoopCommand(options) {
76
121
  ui.notify(`Loop #${entry.id} paused`, "info");
77
122
  }
78
123
  else if (action === "* Resume") {
79
- getStore().resume(entry.id);
80
- getTriggerSystem().add(entry);
124
+ const resumed = getStore().resume(entry.id);
125
+ if (!resumed)
126
+ return viewLoops(ui);
127
+ getTriggerSystem().add(resumed);
81
128
  updateWidget();
82
129
  ui.notify(`Loop #${entry.id} resumed`, "info");
83
130
  }
@@ -91,11 +138,11 @@ export function registerLoopCommand(options) {
91
138
  ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
92
139
  }
93
140
  pi.registerCommand("loop", {
94
- description: "Create a repeating scheduled task: /loop [interval] [prompt]. E.g., /loop 5m check the deploy, /loop 30s am I still here",
141
+ description: "Create a loop. Use /loop [interval] [prompt] for scheduled loops, /loop event <source> <prompt> for event loops, or /loop <goal> for a dynamic goal loop.",
95
142
  handler: async (args, ctx) => {
96
- const trimmed = args.trim();
97
143
  const ui = ctx.ui;
98
- if (!trimmed) {
144
+ const route = parseLoopCommandRoute(args);
145
+ if (route.type === "menu") {
99
146
  const choice = await ui.select("Loop", [
100
147
  "Create scheduled loop",
101
148
  "Create event-triggered loop",
@@ -112,36 +159,19 @@ export function registerLoopCommand(options) {
112
159
  return viewLoops(ui);
113
160
  return settings(ui);
114
161
  }
115
- const intervalMatch = trimmed.match(/^(\d+\s*[smhdS]\b)/i);
116
- if (intervalMatch) {
117
- const interval = intervalMatch[1];
118
- const prompt = trimmed.slice(intervalMatch[0].length).trim();
119
- if (!prompt) {
120
- ui.notify("Provide a prompt after the interval, e.g., /loop 5m check the deploy", "warning");
121
- return;
122
- }
123
- try {
124
- const parsed = parseInterval(interval);
125
- const trigger = { type: "cron", schedule: parsed.cron };
126
- const entry = getStore().create(trigger, prompt, { recurring: true });
127
- getTriggerSystem().add(entry);
128
- updateWidget();
129
- ui.notify(`Loop #${entry.id} created: every ${parsed.description} — ${prompt.slice(0, 50)}`, "info");
130
- }
131
- catch (err) {
132
- ui.notify(err.message, "error");
133
- }
162
+ if (route.type === "event")
163
+ return eventLoop(ui, route.prompt, route.source);
164
+ if (route.type === "cron")
165
+ return createCronLoop(ui, route.interval, route.prompt, route.notifyEvery);
166
+ if (route.type === "invalid-cron") {
167
+ ui.notify(`Invalid cron expression: ${route.interval}`, "error");
134
168
  return;
135
169
  }
136
- const choice = await ui.select("Loop mode", [
137
- `Scheduled: "${trimmed.slice(0, 50)}"`,
138
- `Event-triggered: "${trimmed.slice(0, 50)}"`,
139
- ]);
140
- if (!choice)
170
+ if (route.type === "missing-interval-prompt") {
171
+ ui.notify("Provide a prompt after the interval, e.g., /loop 5m check the deploy", "warning");
141
172
  return;
142
- if (choice.startsWith("Event"))
143
- return eventLoop(ui, trimmed);
144
- return scheduleLoop(ui, trimmed);
173
+ }
174
+ return dynamicLoop(ui, route.goal);
145
175
  },
146
176
  });
147
177
  }
@@ -47,10 +47,10 @@ export function registerTasksCommand(options) {
47
47
  await createNativeTaskInteractively(ui);
48
48
  return viewNativeTasks(ui);
49
49
  }
50
- const match = selected.match(/#(\d+)/);
51
- if (!match)
50
+ const taskId = selected.match(/#(\d+)/)?.[1];
51
+ if (!taskId)
52
52
  return viewNativeTasks(ui);
53
- const task = taskStore.get(match[1]);
53
+ const task = taskStore.get(taskId);
54
54
  if (!task)
55
55
  return viewNativeTasks(ui);
56
56
  const actions = ["x Delete"];
@@ -1,5 +1,5 @@
1
1
  export type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
2
- export type ReducerEntityType = "task" | "loop" | "monitor" | "notification" | "goal";
2
+ export type ReducerEntityType = "task" | "loop" | "monitor" | "notification";
3
3
  export interface ReducerEvent<TType extends string = string, TPayload = unknown> {
4
4
  type: TType;
5
5
  at: number;
package/dist/index.d.ts CHANGED
@@ -4,13 +4,14 @@
4
4
  * Tools:
5
5
  * LoopCreate — Create a scheduled or event-triggered re-wake loop
6
6
  * LoopList — List all active loops with status and next-fire times
7
+ * LoopUpdate — Continue, pause, or complete a dynamic goal loop
7
8
  * LoopDelete — Delete or pause a loop by ID
8
9
  * MonitorCreate — Start a background command that streams output via pi events
9
10
  * MonitorList — List running monitors
10
11
  * MonitorStop — Stop a running monitor
11
12
  *
12
13
  * Commands:
13
- * /loop — Schedule or manage re-wake loops: /loop [interval] [prompt]
14
+ * /loop — Schedule or manage re-wake loops, including /loop <goal>
14
15
  * /tasks — View or manage native fallback tasks when pi-tasks is absent
15
16
  */
16
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";