@polpo-ai/core 0.11.1 → 0.12.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.
- package/dist/__tests__/mission-store.test.js +9 -13
- package/dist/__tests__/mission-store.test.js.map +1 -1
- package/dist/adapter.d.ts +10 -19
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +0 -7
- package/dist/adapter.js.map +1 -1
- package/dist/approval-manager.js +11 -11
- package/dist/approval-manager.js.map +1 -1
- package/dist/assessment-orchestrator.d.ts +1 -1
- package/dist/assessment-orchestrator.js +46 -46
- package/dist/assessment-orchestrator.js.map +1 -1
- package/dist/context-compactor.d.ts +1 -0
- package/dist/context-compactor.d.ts.map +1 -1
- package/dist/context-compactor.js.map +1 -1
- package/dist/escalation-manager.js +8 -8
- package/dist/escalation-manager.js.map +1 -1
- package/dist/events.d.ts +1 -25
- package/dist/events.d.ts.map +1 -1
- package/dist/hooks.d.ts +15 -9
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js.map +1 -1
- package/dist/in-memory-stores.d.ts +21 -0
- package/dist/in-memory-stores.d.ts.map +1 -0
- package/dist/in-memory-stores.js +43 -0
- package/dist/in-memory-stores.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loop/hooks.d.ts +10 -28
- package/dist/loop/hooks.d.ts.map +1 -1
- package/dist/loop/hooks.js +2 -83
- package/dist/loop/hooks.js.map +1 -1
- package/dist/loop/step-helpers.d.ts +20 -0
- package/dist/loop/step-helpers.d.ts.map +1 -0
- package/dist/loop/step-helpers.js +69 -0
- package/dist/loop/step-helpers.js.map +1 -0
- package/dist/mission-executor.d.ts +16 -22
- package/dist/mission-executor.d.ts.map +1 -1
- package/dist/mission-executor.js +204 -459
- package/dist/mission-executor.js.map +1 -1
- package/dist/mission-gating.d.ts +89 -0
- package/dist/mission-gating.d.ts.map +1 -0
- package/dist/mission-gating.js +281 -0
- package/dist/mission-gating.js.map +1 -0
- package/dist/mission-store.d.ts +14 -8
- package/dist/mission-store.d.ts.map +1 -1
- package/dist/mission-store.js +19 -8
- package/dist/mission-store.js.map +1 -1
- package/dist/orchestrator-context.d.ts +1 -1
- package/dist/orchestrator-context.d.ts.map +1 -1
- package/dist/orchestrator-engine.d.ts +6 -6
- package/dist/orchestrator-engine.d.ts.map +1 -1
- package/dist/orchestrator-engine.js +10 -10
- package/dist/orchestrator-engine.js.map +1 -1
- package/dist/playbook-logic.d.ts +1 -1
- package/dist/playbook-logic.js +1 -1
- package/dist/scheduler.js +1 -1
- package/dist/scheduler.js.map +1 -1
- package/dist/schemas.d.ts +2 -2
- package/dist/session-store.d.ts +0 -2
- package/dist/session-store.d.ts.map +1 -1
- package/dist/session-store.js.map +1 -1
- package/dist/sla-monitor.js +3 -3
- package/dist/sla-monitor.js.map +1 -1
- package/dist/task-manager.d.ts +1 -1
- package/dist/task-manager.d.ts.map +1 -1
- package/dist/task-manager.js +37 -37
- package/dist/task-manager.js.map +1 -1
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +19 -36
- package/dist/task-runner.js.map +1 -1
- package/dist/task-store.d.ts +5 -19
- package/dist/task-store.d.ts.map +1 -1
- package/dist/tool-types.d.ts +0 -2
- package/dist/tool-types.d.ts.map +1 -1
- package/dist/types/agent.d.ts +186 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +23 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/assessment.d.ts +134 -0
- package/dist/types/assessment.d.ts.map +1 -0
- package/dist/types/assessment.js +18 -0
- package/dist/types/assessment.js.map +1 -0
- package/dist/types/config.d.ts +193 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +8 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/mission.d.ts +226 -0
- package/dist/types/mission.d.ts.map +1 -0
- package/dist/types/mission.js +6 -0
- package/dist/types/mission.js.map +1 -0
- package/dist/types/notifications.d.ts +162 -0
- package/dist/types/notifications.d.ts.map +1 -0
- package/dist/types/notifications.js +6 -0
- package/dist/types/notifications.js.map +1 -0
- package/dist/types/task.d.ts +191 -0
- package/dist/types/task.d.ts.map +1 -0
- package/dist/types/task.js +5 -0
- package/dist/types/task.js.map +1 -0
- package/dist/types.d.ts +17 -1150
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +18 -31
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/attachment-store.d.ts +0 -42
- package/dist/attachment-store.d.ts.map +0 -1
- package/dist/attachment-store.js +0 -13
- package/dist/attachment-store.js.map +0 -1
package/dist/mission-executor.js
CHANGED
|
@@ -1,44 +1,10 @@
|
|
|
1
1
|
import { sanitizeExpectations, parseMissionDocument } from "./schemas.js";
|
|
2
|
-
import { resolveMissionStore } from "./mission-store.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
state = { definitions: {}, active: {}, resumed: [] };
|
|
6
|
-
async load() {
|
|
7
|
-
return this.state;
|
|
8
|
-
}
|
|
9
|
-
async save(state) {
|
|
10
|
-
this.state = state;
|
|
11
|
-
}
|
|
12
|
-
async removeGroup(state, group) {
|
|
13
|
-
delete state.definitions[group];
|
|
14
|
-
for (const key of Object.keys(state.active)) {
|
|
15
|
-
if (key.startsWith(group + ":"))
|
|
16
|
-
delete state.active[key];
|
|
17
|
-
}
|
|
18
|
-
state.resumed = state.resumed.filter(k => !k.startsWith(group + ":"));
|
|
19
|
-
return state;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
class InMemoryDelayStore {
|
|
23
|
-
state = { definitions: {}, active: {}, expired: [] };
|
|
24
|
-
async load() {
|
|
25
|
-
return this.state;
|
|
26
|
-
}
|
|
27
|
-
async save(state) {
|
|
28
|
-
this.state = state;
|
|
29
|
-
}
|
|
30
|
-
async removeGroup(state, group) {
|
|
31
|
-
delete state.definitions[group];
|
|
32
|
-
for (const key of Object.keys(state.active)) {
|
|
33
|
-
if (key.startsWith(group + ":"))
|
|
34
|
-
delete state.active[key];
|
|
35
|
-
}
|
|
36
|
-
state.expired = state.expired.filter(k => !k.startsWith(group + ":"));
|
|
37
|
-
return state;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
2
|
+
import { resolveMissionStore, resolveMissionForGroup } from "./mission-store.js";
|
|
3
|
+
import { InMemoryCheckpointStore, InMemoryDelayStore } from "./in-memory-stores.js";
|
|
4
|
+
import { MissionGating, parseISO8601Duration } from "./mission-gating.js";
|
|
40
5
|
/**
|
|
41
6
|
* Mission CRUD + execution + resume + group lifecycle.
|
|
7
|
+
* Runtime checkpoint/delay gating is delegated to MissionGating.
|
|
42
8
|
*/
|
|
43
9
|
export class MissionExecutor {
|
|
44
10
|
ctx;
|
|
@@ -47,17 +13,11 @@ export class MissionExecutor {
|
|
|
47
13
|
cleanedGroups = new Set();
|
|
48
14
|
/** Quality gates parsed from mission documents, keyed by mission group name */
|
|
49
15
|
gatesByGroup = new Map();
|
|
50
|
-
/** Persistent checkpoint store — survives server restarts */
|
|
51
16
|
missions;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
cpState;
|
|
17
|
+
/** Runtime checkpoint/delay gating — owns the persisted checkpoint/delay state */
|
|
18
|
+
gating;
|
|
55
19
|
/** Optional quality controller — set by orchestrator after init */
|
|
56
20
|
qualityCtrl;
|
|
57
|
-
/** Persistent delay store — survives server restarts */
|
|
58
|
-
delayStore;
|
|
59
|
-
/** In-memory mirror of persisted delay state (synced on every mutation) */
|
|
60
|
-
delayState;
|
|
61
21
|
/** Track the pre-execution status for scheduled/recurring missions (by group name).
|
|
62
22
|
* When a mission completes/fails, this determines whether to return to scheduled/recurring. */
|
|
63
23
|
scheduledOrigin = new Map();
|
|
@@ -66,8 +26,7 @@ export class MissionExecutor {
|
|
|
66
26
|
this.taskMgr = taskMgr;
|
|
67
27
|
this.agentMgr = agentMgr;
|
|
68
28
|
this.missions = resolveMissionStore(ctx);
|
|
69
|
-
this.
|
|
70
|
-
this.delayStore = ctx.delayStore ?? new InMemoryDelayStore();
|
|
29
|
+
this.gating = new MissionGating(ctx, ctx.checkpointStore ?? new InMemoryCheckpointStore(), ctx.delayStore ?? new InMemoryDelayStore());
|
|
71
30
|
// Rebuild cleanedGroups from persisted task state — groups where ALL tasks
|
|
72
31
|
// are already terminal don't need to be re-processed after a server restart.
|
|
73
32
|
// Constructor cannot be async — expose ready promise for callers to await.
|
|
@@ -77,13 +36,12 @@ export class MissionExecutor {
|
|
|
77
36
|
ready;
|
|
78
37
|
/** Async init: load checkpoint/delay state and rebuild cleanedGroups. */
|
|
79
38
|
async initStoresAndRebuild() {
|
|
80
|
-
|
|
81
|
-
this.delayState = await this.delayStore.load();
|
|
39
|
+
await this.gating.ready;
|
|
82
40
|
await this.rebuildCleanedGroups();
|
|
83
41
|
}
|
|
84
42
|
/** Async init: rebuild cleanedGroups from persisted task state. */
|
|
85
43
|
async rebuildCleanedGroups() {
|
|
86
|
-
const allTasks = await this.ctx.
|
|
44
|
+
const allTasks = await this.ctx.taskStore.listTasks();
|
|
87
45
|
const groups = new Set();
|
|
88
46
|
for (const t of allTasks) {
|
|
89
47
|
if (t.group)
|
|
@@ -104,109 +62,32 @@ export class MissionExecutor {
|
|
|
104
62
|
getQualityGates(group) {
|
|
105
63
|
return this.gatesByGroup.get(group) ?? [];
|
|
106
64
|
}
|
|
65
|
+
// ─── Checkpoint/delay gating (delegated to MissionGating) ─────────────
|
|
107
66
|
/** Get checkpoints for a mission group. Returns empty array if none defined. */
|
|
108
67
|
getCheckpoints(group) {
|
|
109
|
-
return this.
|
|
68
|
+
return this.gating.getCheckpoints(group);
|
|
110
69
|
}
|
|
111
70
|
/**
|
|
112
71
|
* Check if a task is blocked by an active (unresumed) checkpoint.
|
|
113
72
|
* Returns the blocking checkpoint if found, undefined if the task can proceed.
|
|
114
73
|
*/
|
|
115
74
|
async getBlockingCheckpoint(group, taskTitle, taskId, tasks) {
|
|
116
|
-
|
|
117
|
-
if (!checkpoints)
|
|
118
|
-
return undefined;
|
|
119
|
-
for (const cp of checkpoints) {
|
|
120
|
-
// Task must be in blocksTasks
|
|
121
|
-
if (!cp.blocksTasks.includes(taskTitle) && !cp.blocksTasks.includes(taskId)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
const cpKey = `${group}:${cp.name}`;
|
|
125
|
-
// Already resumed — don't block
|
|
126
|
-
if (this.cpState.resumed.includes(cpKey))
|
|
127
|
-
continue;
|
|
128
|
-
// Check if all afterTasks are done
|
|
129
|
-
const afterTasks = tasks.filter(t => cp.afterTasks.includes(t.title) || cp.afterTasks.includes(t.id));
|
|
130
|
-
const allDone = afterTasks.length >= cp.afterTasks.length &&
|
|
131
|
-
afterTasks.every(t => t.status === "done" || t.status === "failed");
|
|
132
|
-
if (!allDone) {
|
|
133
|
-
// afterTasks not finished yet — checkpoint not reached, don't block (deps will block naturally)
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
// Checkpoint reached — activate it if not already active
|
|
137
|
-
if (!this.cpState.active[cpKey]) {
|
|
138
|
-
const reachedAt = new Date().toISOString();
|
|
139
|
-
this.cpState.active[cpKey] = { checkpoint: cp, reachedAt };
|
|
140
|
-
await this.cpStore.save(this.cpState);
|
|
141
|
-
// Pause the mission — use task.missionId when available
|
|
142
|
-
const taskMissionId = tasks.find(t => t.group === group && t.missionId)?.missionId;
|
|
143
|
-
const mission = taskMissionId
|
|
144
|
-
? await this.missions.getMission(taskMissionId)
|
|
145
|
-
: await this.missions.getMissionByName(group);
|
|
146
|
-
if (mission && mission.status === "active") {
|
|
147
|
-
await this.missions.updateMission(mission.id, { status: "paused" });
|
|
148
|
-
}
|
|
149
|
-
// Register notification rules for this checkpoint's channels
|
|
150
|
-
this.ensureCheckpointNotificationRules(cpKey, cp);
|
|
151
|
-
// Emit event (picked up by notification router if rules are configured)
|
|
152
|
-
this.ctx.emitter.emit("checkpoint:reached", {
|
|
153
|
-
missionId: mission?.id,
|
|
154
|
-
group,
|
|
155
|
-
checkpointName: cp.name,
|
|
156
|
-
message: cp.message,
|
|
157
|
-
afterTasks: cp.afterTasks,
|
|
158
|
-
blocksTasks: cp.blocksTasks,
|
|
159
|
-
reachedAt,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
// Return the blocking checkpoint
|
|
163
|
-
return this.cpState.active[cpKey];
|
|
164
|
-
}
|
|
165
|
-
return undefined;
|
|
75
|
+
return this.gating.getBlockingCheckpoint(group, taskTitle, taskId, tasks);
|
|
166
76
|
}
|
|
167
77
|
/**
|
|
168
78
|
* Resume a checkpoint, unblocking its blocksTasks.
|
|
169
79
|
* Returns true if the checkpoint was active and is now resumed, false if not found.
|
|
170
80
|
*/
|
|
171
81
|
async resumeCheckpoint(group, checkpointName) {
|
|
172
|
-
|
|
173
|
-
const active = this.cpState.active[cpKey];
|
|
174
|
-
if (!active)
|
|
175
|
-
return false;
|
|
176
|
-
this.cpState.resumed.push(cpKey);
|
|
177
|
-
delete this.cpState.active[cpKey];
|
|
178
|
-
await this.cpStore.save(this.cpState);
|
|
179
|
-
// Un-pause the mission (back to active) — resolve via missionId from tasks
|
|
180
|
-
const groupTasks = (await this.ctx.registry.getAllTasks()).filter(t => t.group === group);
|
|
181
|
-
const mission = await this.resolveMissionForGroup(groupTasks, group);
|
|
182
|
-
if (mission && mission.status === "paused") {
|
|
183
|
-
await this.missions.updateMission(mission.id, { status: "active" });
|
|
184
|
-
}
|
|
185
|
-
this.ctx.emitter.emit("checkpoint:resumed", {
|
|
186
|
-
missionId: mission?.id,
|
|
187
|
-
group,
|
|
188
|
-
checkpointName,
|
|
189
|
-
});
|
|
190
|
-
return true;
|
|
82
|
+
return this.gating.resumeCheckpoint(group, checkpointName);
|
|
191
83
|
}
|
|
192
84
|
/** Get all active (unresumed) checkpoints across all mission groups. */
|
|
193
85
|
getActiveCheckpoints() {
|
|
194
|
-
|
|
195
|
-
for (const [cpKey, data] of Object.entries(this.cpState.active)) {
|
|
196
|
-
const [group, ...nameParts] = cpKey.split(":");
|
|
197
|
-
const checkpointName = nameParts.join(":");
|
|
198
|
-
result.push({ group, checkpointName, checkpoint: data.checkpoint, reachedAt: data.reachedAt });
|
|
199
|
-
}
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
/** Notification rules for checkpoints removed — no-op. */
|
|
203
|
-
ensureCheckpointNotificationRules(_cpKey, _cp) {
|
|
204
|
-
// No-op: notification routing removed.
|
|
86
|
+
return this.gating.getActiveCheckpoints();
|
|
205
87
|
}
|
|
206
|
-
// ─── Delay runtime ──────────────────────────────────
|
|
207
88
|
/** Get delays for a mission group. Returns empty array if none defined. */
|
|
208
89
|
getDelays(group) {
|
|
209
|
-
return this.
|
|
90
|
+
return this.gating.getDelays(group);
|
|
210
91
|
}
|
|
211
92
|
/**
|
|
212
93
|
* Check if a task is blocked by an active (unexpired) delay.
|
|
@@ -215,91 +96,15 @@ export class MissionExecutor {
|
|
|
215
96
|
* Returns the blocking delay if found, undefined if the task can proceed.
|
|
216
97
|
*/
|
|
217
98
|
async getBlockingDelay(group, taskTitle, taskId, tasks) {
|
|
218
|
-
|
|
219
|
-
if (!delays)
|
|
220
|
-
return undefined;
|
|
221
|
-
for (const dl of delays) {
|
|
222
|
-
// Task must be in blocksTasks
|
|
223
|
-
if (!dl.blocksTasks.includes(taskTitle) && !dl.blocksTasks.includes(taskId)) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
const dlKey = `${group}:${dl.name}`;
|
|
227
|
-
// Already expired — don't block
|
|
228
|
-
if (this.delayState.expired.includes(dlKey))
|
|
229
|
-
continue;
|
|
230
|
-
// Check if all afterTasks are done
|
|
231
|
-
const afterTasks = tasks.filter(t => dl.afterTasks.includes(t.title) || dl.afterTasks.includes(t.id));
|
|
232
|
-
const allDone = afterTasks.length >= dl.afterTasks.length &&
|
|
233
|
-
afterTasks.every(t => t.status === "done" || t.status === "failed");
|
|
234
|
-
if (!allDone) {
|
|
235
|
-
// afterTasks not finished yet — delay not triggered, don't block (deps will block naturally)
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
// Delay triggered — activate timer if not already active
|
|
239
|
-
if (!this.delayState.active[dlKey]) {
|
|
240
|
-
const startedAt = new Date().toISOString();
|
|
241
|
-
const durationMs = parseISO8601Duration(dl.duration);
|
|
242
|
-
const expiresAt = new Date(Date.now() + durationMs).toISOString();
|
|
243
|
-
this.delayState.active[dlKey] = { delay: dl, startedAt, expiresAt };
|
|
244
|
-
await this.delayStore.save(this.delayState);
|
|
245
|
-
// Register notification rules for this delay's channels
|
|
246
|
-
this.ensureDelayNotificationRules(dlKey, dl);
|
|
247
|
-
// Emit event
|
|
248
|
-
const mission = await this.resolveMissionForGroupByName(group);
|
|
249
|
-
this.ctx.emitter.emit("delay:started", {
|
|
250
|
-
missionId: mission?.id,
|
|
251
|
-
group,
|
|
252
|
-
delayName: dl.name,
|
|
253
|
-
duration: dl.duration,
|
|
254
|
-
message: dl.message,
|
|
255
|
-
afterTasks: dl.afterTasks,
|
|
256
|
-
blocksTasks: dl.blocksTasks,
|
|
257
|
-
startedAt,
|
|
258
|
-
expiresAt,
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
// Check if the timer has expired
|
|
262
|
-
const active = this.delayState.active[dlKey];
|
|
263
|
-
if (new Date(active.expiresAt).getTime() <= Date.now()) {
|
|
264
|
-
// Timer expired — mark as expired and unblock
|
|
265
|
-
this.delayState.expired.push(dlKey);
|
|
266
|
-
delete this.delayState.active[dlKey];
|
|
267
|
-
await this.delayStore.save(this.delayState);
|
|
268
|
-
const mission = await this.resolveMissionForGroupByName(group);
|
|
269
|
-
this.ctx.emitter.emit("delay:expired", {
|
|
270
|
-
missionId: mission?.id,
|
|
271
|
-
group,
|
|
272
|
-
delayName: dl.name,
|
|
273
|
-
});
|
|
274
|
-
continue; // Unblocked — check next delay
|
|
275
|
-
}
|
|
276
|
-
// Still waiting — return the blocking delay
|
|
277
|
-
return active;
|
|
278
|
-
}
|
|
279
|
-
return undefined;
|
|
99
|
+
return this.gating.getBlockingDelay(group, taskTitle, taskId, tasks);
|
|
280
100
|
}
|
|
281
101
|
/** Get all active (unexpired) delays across all mission groups. */
|
|
282
102
|
getActiveDelays() {
|
|
283
|
-
|
|
284
|
-
for (const [dlKey, data] of Object.entries(this.delayState.active)) {
|
|
285
|
-
const [group, ...nameParts] = dlKey.split(":");
|
|
286
|
-
const delayName = nameParts.join(":");
|
|
287
|
-
result.push({ group, delayName, delay: data.delay, startedAt: data.startedAt, expiresAt: data.expiresAt });
|
|
288
|
-
}
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
/** Notification rules for delays removed — no-op. */
|
|
292
|
-
ensureDelayNotificationRules(_dlKey, _dl) {
|
|
293
|
-
// No-op: notification routing removed.
|
|
103
|
+
return this.gating.getActiveDelays();
|
|
294
104
|
}
|
|
295
|
-
|
|
296
|
-
async resolveMissionForGroupByName(group) {
|
|
297
|
-
const groupTasks = (await this.ctx.registry.getAllTasks()).filter(t => t.group === group);
|
|
298
|
-
return this.resolveMissionForGroup(groupTasks, group);
|
|
299
|
-
}
|
|
300
|
-
async saveMission(opts) {
|
|
105
|
+
async createMission(opts) {
|
|
301
106
|
const name = opts.name ?? await this.missions.nextMissionName();
|
|
302
|
-
const mission = await this.missions.
|
|
107
|
+
const mission = await this.missions.createMission({
|
|
303
108
|
name,
|
|
304
109
|
data: opts.data,
|
|
305
110
|
prompt: opts.prompt,
|
|
@@ -319,8 +124,8 @@ export class MissionExecutor {
|
|
|
319
124
|
async getMissionByName(name) {
|
|
320
125
|
return this.missions.getMissionByName(name);
|
|
321
126
|
}
|
|
322
|
-
async
|
|
323
|
-
return await this.missions.
|
|
127
|
+
async listMissions() {
|
|
128
|
+
return await this.missions.listMissions();
|
|
324
129
|
}
|
|
325
130
|
async updateMission(missionId, updates) {
|
|
326
131
|
return this.missions.updateMission(missionId, updates);
|
|
@@ -342,20 +147,8 @@ export class MissionExecutor {
|
|
|
342
147
|
if (key.startsWith(missionGroup + " #"))
|
|
343
148
|
this.gatesByGroup.delete(key);
|
|
344
149
|
}
|
|
345
|
-
// 4. Clean up persisted checkpoints
|
|
346
|
-
|
|
347
|
-
for (const key of Object.keys(this.cpState.definitions)) {
|
|
348
|
-
if (key.startsWith(missionGroup + " #")) {
|
|
349
|
-
this.cpState = await this.cpStore.removeGroup(this.cpState, key);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
// 4b. Clean up persisted delays
|
|
353
|
-
this.delayState = await this.delayStore.removeGroup(this.delayState, missionGroup);
|
|
354
|
-
for (const key of Object.keys(this.delayState.definitions)) {
|
|
355
|
-
if (key.startsWith(missionGroup + " #")) {
|
|
356
|
-
this.delayState = await this.delayStore.removeGroup(this.delayState, key);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
150
|
+
// 4. Clean up persisted checkpoints and delays (incl. numbered recurring-run groups)
|
|
151
|
+
await this.gating.removeGroupAndRuns(missionGroup);
|
|
359
152
|
// 5. Clean up scheduled origin cache
|
|
360
153
|
this.scheduledOrigin.delete(missionGroup);
|
|
361
154
|
for (const key of this.scheduledOrigin.keys()) {
|
|
@@ -376,8 +169,8 @@ export class MissionExecutor {
|
|
|
376
169
|
return result;
|
|
377
170
|
}
|
|
378
171
|
async getResumableMissions() {
|
|
379
|
-
const missions = await this.
|
|
380
|
-
const state = await this.ctx.
|
|
172
|
+
const missions = await this.listMissions();
|
|
173
|
+
const state = await this.ctx.taskStore.getState();
|
|
381
174
|
return missions.filter(m => {
|
|
382
175
|
// Non-resumable statuses: draft (never executed), scheduled/recurring (scheduler handles),
|
|
383
176
|
// completed (done), cancelled (aborted)
|
|
@@ -413,7 +206,7 @@ export class MissionExecutor {
|
|
|
413
206
|
this.ctx.emitter.emit("log", { level: "warn", message: `Failed to re-register volatile agents for ${mission.name}: ${err instanceof Error ? err.message : String(err)}` });
|
|
414
207
|
}
|
|
415
208
|
}
|
|
416
|
-
const state = await this.ctx.
|
|
209
|
+
const state = await this.ctx.taskStore.getState();
|
|
417
210
|
const tasks = state.tasks.filter(t => t.group === mission.name);
|
|
418
211
|
const failedTasks = tasks.filter(t => t.status === "failed");
|
|
419
212
|
const pendingTasks = tasks.filter(t => t.status === "pending");
|
|
@@ -511,7 +304,7 @@ export class MissionExecutor {
|
|
|
511
304
|
this.ctx.emitter.emit("log", { level: "warn", message: `Mission task "${t.title}": ${w}` });
|
|
512
305
|
}
|
|
513
306
|
}
|
|
514
|
-
const task = await this.taskMgr.
|
|
307
|
+
const task = await this.taskMgr.createTask({
|
|
515
308
|
title: t.title,
|
|
516
309
|
description: t.description || t.title,
|
|
517
310
|
assignTo: t.assignTo || (await this.agentMgr.getAgents())[0]?.name || "default",
|
|
@@ -533,12 +326,10 @@ export class MissionExecutor {
|
|
|
533
326
|
this.gatesByGroup.set(group, doc.qualityGates);
|
|
534
327
|
}
|
|
535
328
|
if (doc.checkpoints && doc.checkpoints.length > 0) {
|
|
536
|
-
this.
|
|
537
|
-
await this.cpStore.save(this.cpState);
|
|
329
|
+
await this.gating.setCheckpointDefinitions(group, doc.checkpoints);
|
|
538
330
|
}
|
|
539
331
|
if (doc.delays && doc.delays.length > 0) {
|
|
540
|
-
this.
|
|
541
|
-
await this.delayStore.save(this.delayState);
|
|
332
|
+
await this.gating.setDelayDefinitions(group, doc.delays);
|
|
542
333
|
}
|
|
543
334
|
// Track scheduled origin so we know where to return after completion
|
|
544
335
|
if (scheduledStatus) {
|
|
@@ -555,19 +346,6 @@ export class MissionExecutor {
|
|
|
555
346
|
this.ctx.emitter.emit("mission:executed", { missionId, group, taskCount: tasks.length });
|
|
556
347
|
return { tasks, group };
|
|
557
348
|
}
|
|
558
|
-
/**
|
|
559
|
-
* Resolve the Mission for a group of tasks.
|
|
560
|
-
* Uses task.missionId (direct FK) when available, falls back to getMissionByName
|
|
561
|
-
* for legacy tasks that pre-date the missionId field.
|
|
562
|
-
*/
|
|
563
|
-
async resolveMissionForGroup(groupTasks, group) {
|
|
564
|
-
// Prefer the direct ID reference from any task in the group
|
|
565
|
-
const mid = groupTasks.find(t => t.missionId)?.missionId;
|
|
566
|
-
if (mid)
|
|
567
|
-
return this.missions.getMission(mid);
|
|
568
|
-
// Fallback: strip run-number suffix (e.g. "Mission #3" → "Mission") for legacy compat
|
|
569
|
-
return this.missions.getMissionByName(group.replace(/ #\d+$/, ""));
|
|
570
|
-
}
|
|
571
349
|
/** Check if any mission groups have all tasks terminal, and clean up their volatile agents */
|
|
572
350
|
async cleanupCompletedGroups(tasks) {
|
|
573
351
|
const groups = new Set();
|
|
@@ -594,7 +372,7 @@ export class MissionExecutor {
|
|
|
594
372
|
}
|
|
595
373
|
this.cleanedGroups.add(group);
|
|
596
374
|
// Auto-update mission status
|
|
597
|
-
const mission = await this.
|
|
375
|
+
const mission = await resolveMissionForGroup(this.missions, groupTasks, group);
|
|
598
376
|
if (mission && mission.status === "active") {
|
|
599
377
|
let allDone = groupTasks.every(t => t.status === "done");
|
|
600
378
|
// Check mission quality threshold (only if all tasks passed structurally)
|
|
@@ -631,10 +409,8 @@ export class MissionExecutor {
|
|
|
631
409
|
// Clean up gate, checkpoint, delay, and scheduled-origin caches
|
|
632
410
|
this.gatesByGroup.delete(group);
|
|
633
411
|
this.scheduledOrigin.delete(group);
|
|
634
|
-
// Clean up persisted checkpoint entries for this group
|
|
635
|
-
|
|
636
|
-
// Clean up persisted delay entries for this group
|
|
637
|
-
this.delayState = await this.delayStore.removeGroup(this.delayState, group);
|
|
412
|
+
// Clean up persisted checkpoint and delay entries for this group
|
|
413
|
+
await this.gating.removeGroup(group);
|
|
638
414
|
}
|
|
639
415
|
}
|
|
640
416
|
}
|
|
@@ -665,249 +441,237 @@ export class MissionExecutor {
|
|
|
665
441
|
this.ctx.emitter.emit("mission:saved", { missionId: mission.id, name: mission.name, status: mission.status });
|
|
666
442
|
return mission;
|
|
667
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Generic read-modify-write for a named collection inside the mission `data`
|
|
446
|
+
* document. Loads the mission, hands the collection (and the full parsed
|
|
447
|
+
* document) to `mutate`, then re-validates and persists.
|
|
448
|
+
*
|
|
449
|
+
* - `createIfMissing` creates the collection when absent (add operations);
|
|
450
|
+
* otherwise a missing collection throws `missingError`.
|
|
451
|
+
* - Optional collections left empty by the mutation are removed from the
|
|
452
|
+
* document entirely (tasks are required and never dropped).
|
|
453
|
+
*/
|
|
454
|
+
async mutateMissionCollection(missionId, key, mutate, opts = {}) {
|
|
455
|
+
const { doc } = await this.parseMissionData(missionId);
|
|
456
|
+
if (!doc[key]) {
|
|
457
|
+
if (!opts.createIfMissing)
|
|
458
|
+
throw new Error(opts.missingError ?? `No ${key} in mission`);
|
|
459
|
+
doc[key] = [];
|
|
460
|
+
}
|
|
461
|
+
mutate(doc[key], doc);
|
|
462
|
+
if (key !== "tasks" && doc[key]?.length === 0)
|
|
463
|
+
delete doc[key];
|
|
464
|
+
return this.persistMissionData(missionId, doc);
|
|
465
|
+
}
|
|
668
466
|
// ─── Task operations ────────────────────────────────
|
|
669
467
|
/** Add a task to a draft mission's data. */
|
|
670
468
|
async addMissionTask(missionId, task) {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
469
|
+
return this.mutateMissionCollection(missionId, "tasks", tasks => {
|
|
470
|
+
// Enforce unique title
|
|
471
|
+
if (tasks.some(t => t.title === task.title)) {
|
|
472
|
+
throw new Error(`Task title "${task.title}" already exists in this mission`);
|
|
473
|
+
}
|
|
474
|
+
tasks.push(task);
|
|
475
|
+
}, { createIfMissing: true });
|
|
678
476
|
}
|
|
679
477
|
/** Update a specific task within the mission data (matched by title). */
|
|
680
478
|
async updateMissionTask(missionId, taskTitle, updates) {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
479
|
+
return this.mutateMissionCollection(missionId, "tasks", tasks => {
|
|
480
|
+
const idx = tasks.findIndex(t => t.title === taskTitle);
|
|
481
|
+
if (idx === -1)
|
|
482
|
+
throw new Error(`Task "${taskTitle}" not found in mission`);
|
|
483
|
+
// If renaming, enforce unique title
|
|
484
|
+
if (updates.title && updates.title !== taskTitle && tasks.some(t => t.title === updates.title)) {
|
|
485
|
+
throw new Error(`Task title "${updates.title}" already exists in this mission`);
|
|
486
|
+
}
|
|
487
|
+
tasks[idx] = { ...tasks[idx], ...updates };
|
|
488
|
+
}, { missingError: `Task "${taskTitle}" not found in mission` });
|
|
691
489
|
}
|
|
692
490
|
/** Remove a task from the mission data (by title). Also cleans up dependsOn references. */
|
|
693
491
|
async removeMissionTask(missionId, taskTitle) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
492
|
+
return this.mutateMissionCollection(missionId, "tasks", (tasks, doc) => {
|
|
493
|
+
const idx = tasks.findIndex(t => t.title === taskTitle);
|
|
494
|
+
if (idx === -1)
|
|
495
|
+
throw new Error(`Task "${taskTitle}" not found in mission`);
|
|
496
|
+
tasks.splice(idx, 1);
|
|
497
|
+
// Clean up dependsOn references in remaining tasks
|
|
498
|
+
for (const t of tasks) {
|
|
499
|
+
if (t.dependsOn) {
|
|
500
|
+
t.dependsOn = t.dependsOn.filter(d => d !== taskTitle);
|
|
501
|
+
}
|
|
703
502
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
503
|
+
// Clean up quality gates and checkpoints that reference this task
|
|
504
|
+
if (doc.qualityGates) {
|
|
505
|
+
for (const gate of doc.qualityGates) {
|
|
506
|
+
gate.afterTasks = gate.afterTasks.filter(t => t !== taskTitle);
|
|
507
|
+
gate.blocksTasks = gate.blocksTasks.filter(t => t !== taskTitle);
|
|
508
|
+
}
|
|
509
|
+
// Remove gates that became empty
|
|
510
|
+
doc.qualityGates = doc.qualityGates.filter(g => g.afterTasks.length > 0 && g.blocksTasks.length > 0);
|
|
710
511
|
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
cp.blocksTasks = cp.blocksTasks.filter(t => t !== taskTitle);
|
|
512
|
+
if (doc.checkpoints) {
|
|
513
|
+
for (const cp of doc.checkpoints) {
|
|
514
|
+
cp.afterTasks = cp.afterTasks.filter(t => t !== taskTitle);
|
|
515
|
+
cp.blocksTasks = cp.blocksTasks.filter(t => t !== taskTitle);
|
|
516
|
+
}
|
|
517
|
+
doc.checkpoints = doc.checkpoints.filter(cp => cp.afterTasks.length > 0 && cp.blocksTasks.length > 0);
|
|
718
518
|
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
519
|
+
if (doc.delays) {
|
|
520
|
+
for (const dl of doc.delays) {
|
|
521
|
+
dl.afterTasks = dl.afterTasks.filter(t => t !== taskTitle);
|
|
522
|
+
dl.blocksTasks = dl.blocksTasks.filter(t => t !== taskTitle);
|
|
523
|
+
}
|
|
524
|
+
doc.delays = doc.delays.filter(dl => dl.afterTasks.length > 0 && dl.blocksTasks.length > 0);
|
|
725
525
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
throw new Error("Cannot remove the last task from a mission");
|
|
731
|
-
return this.persistMissionData(missionId, doc);
|
|
526
|
+
// Ensure at least 1 task remains (Zod will catch this, but give a nicer error)
|
|
527
|
+
if (tasks.length === 0)
|
|
528
|
+
throw new Error("Cannot remove the last task from a mission");
|
|
529
|
+
}, { missingError: `Task "${taskTitle}" not found in mission` });
|
|
732
530
|
}
|
|
733
531
|
/** Reorder tasks within the mission data. Accepts an array of task titles in the desired order. */
|
|
734
532
|
async reorderMissionTasks(missionId, titles) {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
533
|
+
return this.mutateMissionCollection(missionId, "tasks", (tasks, doc) => {
|
|
534
|
+
const titleSet = new Set(titles);
|
|
535
|
+
if (titleSet.size !== titles.length)
|
|
536
|
+
throw new Error("Duplicate titles in reorder list");
|
|
537
|
+
const existing = new Set(tasks.map(t => t.title));
|
|
538
|
+
for (const t of titles) {
|
|
539
|
+
if (!existing.has(t))
|
|
540
|
+
throw new Error(`Task "${t}" not found in mission`);
|
|
541
|
+
}
|
|
542
|
+
if (titles.length !== tasks.length)
|
|
543
|
+
throw new Error("Reorder list must include all task titles");
|
|
544
|
+
const taskMap = new Map(tasks.map(t => [t.title, t]));
|
|
545
|
+
doc.tasks = titles.map(t => taskMap.get(t));
|
|
546
|
+
});
|
|
749
547
|
}
|
|
750
548
|
// ─── Checkpoint operations ──────────────────────────
|
|
751
549
|
/** Add a checkpoint to a mission's data. */
|
|
752
550
|
async addMissionCheckpoint(missionId, checkpoint) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
doc.checkpoints.push(checkpoint);
|
|
760
|
-
return this.persistMissionData(missionId, doc);
|
|
551
|
+
return this.mutateMissionCollection(missionId, "checkpoints", checkpoints => {
|
|
552
|
+
if (checkpoints.some(c => c.name === checkpoint.name)) {
|
|
553
|
+
throw new Error(`Checkpoint "${checkpoint.name}" already exists in this mission`);
|
|
554
|
+
}
|
|
555
|
+
checkpoints.push(checkpoint);
|
|
556
|
+
}, { createIfMissing: true });
|
|
761
557
|
}
|
|
762
558
|
/** Update a checkpoint in the mission data (matched by name). */
|
|
763
559
|
async updateMissionCheckpoint(missionId, checkpointName, updates) {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
doc.checkpoints[idx] = { ...doc.checkpoints[idx], ...updates };
|
|
774
|
-
return this.persistMissionData(missionId, doc);
|
|
560
|
+
return this.mutateMissionCollection(missionId, "checkpoints", checkpoints => {
|
|
561
|
+
const idx = checkpoints.findIndex(c => c.name === checkpointName);
|
|
562
|
+
if (idx === -1)
|
|
563
|
+
throw new Error(`Checkpoint "${checkpointName}" not found in mission`);
|
|
564
|
+
if (updates.name && updates.name !== checkpointName && checkpoints.some(c => c.name === updates.name)) {
|
|
565
|
+
throw new Error(`Checkpoint "${updates.name}" already exists in this mission`);
|
|
566
|
+
}
|
|
567
|
+
checkpoints[idx] = { ...checkpoints[idx], ...updates };
|
|
568
|
+
}, { missingError: `Checkpoint "${checkpointName}" not found in mission` });
|
|
775
569
|
}
|
|
776
570
|
/** Remove a checkpoint from the mission data (by name). */
|
|
777
571
|
async removeMissionCheckpoint(missionId, checkpointName) {
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
doc.checkpoints.splice(idx, 1);
|
|
785
|
-
if (doc.checkpoints.length === 0)
|
|
786
|
-
delete doc.checkpoints;
|
|
787
|
-
return this.persistMissionData(missionId, doc);
|
|
572
|
+
return this.mutateMissionCollection(missionId, "checkpoints", checkpoints => {
|
|
573
|
+
const idx = checkpoints.findIndex(c => c.name === checkpointName);
|
|
574
|
+
if (idx === -1)
|
|
575
|
+
throw new Error(`Checkpoint "${checkpointName}" not found in mission`);
|
|
576
|
+
checkpoints.splice(idx, 1);
|
|
577
|
+
}, { missingError: `Checkpoint "${checkpointName}" not found in mission` });
|
|
788
578
|
}
|
|
789
579
|
// ─── Delay operations ───────────────────────────────
|
|
790
580
|
/** Add a delay to a mission's data. */
|
|
791
581
|
async addMissionDelay(missionId, delay) {
|
|
792
582
|
// Validate duration format
|
|
793
583
|
parseISO8601Duration(delay.duration);
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
}
|
|
800
|
-
doc.delays.push(delay);
|
|
801
|
-
return this.persistMissionData(missionId, doc);
|
|
584
|
+
return this.mutateMissionCollection(missionId, "delays", delays => {
|
|
585
|
+
if (delays.some(d => d.name === delay.name)) {
|
|
586
|
+
throw new Error(`Delay "${delay.name}" already exists in this mission`);
|
|
587
|
+
}
|
|
588
|
+
delays.push(delay);
|
|
589
|
+
}, { createIfMissing: true });
|
|
802
590
|
}
|
|
803
591
|
/** Update a delay in the mission data (matched by name). */
|
|
804
592
|
async updateMissionDelay(missionId, delayName, updates) {
|
|
805
593
|
if (updates.duration)
|
|
806
594
|
parseISO8601Duration(updates.duration);
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
}
|
|
816
|
-
doc.delays[idx] = { ...doc.delays[idx], ...updates };
|
|
817
|
-
return this.persistMissionData(missionId, doc);
|
|
595
|
+
return this.mutateMissionCollection(missionId, "delays", delays => {
|
|
596
|
+
const idx = delays.findIndex(d => d.name === delayName);
|
|
597
|
+
if (idx === -1)
|
|
598
|
+
throw new Error(`Delay "${delayName}" not found in mission`);
|
|
599
|
+
if (updates.name && updates.name !== delayName && delays.some(d => d.name === updates.name)) {
|
|
600
|
+
throw new Error(`Delay "${updates.name}" already exists in this mission`);
|
|
601
|
+
}
|
|
602
|
+
delays[idx] = { ...delays[idx], ...updates };
|
|
603
|
+
}, { missingError: `Delay "${delayName}" not found in mission` });
|
|
818
604
|
}
|
|
819
605
|
/** Remove a delay from the mission data (by name). */
|
|
820
606
|
async removeMissionDelay(missionId, delayName) {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
doc.delays.splice(idx, 1);
|
|
828
|
-
if (doc.delays.length === 0)
|
|
829
|
-
delete doc.delays;
|
|
830
|
-
return this.persistMissionData(missionId, doc);
|
|
607
|
+
return this.mutateMissionCollection(missionId, "delays", delays => {
|
|
608
|
+
const idx = delays.findIndex(d => d.name === delayName);
|
|
609
|
+
if (idx === -1)
|
|
610
|
+
throw new Error(`Delay "${delayName}" not found in mission`);
|
|
611
|
+
delays.splice(idx, 1);
|
|
612
|
+
}, { missingError: `Delay "${delayName}" not found in mission` });
|
|
831
613
|
}
|
|
832
614
|
// ─── Quality gate operations ────────────────────────
|
|
833
615
|
/** Add a quality gate to a mission's data. */
|
|
834
616
|
async addMissionQualityGate(missionId, gate) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
841
|
-
doc.qualityGates.push(gate);
|
|
842
|
-
return this.persistMissionData(missionId, doc);
|
|
617
|
+
return this.mutateMissionCollection(missionId, "qualityGates", gates => {
|
|
618
|
+
if (gates.some(g => g.name === gate.name)) {
|
|
619
|
+
throw new Error(`Quality gate "${gate.name}" already exists in this mission`);
|
|
620
|
+
}
|
|
621
|
+
gates.push(gate);
|
|
622
|
+
}, { createIfMissing: true });
|
|
843
623
|
}
|
|
844
624
|
/** Update a quality gate in the mission data (matched by name). */
|
|
845
625
|
async updateMissionQualityGate(missionId, gateName, updates) {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
doc.qualityGates[idx] = { ...doc.qualityGates[idx], ...updates };
|
|
856
|
-
return this.persistMissionData(missionId, doc);
|
|
626
|
+
return this.mutateMissionCollection(missionId, "qualityGates", gates => {
|
|
627
|
+
const idx = gates.findIndex(g => g.name === gateName);
|
|
628
|
+
if (idx === -1)
|
|
629
|
+
throw new Error(`Quality gate "${gateName}" not found in mission`);
|
|
630
|
+
if (updates.name && updates.name !== gateName && gates.some(g => g.name === updates.name)) {
|
|
631
|
+
throw new Error(`Quality gate "${updates.name}" already exists in this mission`);
|
|
632
|
+
}
|
|
633
|
+
gates[idx] = { ...gates[idx], ...updates };
|
|
634
|
+
}, { missingError: `Quality gate "${gateName}" not found in mission` });
|
|
857
635
|
}
|
|
858
636
|
/** Remove a quality gate from the mission data (by name). */
|
|
859
637
|
async removeMissionQualityGate(missionId, gateName) {
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
doc.qualityGates.splice(idx, 1);
|
|
867
|
-
if (doc.qualityGates.length === 0)
|
|
868
|
-
delete doc.qualityGates;
|
|
869
|
-
return this.persistMissionData(missionId, doc);
|
|
638
|
+
return this.mutateMissionCollection(missionId, "qualityGates", gates => {
|
|
639
|
+
const idx = gates.findIndex(g => g.name === gateName);
|
|
640
|
+
if (idx === -1)
|
|
641
|
+
throw new Error(`Quality gate "${gateName}" not found in mission`);
|
|
642
|
+
gates.splice(idx, 1);
|
|
643
|
+
}, { missingError: `Quality gate "${gateName}" not found in mission` });
|
|
870
644
|
}
|
|
871
645
|
// ─── Team (volatile agents) operations ──────────────
|
|
872
646
|
/** Add a team member to the mission's volatile team. */
|
|
873
647
|
async addMissionTeamMember(missionId, member) {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
doc.team.push(member);
|
|
881
|
-
return this.persistMissionData(missionId, doc);
|
|
648
|
+
return this.mutateMissionCollection(missionId, "team", team => {
|
|
649
|
+
if (team.some((m) => m.name === member.name)) {
|
|
650
|
+
throw new Error(`Team member "${member.name}" already exists in this mission`);
|
|
651
|
+
}
|
|
652
|
+
team.push(member);
|
|
653
|
+
}, { createIfMissing: true });
|
|
882
654
|
}
|
|
883
655
|
/** Update a team member in the mission data (matched by name). */
|
|
884
656
|
async updateMissionTeamMember(missionId, memberName, updates) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
team[idx] = { ...team[idx], ...updates };
|
|
896
|
-
return this.persistMissionData(missionId, doc);
|
|
657
|
+
return this.mutateMissionCollection(missionId, "team", team => {
|
|
658
|
+
const idx = team.findIndex((m) => m.name === memberName);
|
|
659
|
+
if (idx === -1)
|
|
660
|
+
throw new Error(`Team member "${memberName}" not found in mission`);
|
|
661
|
+
if (updates.name && updates.name !== memberName && team.some((m) => m.name === updates.name)) {
|
|
662
|
+
throw new Error(`Team member "${updates.name}" already exists in this mission`);
|
|
663
|
+
}
|
|
664
|
+
team[idx] = { ...team[idx], ...updates };
|
|
665
|
+
}, { missingError: `Team member "${memberName}" not found in mission` });
|
|
897
666
|
}
|
|
898
667
|
/** Remove a team member from the mission data (by name). */
|
|
899
668
|
async removeMissionTeamMember(missionId, memberName) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
throw new Error(`Team member "${memberName}" not found in mission`);
|
|
907
|
-
team.splice(idx, 1);
|
|
908
|
-
if (team.length === 0)
|
|
909
|
-
delete doc.team;
|
|
910
|
-
return this.persistMissionData(missionId, doc);
|
|
669
|
+
return this.mutateMissionCollection(missionId, "team", team => {
|
|
670
|
+
const idx = team.findIndex((m) => m.name === memberName);
|
|
671
|
+
if (idx === -1)
|
|
672
|
+
throw new Error(`Team member "${memberName}" not found in mission`);
|
|
673
|
+
team.splice(idx, 1);
|
|
674
|
+
}, { missingError: `Team member "${memberName}" not found in mission` });
|
|
911
675
|
}
|
|
912
676
|
// ─── Notifications operations ───────────────────────
|
|
913
677
|
/** Update the mission-level notification rules. */
|
|
@@ -922,7 +686,7 @@ export class MissionExecutor {
|
|
|
922
686
|
return this.persistMissionData(missionId, doc);
|
|
923
687
|
}
|
|
924
688
|
async buildMissionReport(missionId, group, groupTasks, allPassed) {
|
|
925
|
-
const state = await this.ctx.
|
|
689
|
+
const state = await this.ctx.taskStore.getState();
|
|
926
690
|
const processes = state?.processes ?? [];
|
|
927
691
|
const allFilesCreated = new Set();
|
|
928
692
|
const allFilesEdited = new Set();
|
|
@@ -974,23 +738,4 @@ export class MissionExecutor {
|
|
|
974
738
|
};
|
|
975
739
|
}
|
|
976
740
|
}
|
|
977
|
-
// ── ISO 8601 Duration Parser ──────────────────────────────────────────
|
|
978
|
-
// Supports: P[nY][nM][nW][nD][T[nH][nM][nS]]
|
|
979
|
-
// Examples: PT2H (2 hours), PT30M (30 min), P1D (1 day), P1DT6H (1 day 6 hours)
|
|
980
|
-
const ISO_DURATION_RE = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
981
|
-
function parseISO8601Duration(duration) {
|
|
982
|
-
const m = ISO_DURATION_RE.exec(duration);
|
|
983
|
-
if (!m)
|
|
984
|
-
throw new Error(`Invalid ISO 8601 duration: "${duration}"`);
|
|
985
|
-
const years = parseInt(m[1] || "0", 10);
|
|
986
|
-
const months = parseInt(m[2] || "0", 10);
|
|
987
|
-
const weeks = parseInt(m[3] || "0", 10);
|
|
988
|
-
const days = parseInt(m[4] || "0", 10);
|
|
989
|
-
const hours = parseInt(m[5] || "0", 10);
|
|
990
|
-
const minutes = parseInt(m[6] || "0", 10);
|
|
991
|
-
const seconds = parseFloat(m[7] || "0");
|
|
992
|
-
// Approximate: 1 year ≈ 365.25 days, 1 month ≈ 30.44 days
|
|
993
|
-
const totalDays = years * 365.25 + months * 30.44 + weeks * 7 + days;
|
|
994
|
-
return ((totalDays * 24 + hours) * 60 + minutes) * 60000 + seconds * 1000;
|
|
995
|
-
}
|
|
996
741
|
//# sourceMappingURL=mission-executor.js.map
|