@trevonistrevon/pi-loop 0.4.11 → 0.5.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 (87) hide show
  1. package/README.md +20 -9
  2. package/dist/commands/loop-command.d.ts +22 -0
  3. package/dist/commands/loop-command.js +148 -0
  4. package/dist/commands/tasks-command.d.ts +15 -0
  5. package/dist/commands/tasks-command.js +117 -0
  6. package/dist/coordinator.d.ts +35 -0
  7. package/dist/coordinator.js +56 -0
  8. package/dist/goal-coordinator.d.ts +22 -0
  9. package/dist/goal-coordinator.js +28 -0
  10. package/dist/goal-reducer.d.ts +89 -0
  11. package/dist/goal-reducer.js +181 -0
  12. package/dist/goal-store.d.ts +31 -0
  13. package/dist/goal-store.js +298 -0
  14. package/dist/goal-types.d.ts +82 -0
  15. package/dist/goal-types.js +1 -0
  16. package/dist/goal-verifier.d.ts +20 -0
  17. package/dist/goal-verifier.js +198 -0
  18. package/dist/index.js +130 -1087
  19. package/dist/loop-reducer.d.ts +63 -0
  20. package/dist/loop-reducer.js +67 -0
  21. package/dist/monitor-completion-coordinator.d.ts +10 -0
  22. package/dist/monitor-completion-coordinator.js +13 -0
  23. package/dist/monitor-manager.d.ts +2 -0
  24. package/dist/monitor-manager.js +107 -29
  25. package/dist/monitor-reducer.d.ts +82 -0
  26. package/dist/monitor-reducer.js +69 -0
  27. package/dist/notification-reducer.d.ts +81 -0
  28. package/dist/notification-reducer.js +65 -0
  29. package/dist/runtime/monitor-ondone-runtime.d.ts +13 -0
  30. package/dist/runtime/monitor-ondone-runtime.js +49 -0
  31. package/dist/runtime/notification-runtime.d.ts +34 -0
  32. package/dist/runtime/notification-runtime.js +152 -0
  33. package/dist/runtime/scope.d.ts +8 -0
  34. package/dist/runtime/scope.js +33 -0
  35. package/dist/runtime/session-runtime.d.ts +39 -0
  36. package/dist/runtime/session-runtime.js +110 -0
  37. package/dist/runtime/task-backlog-runtime.d.ts +36 -0
  38. package/dist/runtime/task-backlog-runtime.js +105 -0
  39. package/dist/runtime/task-rpc.d.ts +19 -0
  40. package/dist/runtime/task-rpc.js +118 -0
  41. package/dist/store.d.ts +7 -4
  42. package/dist/store.js +129 -49
  43. package/dist/task-backlog-coordinator.d.ts +12 -0
  44. package/dist/task-backlog-coordinator.js +22 -0
  45. package/dist/task-reducer.d.ts +66 -0
  46. package/dist/task-reducer.js +76 -0
  47. package/dist/task-store.d.ts +8 -4
  48. package/dist/task-store.js +102 -33
  49. package/dist/tools/loop-tools.d.ts +41 -0
  50. package/dist/tools/loop-tools.js +241 -0
  51. package/dist/tools/monitor-tools.d.ts +25 -0
  52. package/dist/tools/monitor-tools.js +110 -0
  53. package/dist/tools/native-task-tools.d.ts +15 -0
  54. package/dist/tools/native-task-tools.js +127 -0
  55. package/docs/architecture/goal-state-schema.md +505 -0
  56. package/docs/architecture/state-machine-migration.md +546 -0
  57. package/docs/architecture/state-machine-reducer-event-model.md +823 -0
  58. package/docs/architecture/state-machine-test-matrix.md +249 -0
  59. package/docs/architecture/state-machine-transition-map.md +436 -0
  60. package/package.json +1 -1
  61. package/src/commands/loop-command.ts +184 -0
  62. package/src/commands/tasks-command.ts +135 -0
  63. package/src/coordinator.ts +115 -0
  64. package/src/goal-coordinator.ts +58 -0
  65. package/src/goal-reducer.ts +280 -0
  66. package/src/goal-store.ts +315 -0
  67. package/src/goal-types.ts +104 -0
  68. package/src/goal-verifier.ts +241 -0
  69. package/src/index.ts +134 -1147
  70. package/src/loop-reducer.ts +148 -0
  71. package/src/monitor-completion-coordinator.ts +24 -0
  72. package/src/monitor-manager.ts +115 -27
  73. package/src/monitor-reducer.ts +166 -0
  74. package/src/notification-reducer.ts +155 -0
  75. package/src/runtime/monitor-ondone-runtime.ts +75 -0
  76. package/src/runtime/notification-runtime.ts +212 -0
  77. package/src/runtime/scope.ts +37 -0
  78. package/src/runtime/session-runtime.ts +168 -0
  79. package/src/runtime/task-backlog-runtime.ts +163 -0
  80. package/src/runtime/task-rpc.ts +150 -0
  81. package/src/store.ts +132 -50
  82. package/src/task-backlog-coordinator.ts +32 -0
  83. package/src/task-reducer.ts +152 -0
  84. package/src/task-store.ts +103 -31
  85. package/src/tools/loop-tools.ts +304 -0
  86. package/src/tools/monitor-tools.ts +145 -0
  87. package/src/tools/native-task-tools.ts +144 -0
@@ -0,0 +1,823 @@
1
+ # pi-loop Reducer Event Model
2
+
3
+ ## Purpose
4
+
5
+ This document defines the reducer-facing event model for the planned state-machine extraction.
6
+
7
+ It sits between:
8
+
9
+ - `docs/architecture/state-machine-transition-map.md`
10
+ - `docs/architecture/state-machine-test-matrix.md`
11
+
12
+ Its job is to give tasks `#6` and `#7` a stable contract for:
13
+
14
+ - event names
15
+ - reducer inputs
16
+ - reducer outputs
17
+ - side-effect descriptions
18
+ - entity ownership boundaries
19
+
20
+ This is a design artifact only. It does **not** change runtime behavior yet.
21
+
22
+ ---
23
+
24
+ ## 1. Design goals
25
+
26
+ The event model should:
27
+
28
+ 1. preserve current semantics before simplifying implementation
29
+ 2. keep reducers pure
30
+ 3. separate **state transitions** from **effects**
31
+ 4. avoid one giant FSM for everything
32
+ 5. make cross-entity coordination explicit
33
+ 6. support future Goal orchestration without rewriting the core model again
34
+
35
+ ---
36
+
37
+ ## 2. Model shape
38
+
39
+ Use **separate reducers per entity family** plus a thin coordinator.
40
+
41
+ ### Reducers
42
+
43
+ - `reduceTaskState(...)`
44
+ - `reduceLoopState(...)`
45
+ - `reduceMonitorState(...)`
46
+ - `reduceNotificationState(...)`
47
+ - later: `reduceGoalState(...)`
48
+
49
+ ### Coordinator
50
+
51
+ A small coordinator should:
52
+
53
+ - receive runtime events
54
+ - fan them out to the relevant reducer(s)
55
+ - collect emitted effects
56
+ - execute those effects in order
57
+ - emit any derived follow-up events
58
+
59
+ This keeps:
60
+
61
+ - reducers deterministic
62
+ - side effects testable as data
63
+ - cross-entity logic explicit instead of buried in `src/index.ts`
64
+
65
+ ---
66
+
67
+ ## 3. Shared event envelope
68
+
69
+ All reducer events should use one envelope shape.
70
+
71
+ ```ts
72
+ interface ReducerEvent<TType extends string = string, TPayload = unknown> {
73
+ type: TType;
74
+ at: number;
75
+ source:
76
+ | "tool"
77
+ | "command"
78
+ | "scheduler"
79
+ | "eventbus"
80
+ | "monitor"
81
+ | "session"
82
+ | "coordinator"
83
+ | "system";
84
+ entityType?: "task" | "loop" | "monitor" | "notification" | "goal";
85
+ entityId?: string;
86
+ payload: TPayload;
87
+ }
88
+ ```
89
+
90
+ ### Rules
91
+
92
+ - `type` is the stable contract
93
+ - `at` is required for ordering and deterministic tests
94
+ - `source` records where the event originated
95
+ - `entityType` and `entityId` are optional because some events are global
96
+ - `payload` must be serializable test data
97
+
98
+ ---
99
+
100
+ ## 4. Shared effect envelope
101
+
102
+ Reducers do not call Pi APIs directly. They emit effects.
103
+
104
+ ```ts
105
+ interface ReducerEffect<TEffect extends string = string, TPayload = unknown> {
106
+ type: TEffect;
107
+ entityType?: "task" | "loop" | "monitor" | "notification" | "goal";
108
+ entityId?: string;
109
+ payload: TPayload;
110
+ }
111
+ ```
112
+
113
+ ### Effect classes
114
+
115
+ 1. **persistence effects**
116
+ - save/delete/update store entries
117
+ 2. **runtime wiring effects**
118
+ - add/remove scheduler entries
119
+ - add/remove event subscriptions
120
+ 3. **wake/message effects**
121
+ - queue notification
122
+ - flush notification
123
+ - clear notifications
124
+ 4. **cleanup effects**
125
+ - prune tasks
126
+ - prune monitors
127
+ - delete stale loops
128
+ 5. **derived-event effects**
129
+ - request that the coordinator dispatch another reducer event
130
+
131
+ ---
132
+
133
+ ## 5. State ownership boundaries
134
+
135
+ ### Task reducer owns
136
+
137
+ - task entities
138
+ - task status transitions
139
+ - monotonic task ids
140
+ - completed-task retention vs prune state
141
+
142
+ ### Loop reducer owns
143
+
144
+ - loop entities
145
+ - active/paused/terminal lifecycle
146
+ - fire counts
147
+ - maxFires/expiry decisions
148
+ - backlog-loop classification
149
+
150
+ ### Monitor reducer owns
151
+
152
+ - monitor entities
153
+ - running/completed/error/stopped lifecycle
154
+ - terminal retention/prune status
155
+ - completion callback registration state
156
+
157
+ ### Notification reducer owns
158
+
159
+ - queued notifications
160
+ - dedupe behavior
161
+ - flush eligibility state
162
+ - cleared/dropped/delivered outcomes
163
+
164
+ ### Coordinator owns
165
+
166
+ - event ordering across reducers
167
+ - derived-event dispatch
168
+ - effect execution
169
+ - fan-out rules like "monitor completed -> queue wake"
170
+ - runtime API integration with Pi/tool layer
171
+
172
+ ---
173
+
174
+ ## 6. Event vocabulary
175
+
176
+ The event set should be small but explicit.
177
+
178
+ ### 6.1 Task events
179
+
180
+ ```ts
181
+ type TaskEventType =
182
+ | "TASK_CREATED"
183
+ | "TASK_STARTED"
184
+ | "TASK_COMPLETED"
185
+ | "TASK_REOPENED"
186
+ | "TASK_UPDATED"
187
+ | "TASK_DELETED"
188
+ | "TASKS_PRUNED";
189
+ ```
190
+
191
+ ### Semantics
192
+
193
+ - `TASK_CREATED`: create a new pending task
194
+ - `TASK_STARTED`: transition pending -> in_progress
195
+ - `TASK_COMPLETED`: transition pending|in_progress -> completed
196
+ - `TASK_REOPENED`: transition completed -> pending
197
+ - `TASK_UPDATED`: subject/description-only edits or generic fallback
198
+ - `TASK_DELETED`: remove one task
199
+ - `TASKS_PRUNED`: remove all completed tasks by policy trigger
200
+
201
+ ### Recommended payloads
202
+
203
+ ```ts
204
+ type TaskCreatedPayload = {
205
+ id: string;
206
+ subject: string;
207
+ description: string;
208
+ metadata?: Record<string, unknown>;
209
+ };
210
+
211
+ type TaskStatePayload = { id: string };
212
+
213
+ type TaskUpdatedPayload = {
214
+ id: string;
215
+ subject?: string;
216
+ description?: string;
217
+ };
218
+
219
+ type TasksPrunedPayload = {
220
+ reason: "git_commit" | "zero_pending_cleanup" | "manual";
221
+ };
222
+ ```
223
+
224
+ ---
225
+
226
+ ### 6.2 Loop events
227
+
228
+ ```ts
229
+ type LoopEventType =
230
+ | "LOOP_CREATED"
231
+ | "LOOP_PAUSED"
232
+ | "LOOP_RESUMED"
233
+ | "LOOP_DELETED"
234
+ | "LOOP_FIRED"
235
+ | "LOOP_MAX_FIRES_REACHED"
236
+ | "LOOP_EXPIRED"
237
+ | "LOOP_BACKLOG_EMPTY"
238
+ | "LOOP_BOOTSTRAP_REQUESTED"
239
+ | "LOOP_ARMING_RECONCILE_REQUESTED";
240
+ ```
241
+
242
+ ### Semantics
243
+
244
+ - `LOOP_CREATED`: a new loop exists in active state
245
+ - `LOOP_PAUSED`: active -> paused
246
+ - `LOOP_RESUMED`: paused -> active
247
+ - `LOOP_DELETED`: terminal removal by user/system
248
+ - `LOOP_FIRED`: one due fire occurred
249
+ - `LOOP_MAX_FIRES_REACHED`: final fire consumed limit
250
+ - `LOOP_EXPIRED`: lifetime cap or stale-session expiry
251
+ - `LOOP_BACKLOG_EMPTY`: taskBacklog loop should self-delete
252
+ - `LOOP_BOOTSTRAP_REQUESTED`: existing backlog should create initial wake
253
+ - `LOOP_ARMING_RECONCILE_REQUESTED`: sync loop state with scheduler/subscriptions
254
+
255
+ ### Recommended payloads
256
+
257
+ ```ts
258
+ type LoopCreatedPayload = {
259
+ id: string;
260
+ prompt: string;
261
+ trigger: unknown;
262
+ recurring: boolean;
263
+ autoTask?: boolean;
264
+ taskBacklog?: boolean;
265
+ readOnly?: boolean;
266
+ maxFires?: number;
267
+ };
268
+
269
+ type LoopIdPayload = { id: string };
270
+
271
+ type LoopFiredPayload = {
272
+ id: string;
273
+ trigger: unknown;
274
+ recurring: boolean;
275
+ autoTask?: boolean;
276
+ readOnly?: boolean;
277
+ };
278
+
279
+ type LoopExpiredPayload = {
280
+ id: string;
281
+ reason: "expires_at" | "resume_event_stale" | "already_completed_monitor";
282
+ };
283
+ ```
284
+
285
+ ---
286
+
287
+ ### 6.3 Monitor events
288
+
289
+ ```ts
290
+ type MonitorEventType =
291
+ | "MONITOR_CREATED"
292
+ | "MONITOR_OUTPUT"
293
+ | "MONITOR_COMPLETED"
294
+ | "MONITOR_ERRORED"
295
+ | "MONITOR_STOPPED"
296
+ | "MONITOR_PRUNED"
297
+ | "MONITOR_ONDONE_REGISTERED";
298
+ ```
299
+
300
+ ### Semantics
301
+
302
+ - `MONITOR_CREATED`: running monitor started
303
+ - `MONITOR_OUTPUT`: output line buffered/emitted
304
+ - `MONITOR_COMPLETED`: clean exit
305
+ - `MONITOR_ERRORED`: process error or nonzero exit
306
+ - `MONITOR_STOPPED`: explicit stop path
307
+ - `MONITOR_PRUNED`: retention timeout elapsed
308
+ - `MONITOR_ONDONE_REGISTERED`: completion callback attached
309
+
310
+ ### Recommended payloads
311
+
312
+ ```ts
313
+ type MonitorCreatedPayload = {
314
+ id: string;
315
+ command: string;
316
+ description?: string;
317
+ timeout: number;
318
+ };
319
+
320
+ type MonitorOutputPayload = {
321
+ id: string;
322
+ line: string;
323
+ };
324
+
325
+ type MonitorCompletedPayload = {
326
+ id: string;
327
+ exitCode?: number;
328
+ outputLines: number;
329
+ };
330
+
331
+ type MonitorErroredPayload = {
332
+ id: string;
333
+ exitCode?: number;
334
+ error?: string;
335
+ outputLines?: number;
336
+ };
337
+
338
+ type MonitorStoppedPayload = {
339
+ id: string;
340
+ reason: "manual" | "timeout";
341
+ };
342
+
343
+ type MonitorOnDoneRegisteredPayload = {
344
+ id: string;
345
+ prompt: string;
346
+ loopId?: string;
347
+ };
348
+ ```
349
+
350
+ ---
351
+
352
+ ### 6.4 Notification events
353
+
354
+ ```ts
355
+ type NotificationEventType =
356
+ | "NOTIFICATION_QUEUED"
357
+ | "NOTIFICATION_REPLACED"
358
+ | "NOTIFICATION_DELIVERED"
359
+ | "NOTIFICATION_DROPPED"
360
+ | "NOTIFICATION_CLEARED"
361
+ | "NOTIFICATION_FLUSH_REQUESTED";
362
+ ```
363
+
364
+ ### Semantics
365
+
366
+ - `NOTIFICATION_QUEUED`: add pending wake
367
+ - `NOTIFICATION_REPLACED`: recurring dedupe replaced earlier wake
368
+ - `NOTIFICATION_DELIVERED`: sent to Pi successfully
369
+ - `NOTIFICATION_DROPPED`: removed without delivery by policy
370
+ - `NOTIFICATION_CLEARED`: removed due to session change/shutdown
371
+ - `NOTIFICATION_FLUSH_REQUESTED`: coordinator should attempt delivery pass
372
+
373
+ ### Recommended payloads
374
+
375
+ ```ts
376
+ type NotificationQueuedPayload = {
377
+ key: string;
378
+ loopId: string;
379
+ message: string;
380
+ recurring?: boolean;
381
+ autoTask?: boolean;
382
+ readOnly?: boolean;
383
+ trigger: unknown;
384
+ };
385
+
386
+ type NotificationDroppedPayload = {
387
+ key: string;
388
+ reason:
389
+ | "zero_pending_tasks"
390
+ | "session_switch"
391
+ | "session_shutdown"
392
+ | "superseded";
393
+ };
394
+
395
+ type NotificationClearedPayload = {
396
+ reason: "session_switch" | "session_shutdown";
397
+ };
398
+ ```
399
+
400
+ ---
401
+
402
+ ### 6.5 Session / coordinator events
403
+
404
+ These are not owned by one entity reducer, but they drive coordination.
405
+
406
+ ```ts
407
+ type CoordinatorEventType =
408
+ | "SESSION_TURN_STARTED"
409
+ | "SESSION_BEFORE_AGENT_START"
410
+ | "SESSION_AGENT_STARTED"
411
+ | "SESSION_AGENT_ENDED"
412
+ | "SESSION_SWITCHED"
413
+ | "SESSION_SHUTDOWN"
414
+ | "TASK_BACKLOG_THRESHOLD_REACHED"
415
+ | "TASK_BACKLOG_EMPTIED"
416
+ | "TASK_PROVIDER_DETECTED"
417
+ | "TASK_PROVIDER_UNAVAILABLE"
418
+ | "GIT_COMMIT_SUCCEEDED";
419
+ ```
420
+
421
+ ### Notes
422
+
423
+ - session events should usually emit **derived entity events** rather than mutate entity state directly
424
+ - `GIT_COMMIT_SUCCEEDED` should derive `TASKS_PRUNED`
425
+ - `TASK_BACKLOG_THRESHOLD_REACHED` should derive `LOOP_CREATED` for the worker loop if absent
426
+ - `TASK_BACKLOG_EMPTIED` should derive `LOOP_BACKLOG_EMPTY`
427
+
428
+ ---
429
+
430
+ ## 7. Reducer outputs
431
+
432
+ Each reducer should return both next state and effects.
433
+
434
+ ```ts
435
+ interface ReduceResult<TState> {
436
+ state: TState;
437
+ effects: ReducerEffect[];
438
+ }
439
+ ```
440
+
441
+ ### Rule
442
+
443
+ A reducer may emit effects, but it may **not**:
444
+
445
+ - call Pi APIs
446
+ - read system time directly
447
+ - mutate unrelated reducer state
448
+ - inspect external runtime maps directly
449
+
450
+ All external facts must arrive as events.
451
+
452
+ ---
453
+
454
+ ## 8. Effect vocabulary
455
+
456
+ Use a small, explicit effect set.
457
+
458
+ ### 8.1 Persistence effects
459
+
460
+ ```ts
461
+ type PersistenceEffectType =
462
+ | "PERSIST_TASK"
463
+ | "DELETE_TASK"
464
+ | "PERSIST_LOOP"
465
+ | "DELETE_LOOP"
466
+ | "PERSIST_MONITOR"
467
+ | "DELETE_MONITOR";
468
+ ```
469
+
470
+ ### 8.2 Runtime wiring effects
471
+
472
+ ```ts
473
+ type RuntimeEffectType =
474
+ | "ARM_LOOP_RUNTIME"
475
+ | "DISARM_LOOP_RUNTIME"
476
+ | "REGISTER_MONITOR_ONDONE"
477
+ | "SCHEDULE_MONITOR_PRUNE";
478
+ ```
479
+
480
+ ### 8.3 Notification effects
481
+
482
+ ```ts
483
+ type NotificationEffectType =
484
+ | "QUEUE_NOTIFICATION"
485
+ | "DELIVER_NOTIFICATION"
486
+ | "CLEAR_NOTIFICATIONS"
487
+ | "REQUEST_NOTIFICATION_FLUSH";
488
+ ```
489
+
490
+ ### 8.4 Derived-event effects
491
+
492
+ ```ts
493
+ type DerivedEventEffectType = "DISPATCH_EVENT";
494
+ ```
495
+
496
+ ### 8.5 Cleanup effects
497
+
498
+ ```ts
499
+ type CleanupEffectType =
500
+ | "PRUNE_COMPLETED_TASKS"
501
+ | "PRUNE_TERMINAL_MONITORS"
502
+ | "RECONCILE_BACKLOG_LOOPS";
503
+ ```
504
+
505
+ ---
506
+
507
+ ## 9. Event-to-current-runtime mapping
508
+
509
+ This section maps today's code paths to the future event model.
510
+
511
+ ### Current: `TaskCreate`
512
+
513
+ Emit:
514
+
515
+ - `TASK_CREATED`
516
+ - maybe `TASK_BACKLOG_THRESHOLD_REACHED`
517
+
518
+ ### Current: `TaskUpdate(status=in_progress)`
519
+
520
+ Emit:
521
+
522
+ - `TASK_STARTED`
523
+
524
+ ### Current: `TaskUpdate(status=completed)`
525
+
526
+ Emit:
527
+
528
+ - `TASK_COMPLETED`
529
+ - maybe `TASK_BACKLOG_EMPTIED`
530
+
531
+ ### Current: `TaskUpdate(status=pending)` on completed task
532
+
533
+ Emit:
534
+
535
+ - `TASK_REOPENED`
536
+
537
+ ### Current: `LoopCreate`
538
+
539
+ Emit:
540
+
541
+ - `LOOP_CREATED`
542
+ - maybe `LOOP_BOOTSTRAP_REQUESTED`
543
+ - `LOOP_ARMING_RECONCILE_REQUESTED`
544
+
545
+ ### Current: `LoopDelete(action=pause)`
546
+
547
+ Emit:
548
+
549
+ - `LOOP_PAUSED`
550
+
551
+ ### Current: resume action from interactive UI
552
+
553
+ Emit:
554
+
555
+ - `LOOP_RESUMED`
556
+ - `LOOP_ARMING_RECONCILE_REQUESTED`
557
+
558
+ ### Current: scheduler/event trigger fire
559
+
560
+ Emit:
561
+
562
+ - `LOOP_FIRED`
563
+ - maybe `LOOP_MAX_FIRES_REACHED`
564
+ - derived `NOTIFICATION_QUEUED`
565
+
566
+ ### Current: monitor start
567
+
568
+ Emit:
569
+
570
+ - `MONITOR_CREATED`
571
+ - maybe `MONITOR_ONDONE_REGISTERED`
572
+
573
+ ### Current: monitor clean exit
574
+
575
+ Emit:
576
+
577
+ - `MONITOR_COMPLETED`
578
+ - derived `NOTIFICATION_QUEUED` for onDone path
579
+ - derived `MONITOR_PRUNED` after retention timeout
580
+
581
+ ### Current: monitor error path
582
+
583
+ Emit:
584
+
585
+ - `MONITOR_ERRORED`
586
+
587
+ ### Current: monitor stop
588
+
589
+ Emit:
590
+
591
+ - `MONITOR_STOPPED`
592
+
593
+ ### Current: `session_switch`
594
+
595
+ Emit:
596
+
597
+ - `SESSION_SWITCHED`
598
+ - derived `NOTIFICATION_CLEARED`
599
+ - maybe derived `LOOP_EXPIRED` for stale event/hybrid loops
600
+ - maybe derived `LOOP_DELETED` for memory-scope non-resume clearing
601
+
602
+ ### Current: `session_shutdown`
603
+
604
+ Emit:
605
+
606
+ - `SESSION_SHUTDOWN`
607
+ - derived `NOTIFICATION_CLEARED`
608
+
609
+ ### Current: successful `git commit`
610
+
611
+ Emit:
612
+
613
+ - `GIT_COMMIT_SUCCEEDED`
614
+ - derived `TASKS_PRUNED`
615
+
616
+ ---
617
+
618
+ ## 10. Coordinator rules
619
+
620
+ These rules should move out of ad hoc control flow into explicit coordinator logic.
621
+
622
+ ### Rule A — backlog threshold auto-worker creation
623
+
624
+ When native pending tasks cross the threshold:
625
+
626
+ 1. dispatch `TASK_BACKLOG_THRESHOLD_REACHED`
627
+ 2. if no worker loop exists, emit `LOOP_CREATED`
628
+ 3. emit `LOOP_BOOTSTRAP_REQUESTED` if backlog already exists
629
+
630
+ ### Rule B — backlog empties
631
+
632
+ When pending tasks reach zero:
633
+
634
+ 1. dispatch `TASK_BACKLOG_EMPTIED`
635
+ 2. derive `LOOP_BACKLOG_EMPTY` for each `taskBacklog` loop
636
+ 3. reducer emits `DELETE_LOOP`
637
+
638
+ ### Rule C — loop fire -> notification
639
+
640
+ When `LOOP_FIRED` occurs:
641
+
642
+ 1. loop reducer increments `fireCount`
643
+ 2. if limit reached, also derive `LOOP_MAX_FIRES_REACHED`
644
+ 3. coordinator requests notification queueing
645
+ 4. notification reducer decides replace vs append
646
+
647
+ ### Rule D — notification flush
648
+
649
+ When idle/session conditions allow:
650
+
651
+ 1. dispatch `NOTIFICATION_FLUSH_REQUESTED`
652
+ 2. notification reducer chooses next deliverable item
653
+ 3. emit `DELIVER_NOTIFICATION`
654
+ 4. if delivery blocked, keep state unchanged
655
+
656
+ ### Rule E — monitor onDone
657
+
658
+ When a monitor with onDone completes:
659
+
660
+ 1. dispatch `MONITOR_COMPLETED`
661
+ 2. derive `NOTIFICATION_QUEUED` directly
662
+ 3. do **not** depend on generic eventbus delivery semantics for correctness
663
+
664
+ ### Rule F — session switch/shutdown
665
+
666
+ 1. dispatch session event
667
+ 2. clear pending notifications
668
+ 3. derive any loop cleanup events required by scope/resume rules
669
+ 4. reconcile runtime wiring afterward
670
+
671
+ ---
672
+
673
+ ## 11. Reducer state shapes
674
+
675
+ These should stay small and explicit.
676
+
677
+ ### Task reducer state
678
+
679
+ ```ts
680
+ interface TaskReducerState {
681
+ tasksById: Record<string, TaskEntryView>;
682
+ nextId: number;
683
+ }
684
+ ```
685
+
686
+ ### Loop reducer state
687
+
688
+ ```ts
689
+ interface LoopReducerState {
690
+ loopsById: Record<string, LoopEntryView>;
691
+ }
692
+ ```
693
+
694
+ ### Monitor reducer state
695
+
696
+ ```ts
697
+ interface MonitorReducerState {
698
+ monitorsById: Record<string, MonitorEntryView>;
699
+ }
700
+ ```
701
+
702
+ ### Notification reducer state
703
+
704
+ ```ts
705
+ interface NotificationReducerState {
706
+ notificationsByKey: Record<string, PendingNotificationView>;
707
+ agentRunning: boolean;
708
+ hasPendingMessages: boolean;
709
+ }
710
+ ```
711
+
712
+ ### Important
713
+
714
+ Runtime-only objects like:
715
+
716
+ - child process handles
717
+ - scheduler timer internals
718
+ - event unsubscribe functions
719
+ - abort controllers
720
+
721
+ should remain **outside** reducer state. Reducers track only their logical status.
722
+
723
+ ---
724
+
725
+ ## 12. Recommended migration order
726
+
727
+ ### Step 1 — task reducer
728
+
729
+ Extract first because it is the simplest and already well covered.
730
+
731
+ Start with:
732
+
733
+ - `TASK_CREATED`
734
+ - `TASK_STARTED`
735
+ - `TASK_COMPLETED`
736
+ - `TASK_REOPENED`
737
+ - `TASK_DELETED`
738
+ - `TASKS_PRUNED`
739
+
740
+ ### Step 2 — loop reducer
741
+
742
+ Extract next, but keep runtime arming/disarming as effects.
743
+
744
+ Start with:
745
+
746
+ - `LOOP_CREATED`
747
+ - `LOOP_PAUSED`
748
+ - `LOOP_RESUMED`
749
+ - `LOOP_DELETED`
750
+ - `LOOP_FIRED`
751
+ - `LOOP_MAX_FIRES_REACHED`
752
+ - `LOOP_EXPIRED`
753
+ - `LOOP_BACKLOG_EMPTY`
754
+
755
+ ### Step 3 — notification reducer
756
+
757
+ Then extract queue/dedupe/flush rules.
758
+
759
+ Start with:
760
+
761
+ - `NOTIFICATION_QUEUED`
762
+ - `NOTIFICATION_REPLACED`
763
+ - `NOTIFICATION_DROPPED`
764
+ - `NOTIFICATION_CLEARED`
765
+ - `NOTIFICATION_FLUSH_REQUESTED`
766
+
767
+ ### Step 4 — monitor reducer
768
+
769
+ Finally extract monitor lifecycle because it has the most runtime coupling.
770
+
771
+ Start with:
772
+
773
+ - `MONITOR_CREATED`
774
+ - `MONITOR_COMPLETED`
775
+ - `MONITOR_ERRORED`
776
+ - `MONITOR_STOPPED`
777
+ - `MONITOR_PRUNED`
778
+ - `MONITOR_ONDONE_REGISTERED`
779
+
780
+ ---
781
+
782
+ ## 13. Test alignment
783
+
784
+ Every event in this model should map cleanly to the existing matrix.
785
+
786
+ ### Examples
787
+
788
+ - matrix `T-03` -> `TASK_STARTED`
789
+ - matrix `T-06` -> `TASK_REOPENED`
790
+ - matrix `L-08` -> `LOOP_FIRED` then `LOOP_DELETED`
791
+ - matrix `M-09` -> `MONITOR_COMPLETED` then derived `NOTIFICATION_QUEUED`
792
+ - matrix `N-08` -> `SESSION_SWITCHED` then derived `NOTIFICATION_CLEARED`
793
+ - matrix `S-08` -> `MONITOR_COMPLETED` while `agentRunning=true`, then `SESSION_AGENT_ENDED`, then `NOTIFICATION_DELIVERED`
794
+
795
+ If an event cannot be tied to a transition/invariant/scenario row, it is probably too vague or too implementation-specific.
796
+
797
+ ---
798
+
799
+ ## 14. Non-goals
800
+
801
+ This event model does **not** yet define:
802
+
803
+ - Goal reducer fields
804
+ - persistent serialization format for goals
805
+ - UI rendering model
806
+ - task provider abstraction replacement
807
+ - a generalized event sourcing log
808
+
809
+ It only defines the reducer contract needed to extract the current runtime safely.
810
+
811
+ ---
812
+
813
+ ## 15. Summary
814
+
815
+ The recommended architecture is:
816
+
817
+ - separate reducers per entity family
818
+ - one shared event envelope
819
+ - one shared effect envelope
820
+ - thin coordinator for cross-entity rules
821
+ - runtime integration only in effect execution
822
+
823
+ That gives tasks `#6` and `#7` a concrete target while preserving the current semantics locked down by the new tests.