@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
package/README.md CHANGED
@@ -77,37 +77,48 @@ Works with [@tintinweb/pi-tasks](https://github.com/tintinweb/pi-tasks). Pass `a
77
77
 
78
78
  If `pi-tasks` does not respond during startup detection, `pi-loop` registers a native fallback task system for the session:
79
79
 
80
- - persistent store at `.pi/tasks/tasks.json`
80
+ - session- or project-scoped task files under `.pi/tasks/` depending on `PI_LOOP_SCOPE`
81
81
  - `TaskCreate`, `TaskList`, `TaskUpdate`, `TaskDelete`
82
82
  - `/tasks` interactive viewer
83
- - compact widget task tracking
83
+ - compact status-line task tracking
84
84
 
85
85
  This fallback is session-sticky: `pi-loop` decides once at startup whether `pi-tasks` or native tasks own task management for that session.
86
86
 
87
- ## Widget
87
+ ## Status line
88
88
 
89
- `pi-loop` keeps a compact persistent TUI widget above the editor.
89
+ `pi-loop` keeps a compact persistent status line in the TUI.
90
90
 
91
- It now shows a single focus-friendly status line such as:
91
+ When active work exists, it shows a single focus-friendly line such as:
92
92
 
93
93
  ```text
94
- none
95
94
  1 loop · 1 monitor
96
95
  2 tasks | active: Fix deploy polling
97
96
  1 loop · 2 monitors · 3 tasks | next: Update README
98
97
  ```
99
98
 
100
- Only task counts and the single active/next task are shown in the widget so attention stays on what is currently happening. Use `LoopList`, `MonitorList`, and `/tasks` for detail.
99
+ When no loops, monitors, or native tasks are active, the status line clears completely.
100
+
101
+ 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.
101
102
 
102
103
  ## Configuration
103
104
 
104
105
  | Variable | Effect | Default |
105
106
  |---|---|---|
106
- | `PI_LOOP` | Store path. `off` to disable, absolute or project-relative path | `.pi/loops/loops.json` |
107
+ | `PI_LOOP` | Store path override. `off` to disable, absolute or project-relative path | unset → derived from `PI_LOOP_SCOPE` |
107
108
  | `PI_LOOP_SCOPE` | `memory` (ephemeral), `session` (per-session file), `project` (shared) | `session` |
108
109
  | `PI_LOOP_DEBUG` | Debug logging to stderr | unset |
109
110
 
110
- In `session` scope (default), loop and task files are saved per session ID (e.g. `.pi/tasks/tasks-<sessionId>.json`) so concurrent sessions and worktree agents do not share state. In `memory` scope nothing persists to disk.
111
+ 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.
112
+
113
+ ### Recommended scope policy
114
+
115
+ Keep `PI_LOOP_SCOPE=session` as the default.
116
+
117
+ - `session` is the best balance for normal use: it preserves loops/tasks across a session restart while isolating concurrent sessions and worktrees.
118
+ - `memory` is best for disposable scratch work, tests, or situations where you explicitly do not want any persisted loop/task state.
119
+ - `project` should be opt-in for intentionally shared automation, because it allows multiple sessions in the same repo to see the same persisted state.
120
+
121
+
111
122
 
112
123
  ## Limits
113
124
 
@@ -0,0 +1,22 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { LoopEntry, Trigger } from "../types.js";
3
+ interface LoopStoreLike {
4
+ list(): LoopEntry[];
5
+ get(id: string): LoopEntry | undefined;
6
+ create(trigger: Trigger, prompt: string, options?: Partial<LoopEntry>): LoopEntry;
7
+ pause(id: string): LoopEntry | undefined;
8
+ resume(id: string): LoopEntry | undefined;
9
+ delete(id: string): boolean;
10
+ }
11
+ interface TriggerSystemLike {
12
+ add(entry: LoopEntry): void;
13
+ remove(id: string): void;
14
+ }
15
+ export interface LoopCommandOptions {
16
+ pi: ExtensionAPI;
17
+ getStore: () => LoopStoreLike;
18
+ getTriggerSystem: () => TriggerSystemLike;
19
+ updateWidget: () => void;
20
+ }
21
+ export declare function registerLoopCommand(options: LoopCommandOptions): void;
22
+ export {};
@@ -0,0 +1,148 @@
1
+ import { parseInterval } from "../loop-parse.js";
2
+ export function registerLoopCommand(options) {
3
+ 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;
11
+ try {
12
+ const parsed = parseInterval(interval);
13
+ const trigger = { type: "cron", schedule: parsed.cron };
14
+ const entry = getStore().create(trigger, p, { recurring: true });
15
+ getTriggerSystem().add(entry);
16
+ updateWidget();
17
+ ui.notify(`Loop #${entry.id} created: every ${parsed.description}`, "info");
18
+ }
19
+ catch (err) {
20
+ ui.notify(err.message, "error");
21
+ }
22
+ }
23
+ async function eventLoop(ui, prompt) {
24
+ const p = prompt || await ui.input("Prompt");
25
+ if (!p)
26
+ return;
27
+ const source = await ui.input("Pi event source (e.g., tool_execution_start, before_agent_start)");
28
+ if (!source)
29
+ return;
30
+ const trigger = { type: "event", source };
31
+ const entry = getStore().create(trigger, p, { recurring: true });
32
+ getTriggerSystem().add(entry);
33
+ updateWidget();
34
+ ui.notify(`Event loop #${entry.id} created: fires on "${source}"`, "info");
35
+ }
36
+ async function viewLoops(ui) {
37
+ const loops = getStore().list();
38
+ if (loops.length === 0) {
39
+ await ui.select("No active loops", ["< Back"]);
40
+ return;
41
+ }
42
+ const choices = loops.map((l) => {
43
+ 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})`;
50
+ });
51
+ choices.push("< Back");
52
+ const selected = await ui.select("Active Loops", choices);
53
+ if (!selected || selected === "< Back")
54
+ return;
55
+ const match = selected.match(/#(\d+)/);
56
+ if (match) {
57
+ const entry = getStore().get(match[1]);
58
+ if (entry) {
59
+ const actions = ["x Delete"];
60
+ if (entry.status === "active")
61
+ actions.unshift("- Pause");
62
+ else if (entry.status === "paused")
63
+ actions.unshift("* Resume");
64
+ actions.push("< Back");
65
+ const action = await ui.select(`#${entry.id}: ${entry.prompt}\nTrigger: ${JSON.stringify(entry.trigger)}`, actions);
66
+ if (action === "x Delete") {
67
+ getTriggerSystem().remove(entry.id);
68
+ getStore().delete(entry.id);
69
+ updateWidget();
70
+ ui.notify(`Loop #${entry.id} deleted`, "info");
71
+ }
72
+ else if (action === "- Pause") {
73
+ getStore().pause(entry.id);
74
+ getTriggerSystem().remove(entry.id);
75
+ updateWidget();
76
+ ui.notify(`Loop #${entry.id} paused`, "info");
77
+ }
78
+ else if (action === "* Resume") {
79
+ getStore().resume(entry.id);
80
+ getTriggerSystem().add(entry);
81
+ updateWidget();
82
+ ui.notify(`Loop #${entry.id} resumed`, "info");
83
+ }
84
+ }
85
+ }
86
+ return viewLoops(ui);
87
+ }
88
+ async function settings(ui) {
89
+ const loops = getStore().list();
90
+ const active = loops.filter((l) => l.status === "active").length;
91
+ ui.notify(`${active}/${loops.length} active loops (max 25)`, "info");
92
+ }
93
+ 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",
95
+ handler: async (args, ctx) => {
96
+ const trimmed = args.trim();
97
+ const ui = ctx.ui;
98
+ if (!trimmed) {
99
+ const choice = await ui.select("Loop", [
100
+ "Create scheduled loop",
101
+ "Create event-triggered loop",
102
+ "View active loops",
103
+ "Settings",
104
+ ]);
105
+ if (!choice)
106
+ return;
107
+ if (choice.startsWith("Create scheduled"))
108
+ return scheduleLoop(ui);
109
+ if (choice.startsWith("Create event"))
110
+ return eventLoop(ui);
111
+ if (choice.startsWith("View active"))
112
+ return viewLoops(ui);
113
+ return settings(ui);
114
+ }
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
+ }
134
+ return;
135
+ }
136
+ const choice = await ui.select("Loop mode", [
137
+ `Scheduled: "${trimmed.slice(0, 50)}"`,
138
+ `Event-triggered: "${trimmed.slice(0, 50)}"`,
139
+ `Self-paced: "${trimmed.slice(0, 50)}"`,
140
+ ]);
141
+ if (!choice)
142
+ return;
143
+ if (choice.startsWith("Event"))
144
+ return eventLoop(ui, trimmed);
145
+ return scheduleLoop(ui, trimmed);
146
+ },
147
+ });
148
+ }
@@ -0,0 +1,15 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { TaskStore } from "../task-store.js";
3
+ export interface TaskBacklogResult {
4
+ created: boolean;
5
+ entry?: {
6
+ id: string;
7
+ };
8
+ }
9
+ export interface TasksCommandOptions {
10
+ pi: ExtensionAPI;
11
+ getNativeTaskStore: () => TaskStore | undefined;
12
+ evaluateTaskBacklog: (taskStore: TaskStore, pendingCount: number) => Promise<TaskBacklogResult>;
13
+ updateWidget: () => void;
14
+ }
15
+ export declare function registerTasksCommand(options: TasksCommandOptions): void;
@@ -0,0 +1,117 @@
1
+ export function registerTasksCommand(options) {
2
+ const { pi, getNativeTaskStore, evaluateTaskBacklog, updateWidget } = options;
3
+ async function emitCreated(entry) {
4
+ pi.events.emit("tasks:created", {
5
+ taskId: entry.id,
6
+ subject: entry.subject,
7
+ description: entry.description,
8
+ status: entry.status,
9
+ });
10
+ const taskStore = getNativeTaskStore();
11
+ if (!taskStore)
12
+ return { created: false };
13
+ const backlog = await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
14
+ updateWidget();
15
+ return backlog;
16
+ }
17
+ async function createNativeTaskInteractively(ui) {
18
+ const taskStore = getNativeTaskStore();
19
+ if (!taskStore) {
20
+ ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
21
+ return;
22
+ }
23
+ const subject = await ui.input("Task subject");
24
+ if (!subject)
25
+ return;
26
+ const description = await ui.input("Task description") || subject;
27
+ const entry = taskStore.create(subject, description);
28
+ const backlog = await emitCreated(entry);
29
+ ui.notify(`Task #${entry.id} created`, "info");
30
+ if (backlog.created && backlog.entry) {
31
+ ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
32
+ }
33
+ }
34
+ async function viewNativeTasks(ui) {
35
+ const taskStore = getNativeTaskStore();
36
+ if (!taskStore) {
37
+ ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
38
+ return;
39
+ }
40
+ const tasks = taskStore.list();
41
+ const choices = tasks.map((task) => {
42
+ const icon = task.status === "in_progress" ? ">" : task.status === "completed" ? "ok" : "*";
43
+ return `${icon} #${task.id} [${task.status}] ${task.subject.slice(0, 60)}`;
44
+ });
45
+ choices.unshift("+ Create task");
46
+ choices.push("< Back");
47
+ const selected = await ui.select("Native Tasks", choices);
48
+ if (!selected || selected === "< Back")
49
+ return;
50
+ if (selected === "+ Create task") {
51
+ await createNativeTaskInteractively(ui);
52
+ return viewNativeTasks(ui);
53
+ }
54
+ const match = selected.match(/#(\d+)/);
55
+ if (!match)
56
+ return viewNativeTasks(ui);
57
+ const task = taskStore.get(match[1]);
58
+ if (!task)
59
+ return viewNativeTasks(ui);
60
+ const actions = ["x Delete"];
61
+ if (task.status === "pending") {
62
+ actions.unshift("ok Complete");
63
+ actions.unshift("> Start");
64
+ }
65
+ else if (task.status === "in_progress") {
66
+ actions.unshift("ok Complete");
67
+ actions.unshift("* Return to pending");
68
+ }
69
+ else {
70
+ actions.unshift("* Reopen");
71
+ }
72
+ actions.push("< Back");
73
+ const action = await ui.select(`#${task.id}: ${task.subject}\n\n${task.description}`, actions);
74
+ if (!action || action === "< Back")
75
+ return viewNativeTasks(ui);
76
+ if (action === "x Delete") {
77
+ taskStore.delete(task.id);
78
+ ui.notify(`Task #${task.id} deleted`, "info");
79
+ }
80
+ else if (action === "> Start") {
81
+ taskStore.start(task.id);
82
+ ui.notify(`Task #${task.id} started`, "info");
83
+ }
84
+ else if (action === "ok Complete") {
85
+ taskStore.complete(task.id);
86
+ ui.notify(`Task #${task.id} completed`, "info");
87
+ }
88
+ else if (action === "* Return to pending" || action === "* Reopen") {
89
+ taskStore.reopen(task.id);
90
+ ui.notify(`Task #${task.id} reopened`, "info");
91
+ }
92
+ updateWidget();
93
+ await evaluateTaskBacklog(taskStore, taskStore.pendingCount());
94
+ return viewNativeTasks(ui);
95
+ }
96
+ pi.registerCommand("tasks", {
97
+ description: "View or manage native pi-loop tasks when pi-tasks is not installed",
98
+ handler: async (args, ctx) => {
99
+ const trimmed = args.trim();
100
+ const taskStore = getNativeTaskStore();
101
+ if (!taskStore) {
102
+ ctx.ui.notify("Native tasks are unavailable while pi-tasks is active", "warning");
103
+ return;
104
+ }
105
+ if (trimmed) {
106
+ const entry = taskStore.create(trimmed.slice(0, 80), trimmed);
107
+ const backlog = await emitCreated(entry);
108
+ ctx.ui.notify(`Task #${entry.id} created`, "info");
109
+ if (backlog.created && backlog.entry) {
110
+ ctx.ui.notify(`Worker loop #${backlog.entry.id} auto-created`, "info");
111
+ }
112
+ return;
113
+ }
114
+ await viewNativeTasks(ctx.ui);
115
+ },
116
+ });
117
+ }
@@ -0,0 +1,35 @@
1
+ export type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
2
+ export type ReducerEntityType = "task" | "loop" | "monitor" | "notification" | "goal";
3
+ export interface ReducerEvent<TType extends string = string, TPayload = unknown> {
4
+ type: TType;
5
+ at: number;
6
+ source: ReducerSource;
7
+ entityType?: ReducerEntityType;
8
+ entityId?: string;
9
+ payload: TPayload;
10
+ }
11
+ export interface ReducerEffect<TEffect extends string = string, TPayload = unknown> {
12
+ type: TEffect;
13
+ entityType?: ReducerEntityType;
14
+ entityId?: string;
15
+ payload: TPayload;
16
+ }
17
+ export type DispatchEventEffect = ReducerEffect<"DISPATCH_EVENT", {
18
+ event: ReducerEvent;
19
+ }>;
20
+ export type AnyReducerEffect = ReducerEffect | DispatchEventEffect;
21
+ export type ReducerHandler = (event: ReducerEvent) => undefined | AnyReducerEffect[] | Promise<undefined | AnyReducerEffect[]>;
22
+ export type EffectHandler = (effect: ReducerEffect) => void | Promise<void>;
23
+ export interface CoordinatorOptions {
24
+ reducers: ReducerHandler[];
25
+ effectHandlers?: Partial<Record<string, EffectHandler>>;
26
+ effectExecutor?: EffectHandler;
27
+ maxDispatchDepth?: number;
28
+ }
29
+ export declare class CoordinatorError extends Error {
30
+ constructor(message: string);
31
+ }
32
+ export interface Coordinator {
33
+ dispatch(event: ReducerEvent): Promise<void>;
34
+ }
35
+ export declare function createCoordinator(options: CoordinatorOptions): Coordinator;
@@ -0,0 +1,56 @@
1
+ export class CoordinatorError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = "CoordinatorError";
5
+ }
6
+ }
7
+ export function createCoordinator(options) {
8
+ const { reducers, effectHandlers = {}, effectExecutor, maxDispatchDepth = 100, } = options;
9
+ function isPromiseLike(value) {
10
+ return typeof value === "object" && value !== null && "then" in value;
11
+ }
12
+ async function executeEffect(effect, depth) {
13
+ if (effect.type === "DISPATCH_EVENT") {
14
+ const dispatchEffect = effect;
15
+ const derivedEvent = dispatchEffect.payload.event;
16
+ if (!derivedEvent) {
17
+ throw new CoordinatorError("DISPATCH_EVENT effect missing payload.event");
18
+ }
19
+ await dispatchAtDepth(derivedEvent, depth + 1);
20
+ return;
21
+ }
22
+ const specificHandler = effectHandlers[effect.type];
23
+ if (specificHandler) {
24
+ const handled = specificHandler(effect);
25
+ if (isPromiseLike(handled))
26
+ await handled;
27
+ return;
28
+ }
29
+ if (effectExecutor) {
30
+ const handled = effectExecutor(effect);
31
+ if (isPromiseLike(handled))
32
+ await handled;
33
+ }
34
+ }
35
+ async function dispatchAtDepth(event, depth) {
36
+ if (depth > maxDispatchDepth) {
37
+ throw new CoordinatorError(`Maximum dispatch depth exceeded (${maxDispatchDepth})`);
38
+ }
39
+ const effects = [];
40
+ for (const reducer of reducers) {
41
+ const emitted = reducer(event);
42
+ const resolved = isPromiseLike(emitted) ? await emitted : emitted;
43
+ if (!resolved || resolved.length === 0)
44
+ continue;
45
+ effects.push(...resolved);
46
+ }
47
+ for (const effect of effects) {
48
+ await executeEffect(effect, depth);
49
+ }
50
+ }
51
+ return {
52
+ async dispatch(event) {
53
+ await dispatchAtDepth(event, 1);
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,22 @@
1
+ import type { ReducerEffect, ReducerEvent } from "./coordinator.js";
2
+ import type { GoalReducerState } from "./goal-types.js";
3
+ import type { LoopReducerState } from "./loop-reducer.js";
4
+ import type { MonitorReducerState } from "./monitor-reducer.js";
5
+ import type { TaskReducerState } from "./task-reducer.js";
6
+ export type GoalVerificationRequestedEvent = ReducerEvent<"GOAL_VERIFICATION_REQUESTED", {
7
+ id: string;
8
+ }>;
9
+ export interface GoalCoordinatorSnapshot {
10
+ goalState: GoalReducerState;
11
+ taskState: TaskReducerState;
12
+ loopState: LoopReducerState;
13
+ monitorState: MonitorReducerState;
14
+ }
15
+ export declare function reduceGoalVerificationRequest(event: GoalVerificationRequestedEvent, snapshot: GoalCoordinatorSnapshot): ReducerEffect[];
16
+ export interface GoalCoordinatorOptions {
17
+ getGoalState: () => GoalReducerState;
18
+ getTaskState: () => TaskReducerState;
19
+ getLoopState: () => LoopReducerState;
20
+ getMonitorState: () => MonitorReducerState;
21
+ }
22
+ export declare function createGoalCoordinatorReducer(options: GoalCoordinatorOptions): (event: ReducerEvent) => ReducerEffect[];
@@ -0,0 +1,28 @@
1
+ import { verifyGoal } from "./goal-verifier.js";
2
+ export function reduceGoalVerificationRequest(event, snapshot) {
3
+ if (event.type !== "GOAL_VERIFICATION_REQUESTED")
4
+ return [];
5
+ const goal = snapshot.goalState.goalsById[event.payload.id];
6
+ if (!goal)
7
+ return [];
8
+ return verifyGoal({
9
+ goal,
10
+ taskState: snapshot.taskState,
11
+ loopState: snapshot.loopState,
12
+ monitorState: snapshot.monitorState,
13
+ at: event.at,
14
+ }).effects;
15
+ }
16
+ export function createGoalCoordinatorReducer(options) {
17
+ const { getGoalState, getTaskState, getLoopState, getMonitorState } = options;
18
+ return (event) => {
19
+ if (event.type !== "GOAL_VERIFICATION_REQUESTED")
20
+ return [];
21
+ return reduceGoalVerificationRequest(event, {
22
+ goalState: getGoalState(),
23
+ taskState: getTaskState(),
24
+ loopState: getLoopState(),
25
+ monitorState: getMonitorState(),
26
+ });
27
+ };
28
+ }
@@ -0,0 +1,89 @@
1
+ import type { GoalCriteria, GoalEntry, GoalProgressSnapshot, GoalReducerState, GoalScope } from "./goal-types.js";
2
+ export type GoalReducerEvent = {
3
+ type: "GOAL_CREATED";
4
+ at: number;
5
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
6
+ entityType?: "goal";
7
+ entityId?: string;
8
+ payload: {
9
+ title: string;
10
+ description: string;
11
+ scope: GoalScope;
12
+ criteria: GoalCriteria;
13
+ metadata?: Record<string, unknown>;
14
+ };
15
+ } | {
16
+ type: "GOAL_ACTIVATED" | "GOAL_VERIFICATION_STARTED" | "GOAL_UNBLOCKED";
17
+ at: number;
18
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
19
+ entityType?: "goal";
20
+ entityId?: string;
21
+ payload: {
22
+ id: string;
23
+ };
24
+ } | {
25
+ type: "GOAL_PROGRESS_RECORDED";
26
+ at: number;
27
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
28
+ entityType?: "goal";
29
+ entityId?: string;
30
+ payload: {
31
+ id: string;
32
+ progress: GoalProgressSnapshot;
33
+ };
34
+ } | {
35
+ type: "GOAL_VERIFICATION_PASSED" | "GOAL_VERIFICATION_FAILED" | "GOAL_BLOCKED" | "GOAL_FAILED";
36
+ at: number;
37
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
38
+ entityType?: "goal";
39
+ entityId?: string;
40
+ payload: {
41
+ id: string;
42
+ reason: string;
43
+ progress?: GoalProgressSnapshot;
44
+ };
45
+ } | {
46
+ type: "GOAL_SATISFIED" | "GOAL_ARCHIVED";
47
+ at: number;
48
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
49
+ entityType?: "goal";
50
+ entityId?: string;
51
+ payload: {
52
+ id: string;
53
+ reason?: string;
54
+ };
55
+ } | {
56
+ type: "GOAL_UPDATED";
57
+ at: number;
58
+ source: "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
59
+ entityType?: "goal";
60
+ entityId?: string;
61
+ payload: {
62
+ id: string;
63
+ title?: string;
64
+ description?: string;
65
+ scope?: GoalScope;
66
+ criteria?: GoalCriteria;
67
+ metadata?: Record<string, unknown>;
68
+ };
69
+ };
70
+ export type GoalReducerEffect = {
71
+ type: "PERSIST_GOAL";
72
+ entityType: "goal";
73
+ entityId: string;
74
+ payload: {
75
+ goal: GoalEntry;
76
+ };
77
+ } | {
78
+ type: "REQUEST_GOAL_VERIFICATION";
79
+ entityType: "goal";
80
+ entityId: string;
81
+ payload: {
82
+ id: string;
83
+ };
84
+ };
85
+ export interface GoalReduceResult {
86
+ state: GoalReducerState;
87
+ effects: GoalReducerEffect[];
88
+ }
89
+ export declare function reduceGoalState(state: GoalReducerState, event: GoalReducerEvent): GoalReduceResult;