@trevonistrevon/pi-loop 0.4.10 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -0
- package/dist/goal-types.d.ts +78 -0
- package/dist/goal-types.js +1 -0
- package/dist/goal-verifier.d.ts +20 -0
- package/dist/goal-verifier.js +198 -0
- package/dist/index.js +182 -40
- package/dist/loop-reducer.d.ts +63 -0
- package/dist/loop-reducer.js +67 -0
- package/dist/monitor-completion-coordinator.d.ts +10 -0
- package/dist/monitor-completion-coordinator.js +13 -0
- package/dist/monitor-manager.d.ts +2 -0
- package/dist/monitor-manager.js +107 -29
- package/dist/monitor-reducer.d.ts +82 -0
- package/dist/monitor-reducer.js +69 -0
- package/dist/notification-reducer.d.ts +81 -0
- package/dist/notification-reducer.js +65 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +118 -44
- package/dist/task-backlog-coordinator.d.ts +12 -0
- package/dist/task-backlog-coordinator.js +22 -0
- package/dist/task-reducer.d.ts +66 -0
- package/dist/task-reducer.js +76 -0
- package/dist/task-store.d.ts +2 -0
- package/dist/task-store.js +82 -30
- package/docs/architecture/goal-state-schema.md +505 -0
- package/docs/architecture/state-machine-migration.md +546 -0
- package/docs/architecture/state-machine-reducer-event-model.md +823 -0
- package/docs/architecture/state-machine-test-matrix.md +249 -0
- package/docs/architecture/state-machine-transition-map.md +436 -0
- package/package.json +1 -1
- package/src/coordinator.ts +115 -0
- package/src/goal-types.ts +99 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +209 -39
- package/src/loop-reducer.ts +148 -0
- package/src/monitor-completion-coordinator.ts +24 -0
- package/src/monitor-manager.ts +115 -27
- package/src/monitor-reducer.ts +166 -0
- package/src/notification-reducer.ts +155 -0
- package/src/store.ts +119 -43
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +84 -27
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
-
pi install
|
|
9
|
+
pi install npm:@trevonistrevon/pi-loop
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
## Quick start
|
|
@@ -109,6 +109,16 @@ Only task counts and the single active/next task are shown in the widget so atte
|
|
|
109
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
111
|
|
|
112
|
+
### Recommended scope policy
|
|
113
|
+
|
|
114
|
+
Keep `PI_LOOP_SCOPE=session` as the default.
|
|
115
|
+
|
|
116
|
+
- `session` is the best balance for normal use: it preserves loops/tasks across a session restart while isolating concurrent sessions and worktrees.
|
|
117
|
+
- `memory` is best for disposable scratch work, tests, or situations where you explicitly do not want any persisted loop/task state.
|
|
118
|
+
- `project` should be opt-in for intentionally shared automation, because it allows multiple sessions in the same repo to see the same persisted state.
|
|
119
|
+
|
|
120
|
+
This matches the current wake model well: pending notifications stay in memory and are cancelable, while durable loop/task intent remains scoped per session.
|
|
121
|
+
|
|
112
122
|
## Limits
|
|
113
123
|
|
|
114
124
|
25 active loops, 25 running monitors. Recurring loops expire after 7 days.
|
|
@@ -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,78 @@
|
|
|
1
|
+
export type GoalStatus = "pending" | "active" | "satisfied" | "blocked" | "failed" | "archived";
|
|
2
|
+
export type GoalVerificationStatus = "unknown" | "checking" | "verified" | "unverified" | "inconclusive";
|
|
3
|
+
export interface GoalScope {
|
|
4
|
+
taskIds?: string[];
|
|
5
|
+
loopIds?: string[];
|
|
6
|
+
monitorIds?: string[];
|
|
7
|
+
tags?: string[];
|
|
8
|
+
subjectPrefixes?: string[];
|
|
9
|
+
includeFutureMatchingTasks?: boolean;
|
|
10
|
+
includeFutureMatchingLoops?: boolean;
|
|
11
|
+
includeFutureMatchingMonitors?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface GoalSuccessCriteria {
|
|
14
|
+
minCompletedTasks?: number;
|
|
15
|
+
requiredTaskIds?: string[];
|
|
16
|
+
requiredMonitorIdsCompleted?: string[];
|
|
17
|
+
requiredLoopIdsPresent?: string[];
|
|
18
|
+
requireNoPendingTasksInScope?: boolean;
|
|
19
|
+
requireLatestVerificationPass?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface GoalFailureCriteria {
|
|
22
|
+
anyMonitorIdsErrored?: string[];
|
|
23
|
+
maxVerificationFailures?: number;
|
|
24
|
+
failIfTaskIdsDeleted?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface GoalBlockedCriteria {
|
|
27
|
+
blockedIfAllTasksCompletedButVerificationFails?: boolean;
|
|
28
|
+
blockedIfNoScopedProgressSinceMs?: number;
|
|
29
|
+
blockedIfRequiredLoopMissing?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface GoalCriteria {
|
|
32
|
+
success: GoalSuccessCriteria;
|
|
33
|
+
failure?: GoalFailureCriteria;
|
|
34
|
+
blocked?: GoalBlockedCriteria;
|
|
35
|
+
}
|
|
36
|
+
export interface GoalProgressSnapshot {
|
|
37
|
+
totalTasks: number;
|
|
38
|
+
pendingTasks: number;
|
|
39
|
+
inProgressTasks: number;
|
|
40
|
+
completedTasks: number;
|
|
41
|
+
activeLoops: number;
|
|
42
|
+
pausedLoops: number;
|
|
43
|
+
runningMonitors: number;
|
|
44
|
+
completedMonitors: number;
|
|
45
|
+
erroredMonitors: number;
|
|
46
|
+
stoppedMonitors: number;
|
|
47
|
+
lastProgressAt?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface GoalVerificationState {
|
|
50
|
+
attempts: number;
|
|
51
|
+
passes: number;
|
|
52
|
+
failures: number;
|
|
53
|
+
lastCheckedAt?: number;
|
|
54
|
+
lastPassedAt?: number;
|
|
55
|
+
lastFailedAt?: number;
|
|
56
|
+
lastReason?: string;
|
|
57
|
+
nextCheckAfter?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface GoalEntry {
|
|
60
|
+
id: string;
|
|
61
|
+
title: string;
|
|
62
|
+
description: string;
|
|
63
|
+
status: GoalStatus;
|
|
64
|
+
verificationStatus: GoalVerificationStatus;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
activatedAt?: number;
|
|
68
|
+
resolvedAt?: number;
|
|
69
|
+
scope: GoalScope;
|
|
70
|
+
criteria: GoalCriteria;
|
|
71
|
+
progress: GoalProgressSnapshot;
|
|
72
|
+
verification: GoalVerificationState;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
export interface GoalReducerState {
|
|
76
|
+
nextId: number;
|
|
77
|
+
goalsById: Record<string, GoalEntry>;
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReducerEffect } from "./coordinator.js";
|
|
2
|
+
import type { GoalEntry, GoalProgressSnapshot } 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 interface GoalVerifierInput {
|
|
7
|
+
goal: GoalEntry;
|
|
8
|
+
taskState: TaskReducerState;
|
|
9
|
+
loopState: LoopReducerState;
|
|
10
|
+
monitorState: MonitorReducerState;
|
|
11
|
+
at: number;
|
|
12
|
+
}
|
|
13
|
+
export interface GoalVerifierResult {
|
|
14
|
+
progress: GoalProgressSnapshot;
|
|
15
|
+
verdict: "passed" | "failed" | "blocked";
|
|
16
|
+
reason: string;
|
|
17
|
+
effects: ReducerEffect[];
|
|
18
|
+
}
|
|
19
|
+
export declare function projectGoalProgress(goal: GoalEntry, taskState: TaskReducerState, loopState: LoopReducerState, monitorState: MonitorReducerState): GoalProgressSnapshot;
|
|
20
|
+
export declare function verifyGoal(input: GoalVerifierInput): GoalVerifierResult;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
function uniqueById(items) {
|
|
2
|
+
const seen = new Set();
|
|
3
|
+
const result = [];
|
|
4
|
+
for (const item of items) {
|
|
5
|
+
if (seen.has(item.id))
|
|
6
|
+
continue;
|
|
7
|
+
seen.add(item.id);
|
|
8
|
+
result.push(item);
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
function selectTasks(goal, taskState) {
|
|
13
|
+
const selected = [];
|
|
14
|
+
for (const id of goal.scope.taskIds ?? []) {
|
|
15
|
+
const task = taskState.tasksById[id];
|
|
16
|
+
if (task)
|
|
17
|
+
selected.push(task);
|
|
18
|
+
}
|
|
19
|
+
for (const prefix of goal.scope.subjectPrefixes ?? []) {
|
|
20
|
+
for (const task of Object.values(taskState.tasksById)) {
|
|
21
|
+
if (task.subject.startsWith(prefix))
|
|
22
|
+
selected.push(task);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return uniqueById(selected);
|
|
26
|
+
}
|
|
27
|
+
function selectLoops(goal, loopState) {
|
|
28
|
+
const selected = [];
|
|
29
|
+
for (const id of goal.scope.loopIds ?? []) {
|
|
30
|
+
const loop = loopState.loopsById[id];
|
|
31
|
+
if (loop)
|
|
32
|
+
selected.push(loop);
|
|
33
|
+
}
|
|
34
|
+
return uniqueById(selected);
|
|
35
|
+
}
|
|
36
|
+
function selectMonitors(goal, monitorState) {
|
|
37
|
+
const selected = [];
|
|
38
|
+
for (const id of goal.scope.monitorIds ?? []) {
|
|
39
|
+
const monitor = monitorState.monitorsById[id];
|
|
40
|
+
if (monitor)
|
|
41
|
+
selected.push(monitor);
|
|
42
|
+
}
|
|
43
|
+
return uniqueById(selected);
|
|
44
|
+
}
|
|
45
|
+
export function projectGoalProgress(goal, taskState, loopState, monitorState) {
|
|
46
|
+
const tasks = selectTasks(goal, taskState);
|
|
47
|
+
const loops = selectLoops(goal, loopState);
|
|
48
|
+
const monitors = selectMonitors(goal, monitorState);
|
|
49
|
+
const timestamps = [];
|
|
50
|
+
for (const task of tasks)
|
|
51
|
+
timestamps.push(task.updatedAt);
|
|
52
|
+
for (const loop of loops)
|
|
53
|
+
timestamps.push(loop.updatedAt);
|
|
54
|
+
for (const monitor of monitors)
|
|
55
|
+
timestamps.push(monitor.completedAt ?? monitor.startedAt);
|
|
56
|
+
return {
|
|
57
|
+
totalTasks: tasks.length,
|
|
58
|
+
pendingTasks: tasks.filter(task => task.status === "pending").length,
|
|
59
|
+
inProgressTasks: tasks.filter(task => task.status === "in_progress").length,
|
|
60
|
+
completedTasks: tasks.filter(task => task.status === "completed").length,
|
|
61
|
+
activeLoops: loops.filter(loop => loop.status === "active").length,
|
|
62
|
+
pausedLoops: loops.filter(loop => loop.status === "paused").length,
|
|
63
|
+
runningMonitors: monitors.filter(monitor => monitor.status === "running").length,
|
|
64
|
+
completedMonitors: monitors.filter(monitor => monitor.status === "completed").length,
|
|
65
|
+
erroredMonitors: monitors.filter(monitor => monitor.status === "error").length,
|
|
66
|
+
stoppedMonitors: monitors.filter(monitor => monitor.status === "stopped").length,
|
|
67
|
+
lastProgressAt: timestamps.length > 0 ? Math.max(...timestamps) : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function dispatchEffect(event) {
|
|
71
|
+
return {
|
|
72
|
+
type: "DISPATCH_EVENT",
|
|
73
|
+
entityType: "goal",
|
|
74
|
+
entityId: event.entityId,
|
|
75
|
+
payload: { event },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildEffects(goal, at, progress, resultType, reason) {
|
|
79
|
+
return [
|
|
80
|
+
dispatchEffect({
|
|
81
|
+
type: "GOAL_VERIFICATION_STARTED",
|
|
82
|
+
at,
|
|
83
|
+
source: "coordinator",
|
|
84
|
+
entityType: "goal",
|
|
85
|
+
entityId: goal.id,
|
|
86
|
+
payload: { id: goal.id },
|
|
87
|
+
}),
|
|
88
|
+
dispatchEffect({
|
|
89
|
+
type: "GOAL_PROGRESS_RECORDED",
|
|
90
|
+
at,
|
|
91
|
+
source: "coordinator",
|
|
92
|
+
entityType: "goal",
|
|
93
|
+
entityId: goal.id,
|
|
94
|
+
payload: { id: goal.id, progress },
|
|
95
|
+
}),
|
|
96
|
+
dispatchEffect({
|
|
97
|
+
type: resultType,
|
|
98
|
+
at,
|
|
99
|
+
source: "coordinator",
|
|
100
|
+
entityType: "goal",
|
|
101
|
+
entityId: goal.id,
|
|
102
|
+
payload: { id: goal.id, reason, progress },
|
|
103
|
+
}),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
export function verifyGoal(input) {
|
|
107
|
+
const { goal, taskState, loopState, monitorState, at } = input;
|
|
108
|
+
const progress = projectGoalProgress(goal, taskState, loopState, monitorState);
|
|
109
|
+
if (goal.criteria.failure?.maxVerificationFailures !== undefined
|
|
110
|
+
&& goal.verification.failures >= goal.criteria.failure.maxVerificationFailures) {
|
|
111
|
+
return {
|
|
112
|
+
progress,
|
|
113
|
+
verdict: "failed",
|
|
114
|
+
reason: "maximum verification failures reached",
|
|
115
|
+
effects: buildEffects(goal, at, progress, "GOAL_FAILED", "maximum verification failures reached"),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
for (const monitorId of goal.criteria.failure?.anyMonitorIdsErrored ?? []) {
|
|
119
|
+
if (monitorState.monitorsById[monitorId]?.status === "error") {
|
|
120
|
+
return {
|
|
121
|
+
progress,
|
|
122
|
+
verdict: "failed",
|
|
123
|
+
reason: `monitor #${monitorId} errored`,
|
|
124
|
+
effects: buildEffects(goal, at, progress, "GOAL_FAILED", `monitor #${monitorId} errored`),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const taskId of goal.criteria.failure?.failIfTaskIdsDeleted ?? []) {
|
|
129
|
+
if (!taskState.tasksById[taskId]) {
|
|
130
|
+
return {
|
|
131
|
+
progress,
|
|
132
|
+
verdict: "failed",
|
|
133
|
+
reason: `task #${taskId} missing`,
|
|
134
|
+
effects: buildEffects(goal, at, progress, "GOAL_FAILED", `task #${taskId} missing`),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const requiredTasksDone = (goal.criteria.success.requiredTaskIds ?? [])
|
|
139
|
+
.every(taskId => taskState.tasksById[taskId]?.status === "completed");
|
|
140
|
+
const requiredMonitorsDone = (goal.criteria.success.requiredMonitorIdsCompleted ?? [])
|
|
141
|
+
.every(monitorId => monitorState.monitorsById[monitorId]?.status === "completed");
|
|
142
|
+
const requiredLoopsPresent = (goal.criteria.success.requiredLoopIdsPresent ?? [])
|
|
143
|
+
.every(loopId => Boolean(loopState.loopsById[loopId]));
|
|
144
|
+
const minCompletedTasksMet = progress.completedTasks >= (goal.criteria.success.minCompletedTasks ?? 0);
|
|
145
|
+
const noPendingWork = !goal.criteria.success.requireNoPendingTasksInScope
|
|
146
|
+
|| (progress.pendingTasks === 0 && progress.inProgressTasks === 0);
|
|
147
|
+
const latestVerificationPass = !goal.criteria.success.requireLatestVerificationPass
|
|
148
|
+
|| goal.verificationStatus === "verified";
|
|
149
|
+
const success = requiredTasksDone
|
|
150
|
+
&& requiredMonitorsDone
|
|
151
|
+
&& requiredLoopsPresent
|
|
152
|
+
&& minCompletedTasksMet
|
|
153
|
+
&& noPendingWork
|
|
154
|
+
&& latestVerificationPass;
|
|
155
|
+
if (success) {
|
|
156
|
+
return {
|
|
157
|
+
progress,
|
|
158
|
+
verdict: "passed",
|
|
159
|
+
reason: "success criteria satisfied",
|
|
160
|
+
effects: buildEffects(goal, at, progress, "GOAL_VERIFICATION_PASSED", "success criteria satisfied"),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (goal.criteria.blocked?.blockedIfRequiredLoopMissing && !requiredLoopsPresent) {
|
|
164
|
+
return {
|
|
165
|
+
progress,
|
|
166
|
+
verdict: "blocked",
|
|
167
|
+
reason: "required loop missing",
|
|
168
|
+
effects: buildEffects(goal, at, progress, "GOAL_BLOCKED", "required loop missing"),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (goal.criteria.blocked?.blockedIfNoScopedProgressSinceMs !== undefined
|
|
172
|
+
&& progress.lastProgressAt !== undefined
|
|
173
|
+
&& at - progress.lastProgressAt >= goal.criteria.blocked.blockedIfNoScopedProgressSinceMs) {
|
|
174
|
+
return {
|
|
175
|
+
progress,
|
|
176
|
+
verdict: "blocked",
|
|
177
|
+
reason: "no scoped progress within configured interval",
|
|
178
|
+
effects: buildEffects(goal, at, progress, "GOAL_BLOCKED", "no scoped progress within configured interval"),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (goal.criteria.blocked?.blockedIfAllTasksCompletedButVerificationFails
|
|
182
|
+
&& progress.totalTasks > 0
|
|
183
|
+
&& progress.pendingTasks === 0
|
|
184
|
+
&& progress.inProgressTasks === 0) {
|
|
185
|
+
return {
|
|
186
|
+
progress,
|
|
187
|
+
verdict: "blocked",
|
|
188
|
+
reason: "all scoped tasks completed but verification has not passed",
|
|
189
|
+
effects: buildEffects(goal, at, progress, "GOAL_BLOCKED", "all scoped tasks completed but verification has not passed"),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
progress,
|
|
194
|
+
verdict: "failed",
|
|
195
|
+
reason: "success criteria not yet satisfied",
|
|
196
|
+
effects: buildEffects(goal, at, progress, "GOAL_VERIFICATION_FAILED", "success criteria not yet satisfied"),
|
|
197
|
+
};
|
|
198
|
+
}
|