@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.
Files changed (109) hide show
  1. package/dist/__tests__/mission-store.test.js +9 -13
  2. package/dist/__tests__/mission-store.test.js.map +1 -1
  3. package/dist/adapter.d.ts +10 -19
  4. package/dist/adapter.d.ts.map +1 -1
  5. package/dist/adapter.js +0 -7
  6. package/dist/adapter.js.map +1 -1
  7. package/dist/approval-manager.js +11 -11
  8. package/dist/approval-manager.js.map +1 -1
  9. package/dist/assessment-orchestrator.d.ts +1 -1
  10. package/dist/assessment-orchestrator.js +46 -46
  11. package/dist/assessment-orchestrator.js.map +1 -1
  12. package/dist/context-compactor.d.ts +1 -0
  13. package/dist/context-compactor.d.ts.map +1 -1
  14. package/dist/context-compactor.js.map +1 -1
  15. package/dist/escalation-manager.js +8 -8
  16. package/dist/escalation-manager.js.map +1 -1
  17. package/dist/events.d.ts +1 -25
  18. package/dist/events.d.ts.map +1 -1
  19. package/dist/hooks.d.ts +15 -9
  20. package/dist/hooks.d.ts.map +1 -1
  21. package/dist/hooks.js.map +1 -1
  22. package/dist/in-memory-stores.d.ts +21 -0
  23. package/dist/in-memory-stores.d.ts.map +1 -0
  24. package/dist/in-memory-stores.js +43 -0
  25. package/dist/in-memory-stores.js.map +1 -0
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/loop/hooks.d.ts +10 -28
  31. package/dist/loop/hooks.d.ts.map +1 -1
  32. package/dist/loop/hooks.js +2 -83
  33. package/dist/loop/hooks.js.map +1 -1
  34. package/dist/loop/step-helpers.d.ts +20 -0
  35. package/dist/loop/step-helpers.d.ts.map +1 -0
  36. package/dist/loop/step-helpers.js +69 -0
  37. package/dist/loop/step-helpers.js.map +1 -0
  38. package/dist/mission-executor.d.ts +16 -22
  39. package/dist/mission-executor.d.ts.map +1 -1
  40. package/dist/mission-executor.js +204 -459
  41. package/dist/mission-executor.js.map +1 -1
  42. package/dist/mission-gating.d.ts +89 -0
  43. package/dist/mission-gating.d.ts.map +1 -0
  44. package/dist/mission-gating.js +281 -0
  45. package/dist/mission-gating.js.map +1 -0
  46. package/dist/mission-store.d.ts +14 -8
  47. package/dist/mission-store.d.ts.map +1 -1
  48. package/dist/mission-store.js +19 -8
  49. package/dist/mission-store.js.map +1 -1
  50. package/dist/orchestrator-context.d.ts +1 -1
  51. package/dist/orchestrator-context.d.ts.map +1 -1
  52. package/dist/orchestrator-engine.d.ts +6 -6
  53. package/dist/orchestrator-engine.d.ts.map +1 -1
  54. package/dist/orchestrator-engine.js +10 -10
  55. package/dist/orchestrator-engine.js.map +1 -1
  56. package/dist/playbook-logic.d.ts +1 -1
  57. package/dist/playbook-logic.js +1 -1
  58. package/dist/scheduler.js +1 -1
  59. package/dist/scheduler.js.map +1 -1
  60. package/dist/schemas.d.ts +2 -2
  61. package/dist/session-store.d.ts +0 -2
  62. package/dist/session-store.d.ts.map +1 -1
  63. package/dist/session-store.js.map +1 -1
  64. package/dist/sla-monitor.js +3 -3
  65. package/dist/sla-monitor.js.map +1 -1
  66. package/dist/task-manager.d.ts +1 -1
  67. package/dist/task-manager.d.ts.map +1 -1
  68. package/dist/task-manager.js +37 -37
  69. package/dist/task-manager.js.map +1 -1
  70. package/dist/task-runner.d.ts.map +1 -1
  71. package/dist/task-runner.js +19 -36
  72. package/dist/task-runner.js.map +1 -1
  73. package/dist/task-store.d.ts +5 -19
  74. package/dist/task-store.d.ts.map +1 -1
  75. package/dist/tool-types.d.ts +0 -2
  76. package/dist/tool-types.d.ts.map +1 -1
  77. package/dist/types/agent.d.ts +186 -0
  78. package/dist/types/agent.d.ts.map +1 -0
  79. package/dist/types/agent.js +23 -0
  80. package/dist/types/agent.js.map +1 -0
  81. package/dist/types/assessment.d.ts +134 -0
  82. package/dist/types/assessment.d.ts.map +1 -0
  83. package/dist/types/assessment.js +18 -0
  84. package/dist/types/assessment.js.map +1 -0
  85. package/dist/types/config.d.ts +193 -0
  86. package/dist/types/config.d.ts.map +1 -0
  87. package/dist/types/config.js +8 -0
  88. package/dist/types/config.js.map +1 -0
  89. package/dist/types/mission.d.ts +226 -0
  90. package/dist/types/mission.d.ts.map +1 -0
  91. package/dist/types/mission.js +6 -0
  92. package/dist/types/mission.js.map +1 -0
  93. package/dist/types/notifications.d.ts +162 -0
  94. package/dist/types/notifications.d.ts.map +1 -0
  95. package/dist/types/notifications.js +6 -0
  96. package/dist/types/notifications.js.map +1 -0
  97. package/dist/types/task.d.ts +191 -0
  98. package/dist/types/task.d.ts.map +1 -0
  99. package/dist/types/task.js +5 -0
  100. package/dist/types/task.js.map +1 -0
  101. package/dist/types.d.ts +17 -1150
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/types.js +18 -31
  104. package/dist/types.js.map +1 -1
  105. package/package.json +1 -1
  106. package/dist/attachment-store.d.ts +0 -42
  107. package/dist/attachment-store.d.ts.map +0 -1
  108. package/dist/attachment-store.js +0 -13
  109. package/dist/attachment-store.js.map +0 -1
@@ -1,44 +1,10 @@
1
1
  import { sanitizeExpectations, parseMissionDocument } from "./schemas.js";
2
- import { resolveMissionStore } from "./mission-store.js";
3
- // ── In-memory fallback stores (no Node.js deps) ─────────────────────────
4
- class InMemoryCheckpointStore {
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
- cpStore;
53
- /** In-memory mirror of persisted checkpoint state (synced on every mutation) */
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.cpStore = ctx.checkpointStore ?? new InMemoryCheckpointStore();
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
- this.cpState = await this.cpStore.load();
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.registry.getAllTasks();
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.cpState?.definitions?.[group] ?? [];
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
- const checkpoints = this.cpState?.definitions?.[group];
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
- const cpKey = `${group}:${checkpointName}`;
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
- const result = [];
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.delayState?.definitions?.[group] ?? [];
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
- const delays = this.delayState?.definitions?.[group];
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
- const result = [];
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
- /** Resolve a mission by group name (helper for delay/checkpoint events). */
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.saveMission({
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 getAllMissions() {
323
- return await this.missions.getAllMissions();
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
- this.cpState = await this.cpStore.removeGroup(this.cpState, missionGroup);
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.getAllMissions();
380
- const state = await this.ctx.registry.getState();
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.registry.getState();
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.addTask({
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.cpState.definitions[group] = doc.checkpoints;
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.delayState.definitions[group] = doc.delays;
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.resolveMissionForGroup(groupTasks, group);
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
- this.cpState = await this.cpStore.removeGroup(this.cpState, group);
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
- const { doc } = await this.parseMissionData(missionId);
672
- // Enforce unique title
673
- if (doc.tasks.some(t => t.title === task.title)) {
674
- throw new Error(`Task title "${task.title}" already exists in this mission`);
675
- }
676
- doc.tasks.push(task);
677
- return this.persistMissionData(missionId, doc);
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
- const { doc } = await this.parseMissionData(missionId);
682
- const idx = doc.tasks.findIndex(t => t.title === taskTitle);
683
- if (idx === -1)
684
- throw new Error(`Task "${taskTitle}" not found in mission`);
685
- // If renaming, enforce unique title
686
- if (updates.title && updates.title !== taskTitle && doc.tasks.some(t => t.title === updates.title)) {
687
- throw new Error(`Task title "${updates.title}" already exists in this mission`);
688
- }
689
- doc.tasks[idx] = { ...doc.tasks[idx], ...updates };
690
- return this.persistMissionData(missionId, doc);
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
- const { doc } = await this.parseMissionData(missionId);
695
- const idx = doc.tasks.findIndex(t => t.title === taskTitle);
696
- if (idx === -1)
697
- throw new Error(`Task "${taskTitle}" not found in mission`);
698
- doc.tasks.splice(idx, 1);
699
- // Clean up dependsOn references in remaining tasks
700
- for (const t of doc.tasks) {
701
- if (t.dependsOn) {
702
- t.dependsOn = t.dependsOn.filter(d => d !== taskTitle);
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
- // Clean up quality gates and checkpoints that reference this task
706
- if (doc.qualityGates) {
707
- for (const gate of doc.qualityGates) {
708
- gate.afterTasks = gate.afterTasks.filter(t => t !== taskTitle);
709
- gate.blocksTasks = gate.blocksTasks.filter(t => t !== taskTitle);
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
- // Remove gates that became empty
712
- doc.qualityGates = doc.qualityGates.filter(g => g.afterTasks.length > 0 && g.blocksTasks.length > 0);
713
- }
714
- if (doc.checkpoints) {
715
- for (const cp of doc.checkpoints) {
716
- cp.afterTasks = cp.afterTasks.filter(t => t !== taskTitle);
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
- doc.checkpoints = doc.checkpoints.filter(cp => cp.afterTasks.length > 0 && cp.blocksTasks.length > 0);
720
- }
721
- if (doc.delays) {
722
- for (const dl of doc.delays) {
723
- dl.afterTasks = dl.afterTasks.filter(t => t !== taskTitle);
724
- dl.blocksTasks = dl.blocksTasks.filter(t => t !== taskTitle);
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
- doc.delays = doc.delays.filter(dl => dl.afterTasks.length > 0 && dl.blocksTasks.length > 0);
727
- }
728
- // Ensure at least 1 task remains (Zod will catch this, but give a nicer error)
729
- if (doc.tasks.length === 0)
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
- const { doc } = await this.parseMissionData(missionId);
736
- const titleSet = new Set(titles);
737
- if (titleSet.size !== titles.length)
738
- throw new Error("Duplicate titles in reorder list");
739
- const existing = new Set(doc.tasks.map(t => t.title));
740
- for (const t of titles) {
741
- if (!existing.has(t))
742
- throw new Error(`Task "${t}" not found in mission`);
743
- }
744
- if (titles.length !== doc.tasks.length)
745
- throw new Error("Reorder list must include all task titles");
746
- const taskMap = new Map(doc.tasks.map(t => [t.title, t]));
747
- doc.tasks = titles.map(t => taskMap.get(t));
748
- return this.persistMissionData(missionId, doc);
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
- const { doc } = await this.parseMissionData(missionId);
754
- if (!doc.checkpoints)
755
- doc.checkpoints = [];
756
- if (doc.checkpoints.some(c => c.name === checkpoint.name)) {
757
- throw new Error(`Checkpoint "${checkpoint.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
765
- if (!doc.checkpoints)
766
- throw new Error(`Checkpoint "${checkpointName}" not found in mission`);
767
- const idx = doc.checkpoints.findIndex(c => c.name === checkpointName);
768
- if (idx === -1)
769
- throw new Error(`Checkpoint "${checkpointName}" not found in mission`);
770
- if (updates.name && updates.name !== checkpointName && doc.checkpoints.some(c => c.name === updates.name)) {
771
- throw new Error(`Checkpoint "${updates.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
779
- if (!doc.checkpoints)
780
- throw new Error(`Checkpoint "${checkpointName}" not found in mission`);
781
- const idx = doc.checkpoints.findIndex(c => c.name === checkpointName);
782
- if (idx === -1)
783
- throw new Error(`Checkpoint "${checkpointName}" not found in mission`);
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
- const { doc } = await this.parseMissionData(missionId);
795
- if (!doc.delays)
796
- doc.delays = [];
797
- if (doc.delays.some(d => d.name === delay.name)) {
798
- throw new Error(`Delay "${delay.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
808
- if (!doc.delays)
809
- throw new Error(`Delay "${delayName}" not found in mission`);
810
- const idx = doc.delays.findIndex(d => d.name === delayName);
811
- if (idx === -1)
812
- throw new Error(`Delay "${delayName}" not found in mission`);
813
- if (updates.name && updates.name !== delayName && doc.delays.some(d => d.name === updates.name)) {
814
- throw new Error(`Delay "${updates.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
822
- if (!doc.delays)
823
- throw new Error(`Delay "${delayName}" not found in mission`);
824
- const idx = doc.delays.findIndex(d => d.name === delayName);
825
- if (idx === -1)
826
- throw new Error(`Delay "${delayName}" not found in mission`);
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
- const { doc } = await this.parseMissionData(missionId);
836
- if (!doc.qualityGates)
837
- doc.qualityGates = [];
838
- if (doc.qualityGates.some(g => g.name === gate.name)) {
839
- throw new Error(`Quality gate "${gate.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
847
- if (!doc.qualityGates)
848
- throw new Error(`Quality gate "${gateName}" not found in mission`);
849
- const idx = doc.qualityGates.findIndex(g => g.name === gateName);
850
- if (idx === -1)
851
- throw new Error(`Quality gate "${gateName}" not found in mission`);
852
- if (updates.name && updates.name !== gateName && doc.qualityGates.some(g => g.name === updates.name)) {
853
- throw new Error(`Quality gate "${updates.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
861
- if (!doc.qualityGates)
862
- throw new Error(`Quality gate "${gateName}" not found in mission`);
863
- const idx = doc.qualityGates.findIndex(g => g.name === gateName);
864
- if (idx === -1)
865
- throw new Error(`Quality gate "${gateName}" not found in mission`);
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
- const { doc } = await this.parseMissionData(missionId);
875
- if (!doc.team)
876
- doc.team = [];
877
- if (doc.team.some((m) => m.name === member.name)) {
878
- throw new Error(`Team member "${member.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
886
- if (!doc.team)
887
- throw new Error(`Team member "${memberName}" not found in mission`);
888
- const team = doc.team;
889
- const idx = team.findIndex((m) => m.name === memberName);
890
- if (idx === -1)
891
- throw new Error(`Team member "${memberName}" not found in mission`);
892
- if (updates.name && updates.name !== memberName && team.some((m) => m.name === updates.name)) {
893
- throw new Error(`Team member "${updates.name}" already exists in this mission`);
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
- const { doc } = await this.parseMissionData(missionId);
901
- if (!doc.team)
902
- throw new Error(`Team member "${memberName}" not found in mission`);
903
- const team = doc.team;
904
- const idx = team.findIndex((m) => m.name === memberName);
905
- if (idx === -1)
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.registry.getState();
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