@h-rig/core 0.0.6-alpha.155 → 0.0.6-alpha.156

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/src/index.js CHANGED
@@ -1,97 +1,10 @@
1
1
  // @bun
2
2
  // packages/core/src/define-plugin.ts
3
3
  import { Schema } from "effect";
4
- import { RigPlugin } from "@rig/contracts";
5
- function definePlugin(meta, runtime) {
6
- const validated = Schema.decodeUnknownSync(RigPlugin)(meta);
7
- if (!runtime) {
8
- return validated;
9
- }
10
- const declaredValidators = new Map((validated.contributes?.validators ?? []).map((v) => [v.id, v]));
11
- const runtimeValidators = new Map((runtime.validators ?? []).map((v) => [v.id, v]));
12
- for (const v of runtimeValidators.values()) {
13
- const metadata = declaredValidators.get(v.id);
14
- if (!metadata) {
15
- throw new Error(`definePlugin(${validated.name}): executable validator "${v.id}" has no matching metadata entry in contributes.validators`);
16
- }
17
- if (metadata.category !== v.category) {
18
- throw new Error(`definePlugin(${validated.name}): executable validator "${v.id}" category "${v.category}" does not match metadata category "${metadata.category}"`);
19
- }
20
- }
21
- if (runtime.validators) {
22
- for (const v of declaredValidators.values()) {
23
- if (!runtimeValidators.has(v.id)) {
24
- throw new Error(`definePlugin(${validated.name}): validator metadata "${v.id}" has no runtime implementation`);
25
- }
26
- }
27
- }
28
- const declaredSources = new Map((validated.contributes?.taskSources ?? []).map((s) => [s.id, s]));
29
- const runtimeSources = new Map((runtime.taskSources ?? []).map((s) => [s.id, s]));
30
- for (const s of runtimeSources.values()) {
31
- const metadata = declaredSources.get(s.id);
32
- if (!metadata) {
33
- throw new Error(`definePlugin(${validated.name}): executable task source "${s.id}" has no matching metadata entry in contributes.taskSources`);
34
- }
35
- if (metadata.kind !== s.kind) {
36
- throw new Error(`definePlugin(${validated.name}): executable task source "${s.id}" kind "${s.kind}" does not match metadata kind "${metadata.kind}"`);
37
- }
38
- }
39
- if (runtime.taskSources) {
40
- for (const s of declaredSources.values()) {
41
- if (!runtimeSources.has(s.id)) {
42
- throw new Error(`definePlugin(${validated.name}): task source metadata "${s.id}" has no runtime implementation`);
43
- }
44
- }
45
- }
46
- const declaredHooks = new Map((validated.contributes?.hooks ?? []).map((h) => [h.id, h]));
47
- for (const hookId of Object.keys(runtime.hooks ?? {})) {
48
- const metadata = declaredHooks.get(hookId);
49
- if (!metadata) {
50
- throw new Error(`definePlugin(${validated.name}): typed hook "${hookId}" has no matching metadata entry in contributes.hooks`);
51
- }
52
- if (metadata.command) {
53
- throw new Error(`definePlugin(${validated.name}): hook "${hookId}" has both a typed implementation and a command string \u2014 pick one`);
54
- }
55
- }
56
- if (runtime.hooks) {
57
- for (const h of declaredHooks.values()) {
58
- if (!runtime.hooks[h.id] && !h.command) {
59
- throw new Error(`definePlugin(${validated.name}): hook metadata "${h.id}" has no implementation (typed function or command)`);
60
- }
61
- }
62
- }
63
- const declaredCapabilities = new Map((validated.contributes?.capabilities ?? []).map((capability) => [capability.id, capability]));
64
- for (const capability of runtime.featureCapabilities ?? []) {
65
- if (!declaredCapabilities.has(capability.id)) {
66
- throw new Error(`definePlugin(${validated.name}): executable capability "${capability.id}" has no matching metadata entry in contributes.capabilities`);
67
- }
68
- }
69
- const declaredPanels = new Map((validated.contributes?.panels ?? []).map((panel) => [panel.id, panel]));
70
- for (const panel of runtime.panels ?? []) {
71
- const metadata = declaredPanels.get(panel.id);
72
- if (!metadata) {
73
- throw new Error(`definePlugin(${validated.name}): executable panel "${panel.id}" has no matching metadata entry in contributes.panels`);
74
- }
75
- if (metadata.slot !== panel.slot) {
76
- throw new Error(`definePlugin(${validated.name}): executable panel "${panel.id}" slot "${panel.slot}" does not match metadata slot "${metadata.slot}"`);
77
- }
78
- if (metadata.capabilityId !== panel.capabilityId) {
79
- throw new Error(`definePlugin(${validated.name}): executable panel "${panel.id}" capabilityId "${panel.capabilityId ?? "(none)"}" does not match metadata capabilityId "${metadata.capabilityId ?? "(none)"}"`);
80
- }
81
- }
82
- const declaredBlockerClassifiers = new Map((validated.contributes?.blockerClassifiers ?? []).map((classifier) => [classifier.id, classifier]));
83
- for (const classifier of runtime.blockerClassifiers ?? []) {
84
- if (!declaredBlockerClassifiers.has(classifier.id)) {
85
- throw new Error(`definePlugin(${validated.name}): executable blocker classifier "${classifier.id}" has no matching metadata entry in contributes.blockerClassifiers`);
86
- }
87
- }
88
- const declaredCliCommands = new Map((validated.contributes?.cliCommands ?? []).map((command) => [command.id, command]));
89
- for (const command of runtime.cliCommands ?? []) {
90
- if (!declaredCliCommands.has(command.id)) {
91
- throw new Error(`definePlugin(${validated.name}): executable cli command "${command.id}" has no matching metadata entry in contributes.cliCommands`);
92
- }
93
- }
94
- return { ...validated, ...runtime };
4
+ import { RigPlugin as RigPluginManifest } from "@rig/contracts";
5
+ function definePlugin(plugin) {
6
+ Schema.decodeUnknownSync(RigPluginManifest)(plugin);
7
+ return plugin;
95
8
  }
96
9
  // packages/core/src/define-config.ts
97
10
  import { Schema as Schema2 } from "effect";
@@ -144,102 +57,8 @@ function assertUniquePluginNames(plugins) {
144
57
  seen.add(plugin.name);
145
58
  }
146
59
  }
147
- function assertRuntimeMatchesMetadata(plugin) {
148
- const declaredValidators = new Map((plugin.contributes?.validators ?? []).map((validator) => [validator.id, validator]));
149
- const runtimeValidators = new Map((plugin?.validators ?? []).map((validator) => [validator.id, validator]));
150
- for (const validator of runtimeValidators.values()) {
151
- const metadata = declaredValidators.get(validator.id);
152
- if (!metadata) {
153
- throw new Error(`plugin "${plugin.name}" executable validator "${validator.id}" has no matching metadata entry in contributes.validators`);
154
- }
155
- if (metadata.category !== validator.category) {
156
- throw new Error(`plugin "${plugin.name}" executable validator "${validator.id}" category "${validator.category}" does not match metadata category "${metadata.category}"`);
157
- }
158
- }
159
- if (plugin?.validators) {
160
- for (const validator of declaredValidators.values()) {
161
- if (!runtimeValidators.has(validator.id)) {
162
- throw new Error(`plugin "${plugin.name}" validator metadata "${validator.id}" has no runtime implementation`);
163
- }
164
- }
165
- }
166
- const declaredTaskSources = new Map((plugin.contributes?.taskSources ?? []).map((source) => [source.id, source]));
167
- const runtimeTaskSources = new Map((plugin?.taskSources ?? []).map((source) => [source.id, source]));
168
- for (const source of runtimeTaskSources.values()) {
169
- const metadata = declaredTaskSources.get(source.id);
170
- if (!metadata) {
171
- throw new Error(`plugin "${plugin.name}" executable task source "${source.id}" has no matching metadata entry in contributes.taskSources`);
172
- }
173
- if (metadata.kind !== source.kind) {
174
- throw new Error(`plugin "${plugin.name}" executable task source "${source.id}" kind "${source.kind}" does not match metadata kind "${metadata.kind}"`);
175
- }
176
- }
177
- if (plugin?.taskSources) {
178
- for (const source of declaredTaskSources.values()) {
179
- if (!runtimeTaskSources.has(source.id)) {
180
- throw new Error(`plugin "${plugin.name}" task source metadata "${source.id}" has no runtime implementation`);
181
- }
182
- }
183
- }
184
- const declaredHooks = new Map((plugin.contributes?.hooks ?? []).map((hook) => [hook.id, hook]));
185
- const runtimeHooks = plugin?.hooks;
186
- for (const hookId of Object.keys(runtimeHooks ?? {})) {
187
- const metadata = declaredHooks.get(hookId);
188
- if (!metadata) {
189
- throw new Error(`plugin "${plugin.name}" typed hook "${hookId}" has no matching metadata entry in contributes.hooks`);
190
- }
191
- if (metadata.command) {
192
- throw new Error(`plugin "${plugin.name}" hook "${hookId}" has both a typed implementation and a command string \u2014 pick one`);
193
- }
194
- }
195
- if (runtimeHooks) {
196
- for (const hook of declaredHooks.values()) {
197
- if (!runtimeHooks[hook.id] && !hook.command) {
198
- throw new Error(`plugin "${plugin.name}" hook metadata "${hook.id}" has no implementation (typed function or command)`);
199
- }
200
- }
201
- }
202
- const declaredCapabilities = new Map((plugin.contributes?.capabilities ?? []).map((capability) => [capability.id, capability]));
203
- const runtimeCapabilities = new Map((plugin?.featureCapabilities ?? []).map((capability) => [capability.id, capability]));
204
- for (const capability of runtimeCapabilities.values()) {
205
- if (!declaredCapabilities.has(capability.id)) {
206
- throw new Error(`plugin "${plugin.name}" executable capability "${capability.id}" has no matching metadata entry in contributes.capabilities`);
207
- }
208
- }
209
- const declaredPanels = new Map((plugin.contributes?.panels ?? []).map((panel) => [panel.id, panel]));
210
- const runtimePanels = new Map((plugin?.panels ?? []).map((panel) => [panel.id, panel]));
211
- for (const panel of runtimePanels.values()) {
212
- const metadata = declaredPanels.get(panel.id);
213
- if (!metadata) {
214
- throw new Error(`plugin "${plugin.name}" executable panel "${panel.id}" has no matching metadata entry in contributes.panels`);
215
- }
216
- if (metadata.slot !== panel.slot) {
217
- throw new Error(`plugin "${plugin.name}" executable panel "${panel.id}" slot "${panel.slot}" does not match metadata slot "${metadata.slot}"`);
218
- }
219
- if (metadata.capabilityId !== panel.capabilityId) {
220
- throw new Error(`plugin "${plugin.name}" executable panel "${panel.id}" capabilityId "${panel.capabilityId ?? "(none)"}" does not match metadata capabilityId "${metadata.capabilityId ?? "(none)"}"`);
221
- }
222
- }
223
- const declaredBlockerClassifiers = new Map((plugin.contributes?.blockerClassifiers ?? []).map((classifier) => [classifier.id, classifier]));
224
- const runtimeBlockerClassifiers = new Map((plugin?.blockerClassifiers ?? []).map((classifier) => [classifier.id, classifier]));
225
- for (const classifier of runtimeBlockerClassifiers.values()) {
226
- if (!declaredBlockerClassifiers.has(classifier.id)) {
227
- throw new Error(`plugin "${plugin.name}" executable blocker classifier "${classifier.id}" has no matching metadata entry in contributes.blockerClassifiers`);
228
- }
229
- }
230
- const declaredCliCommands = new Map((plugin.contributes?.cliCommands ?? []).map((command) => [command.id, command]));
231
- const runtimeCliCommands = new Map((plugin?.cliCommands ?? []).map((command) => [command.id, command]));
232
- for (const command of runtimeCliCommands.values()) {
233
- if (!declaredCliCommands.has(command.id)) {
234
- throw new Error(`plugin "${plugin.name}" executable cli command "${command.id}" has no matching metadata entry in contributes.cliCommands`);
235
- }
236
- }
237
- }
238
60
  function createPluginHost(plugins) {
239
61
  assertUniquePluginNames(plugins);
240
- for (const plugin of plugins) {
241
- assertRuntimeMatchesMetadata(plugin);
242
- }
243
62
  const validators = [];
244
63
  const hooks = [];
245
64
  const skills = [];
@@ -251,38 +70,15 @@ function createPluginHost(plugins) {
251
70
  const capabilities = [];
252
71
  const panels = [];
253
72
  const blockerClassifiers = [];
73
+ const sessionExtensions = [];
254
74
  const stages = [];
255
75
  const stageMutations = [];
256
76
  const stageExecutors = {};
257
- const executableValidators = [];
258
- const executableTaskSources = [];
259
- const executableCapabilities = [];
260
- const executablePanels = [];
261
- const executableBlockerClassifiers = [];
262
- const executableCliCommands = [];
263
77
  for (const plugin of plugins) {
264
78
  const c = plugin.contributes;
265
79
  if (!c)
266
80
  continue;
267
81
  const pluginName = plugin.name;
268
- if (plugin?.validators) {
269
- executableValidators.push(...plugin.validators.map((item) => ({ item, pluginName })));
270
- }
271
- if (plugin?.taskSources) {
272
- executableTaskSources.push(...plugin.taskSources.map((item) => ({ item, pluginName })));
273
- }
274
- if (plugin?.featureCapabilities) {
275
- executableCapabilities.push(...plugin.featureCapabilities.map((item) => ({ item, pluginName })));
276
- }
277
- if (plugin?.panels) {
278
- executablePanels.push(...plugin.panels.map((item) => ({ item, pluginName })));
279
- }
280
- if (plugin?.blockerClassifiers) {
281
- executableBlockerClassifiers.push(...plugin.blockerClassifiers.map((item) => ({ item, pluginName })));
282
- }
283
- if (plugin?.cliCommands) {
284
- executableCliCommands.push(...plugin.cliCommands.map((item) => ({ item, pluginName })));
285
- }
286
82
  if (c.validators)
287
83
  validators.push(...c.validators.map((item) => ({ item, pluginName })));
288
84
  if (c.hooks)
@@ -305,27 +101,17 @@ function createPluginHost(plugins) {
305
101
  panels.push(...c.panels.map((item) => ({ item, pluginName })));
306
102
  if (c.blockerClassifiers)
307
103
  blockerClassifiers.push(...c.blockerClassifiers.map((item) => ({ item, pluginName })));
308
- if (c.stages)
309
- stages.push(...c.stages.map((item) => ({ item, pluginName })));
104
+ if (c.sessionExtensions)
105
+ sessionExtensions.push(...c.sessionExtensions.map((item) => ({ item, pluginName })));
310
106
  if (c.stageMutations)
311
107
  stageMutations.push(...c.stageMutations.map((item) => ({ item, pluginName })));
312
- if (plugin?.stages)
313
- Object.assign(stageExecutors, plugin.stages);
314
- }
315
- indexById(executableValidators, "executableValidator");
316
- indexById(executableTaskSources, "executableTaskSource");
317
- indexById(executableCapabilities, "executableCapability");
318
- indexById(executablePanels, "executablePanel");
319
- indexById(executableBlockerClassifiers, "executableBlockerClassifier");
320
- indexById(executableCliCommands, "executableCliCommand");
321
- const taskSourceFactoryByKind = new Map;
322
- const taskSourceKindRegistrant = new Map;
323
- for (const { item, pluginName } of executableTaskSources) {
324
- if (taskSourceFactoryByKind.has(item.kind)) {
325
- throw new Error(`duplicate task source kind "${item.kind}": registered by plugins "${taskSourceKindRegistrant.get(item.kind)}" and "${pluginName}"`);
108
+ if (c.stages) {
109
+ for (const stage of c.stages) {
110
+ stages.push({ item: stage, pluginName });
111
+ if (stage.run)
112
+ stageExecutors[stage.id] = stage.run;
113
+ }
326
114
  }
327
- taskSourceFactoryByKind.set(item.kind, item);
328
- taskSourceKindRegistrant.set(item.kind, pluginName);
329
115
  }
330
116
  const validatorMap = indexById(validators, "validator");
331
117
  const hookMap = indexById(hooks, "hook");
@@ -338,6 +124,16 @@ function createPluginHost(plugins) {
338
124
  const capabilityMap = indexById(capabilities, "capability");
339
125
  const panelMap = indexById(panels, "panel");
340
126
  const blockerClassifierMap = indexById(blockerClassifiers, "blockerClassifier");
127
+ indexById(sessionExtensions, "sessionExtension");
128
+ const taskSourceFactoryByKind = new Map;
129
+ const taskSourceKindRegistrant = new Map;
130
+ for (const { item, pluginName } of taskSources) {
131
+ if (taskSourceFactoryByKind.has(item.kind)) {
132
+ throw new Error(`duplicate task source kind "${item.kind}": registered by plugins "${taskSourceKindRegistrant.get(item.kind)}" and "${pluginName}"`);
133
+ }
134
+ taskSourceFactoryByKind.set(item.kind, item);
135
+ taskSourceKindRegistrant.set(item.kind, pluginName);
136
+ }
341
137
  const allValidators = validators.map((c) => c.item);
342
138
  const allHooks = hooks.map((c) => c.item);
343
139
  const allSkills = skills.map((c) => c.item);
@@ -351,12 +147,7 @@ function createPluginHost(plugins) {
351
147
  const allCapabilities = capabilities.map((c) => c.item);
352
148
  const allPanels = panels.map((c) => c.item);
353
149
  const allBlockerClassifiers = blockerClassifiers.map((c) => c.item);
354
- const allExecutableValidators = executableValidators.map((c) => c.item);
355
- const allExecutableTaskSources = executableTaskSources.map((c) => c.item);
356
- const allExecutableCapabilities = executableCapabilities.map((c) => c.item);
357
- const allExecutablePanels = executablePanels.map((c) => c.item);
358
- const allExecutableBlockerClassifiers = executableBlockerClassifiers.map((c) => c.item);
359
- const allExecutableCliCommands = executableCliCommands.map((c) => c.item);
150
+ const allSessionExtensions = sessionExtensions.map((c) => c.item);
360
151
  const executableCliCommandByName = new Map;
361
152
  const registerExecutableCliCommandSelector = (selector, contribution) => {
362
153
  const existing = executableCliCommandByName.get(selector);
@@ -365,7 +156,7 @@ function createPluginHost(plugins) {
365
156
  }
366
157
  executableCliCommandByName.set(selector, contribution);
367
158
  };
368
- for (const contribution of executableCliCommands) {
159
+ for (const contribution of cliCommands) {
369
160
  const command = contribution.item;
370
161
  const family = command.family ?? command.id;
371
162
  registerExecutableCliCommandSelector(command.id, contribution);
@@ -400,3485 +191,23 @@ function createPluginHost(plugins) {
400
191
  listStages: () => allStages,
401
192
  listStageMutations: () => allStageMutations,
402
193
  listStageExecutors: () => stageExecutors,
403
- listExecutableValidators: () => allExecutableValidators,
404
- listExecutableTaskSources: () => allExecutableTaskSources,
405
- listExecutableCapabilities: () => allExecutableCapabilities,
406
- listExecutablePanels: () => allExecutablePanels,
407
- listExecutableBlockerClassifiers: () => allExecutableBlockerClassifiers,
408
- listExecutableCliCommands: () => allExecutableCliCommands,
194
+ listExecutableValidators: () => allValidators.filter((v) => typeof v.run === "function"),
195
+ listExecutableTaskSources: () => allTaskSources,
196
+ listExecutableCapabilities: () => allCapabilities.filter((c) => typeof c.run === "function"),
197
+ listExecutablePanels: () => allPanels.filter((p) => typeof p.produce === "function"),
198
+ listExecutableBlockerClassifiers: () => allBlockerClassifiers,
199
+ listExecutableCliCommands: () => allCliCommands,
200
+ listSessionExtensions: () => allSessionExtensions,
409
201
  resolveExecutableCliCommand: (requested) => executableCliCommandByName.get(requested)?.item,
410
202
  resolveTaskSourceFactoryByKind: (kind) => taskSourceFactoryByKind.get(kind)
411
203
  };
412
204
  }
413
- // packages/core/src/rig-init-builder.ts
414
- function buildRigInitConfigSource(input) {
415
- const lines = [`import { defineConfig } from "@rig/core/config";`];
416
- if (input.useStandardPlugin) {
417
- lines.push(`import { standardPlugins } from "@rig/standard-plugin/bundle";`);
418
- if (input.taskSource.kind === "github-issues") {
419
- lines.push(`import { createStateGitHubCredentialProvider } from "@rig/standard-plugin";`);
420
- }
421
- }
422
- lines.push(``, `export default defineConfig({`);
423
- const projectRepo = input.projectRepo ?? (input.taskSource.kind === "github-issues" ? `${input.taskSource.owner}/${input.taskSource.repo}` : undefined);
424
- lines.push(projectRepo ? ` project: { name: ${JSON.stringify(input.projectName)}, repo: ${JSON.stringify(projectRepo)} },` : ` project: { name: ${JSON.stringify(input.projectName)} },`);
425
- if (input.useStandardPlugin && input.taskSource.kind === "github-issues") {
426
- lines.push(` plugins: [...standardPlugins({`);
427
- lines.push(` taskSources: {`);
428
- lines.push(` githubCredentialProvider: createStateGitHubCredentialProvider(),`);
429
- lines.push(` githubWorkspaceId: ${JSON.stringify(`${input.taskSource.owner}/${input.taskSource.repo}`)},`);
430
- lines.push(` githubUserId: process.env.RIG_GITHUB_USER_ID ?? "server-selected-user",`);
431
- lines.push(` },`);
432
- lines.push(` })],`);
433
- } else {
434
- lines.push(` plugins: [${input.useStandardPlugin ? "...standardPlugins()" : ""}],`);
435
- }
436
- if (input.taskSource.kind === "github-issues") {
437
- lines.push(` taskSource: {`);
438
- lines.push(` kind: "github-issues",`);
439
- lines.push(` owner: ${JSON.stringify(input.taskSource.owner)},`);
440
- lines.push(` repo: ${JSON.stringify(input.taskSource.repo)},`);
441
- lines.push(` // labels: ["task"], // uncomment to filter by labels`);
442
- lines.push(` state: "open",`);
443
- if (input.taskSource.assignee?.trim()) {
444
- lines.push(` options: { assignee: ${JSON.stringify(input.taskSource.assignee.trim())} },`);
445
- }
446
- lines.push(` },`);
447
- } else {
448
- lines.push(` taskSource: {`);
449
- lines.push(` kind: "files",`);
450
- lines.push(` path: ${JSON.stringify(input.taskSource.path)},`);
451
- lines.push(` },`);
452
- }
453
- lines.push(` workspace: { mainRepo: ".", isolation: "worktree" },`);
454
- const sshTarget = input.sshTarget?.trim();
455
- lines.push(sshTarget ? ` runtime: { harness: "pi", mode: "yolo", server: { sshTarget: ${JSON.stringify(sshTarget)} } },` : ` runtime: { harness: "pi", mode: "yolo" }, // server.sshTarget unset = local placement`);
456
- lines.push(` planning: { mode: "auto" },`);
457
- lines.push(` github: {`);
458
- lines.push(` issueUpdates: "lifecycle",`);
459
- lines.push(` projects: { enabled: false },`);
460
- lines.push(` },`);
461
- lines.push(` automation: { maxValidationAttempts: 30, maxPrFixIterations: 100500 },`);
462
- lines.push(` pr: { mode: "auto", watchChecks: true, autoFixChecks: true, autoFixReview: true },`);
463
- lines.push(` merge: { mode: "auto", method: "repo-default", deleteBranch: "repo-default", bypass: false },`);
464
- lines.push(` issueAnalysis: { enabled: true, harness: "pi", mode: "continuous" },`);
465
- lines.push(`});`);
466
- lines.push(``);
467
- return lines.join(`
468
- `);
469
- }
470
- // packages/core/src/engineReadModelReducer.ts
471
- import {
472
- ActionId,
473
- ConversationId,
474
- EngineRuntimeId,
475
- MessageId,
476
- RunId,
477
- TaskId,
478
- WorkspaceId,
479
- WorktreeId
480
- } from "@rig/contracts";
481
- var CANONICAL_RUNTIME_ADAPTER = "pi";
482
- function isRecord(value) {
483
- return typeof value === "object" && value !== null;
484
- }
485
- function readString(payload, key) {
486
- const value = payload[key];
487
- return typeof value === "string" ? value : undefined;
488
- }
489
- function readNullableString(payload, key) {
490
- const value = payload[key];
491
- return typeof value === "string" ? value : value === null ? null : undefined;
492
- }
493
- function readRecord(payload, key) {
494
- const value = payload[key];
495
- return isRecord(value) ? value : undefined;
496
- }
497
- function asWorkspaceId(value) {
498
- return WorkspaceId.makeUnsafe(value);
499
- }
500
- function asTaskId(value) {
501
- return TaskId.makeUnsafe(value);
502
- }
503
- function asRunId(value) {
504
- return RunId.makeUnsafe(value);
505
- }
506
- function conversationIdFromRunId(value) {
507
- return ConversationId.makeUnsafe(value);
508
- }
509
- function asActionId(value) {
510
- return ActionId.makeUnsafe(value);
511
- }
512
- function runtimeIdFromRunId(value) {
513
- return EngineRuntimeId.makeUnsafe(value);
514
- }
515
- function worktreeIdFromRunId(value) {
516
- return WorktreeId.makeUnsafe(value);
517
- }
518
- function asMessageId(value) {
519
- return MessageId.makeUnsafe(value);
520
- }
521
- function upsertById(items, entry) {
522
- const index = items.findIndex((candidate) => candidate.id === entry.id);
523
- if (index < 0) {
524
- return [...items, entry];
525
- }
526
- const next = items.slice();
527
- next[index] = entry;
528
- return next;
529
- }
530
- function removeById(items, id) {
531
- return items.filter((candidate) => candidate.id !== id);
532
- }
533
- function patchById(items, id, updater) {
534
- const index = items.findIndex((candidate) => candidate.id === id);
535
- if (index < 0) {
536
- return items.slice();
537
- }
538
- const next = items.slice();
539
- next[index] = updater(next[index]);
540
- return next;
541
- }
542
- function upsertByKey(items, entry, key) {
543
- const index = items.findIndex((candidate) => candidate[key] === entry[key]);
544
- if (index < 0) {
545
- return [...items, entry];
546
- }
547
- const next = items.slice();
548
- next[index] = entry;
549
- return next;
550
- }
551
- function patchByKey(items, keyValue, key, updater) {
552
- const index = items.findIndex((candidate) => candidate[key] === keyValue);
553
- if (index < 0) {
554
- return items.slice();
555
- }
556
- const next = items.slice();
557
- next[index] = updater(next[index]);
558
- return next;
559
- }
560
- function mergeById(items, incoming) {
561
- return incoming.reduce((acc, item) => upsertById(acc, item), [...items]);
562
- }
563
- function replaceWorkspaceSlice(items, workspaceId, incoming) {
564
- return [...items.filter((item) => item.workspaceId !== workspaceId), ...incoming];
565
- }
566
- function withQueuePositions(items) {
567
- return items.map((item, index) => Object.assign({}, item, {
568
- position: index
569
- }));
570
- }
571
- function maxIsoDate(left, right) {
572
- return left.localeCompare(right) >= 0 ? left : right;
573
- }
574
- var REMOTE_HOST_STATUS_ONLINE = new Set([
575
- "ready",
576
- "busy",
577
- "degraded",
578
- "draining"
579
- ]);
580
- function deriveRemoteFleetStatus(hosts, warnings) {
581
- if (hosts.length === 0)
582
- return "empty";
583
- if (warnings.length > 0)
584
- return "degraded";
585
- if (hosts.some((host) => host.status === "degraded" || host.status === "quarantined")) {
586
- return "degraded";
587
- }
588
- const onlineHostCount = hosts.filter((host) => REMOTE_HOST_STATUS_ONLINE.has(host.status)).length;
589
- return onlineHostCount > 0 ? "ready" : "degraded";
590
- }
591
- function buildRemoteFleetSummary(input) {
592
- const warnings = input.fleet?.warnings ?? [];
593
- const manifestCount = input.fleet?.manifestCount ?? 0;
594
- const onlineHostCount = input.hosts.filter((host) => REMOTE_HOST_STATUS_ONLINE.has(host.status)).length;
595
- return {
596
- updatedAt: input.updatedAt,
597
- status: deriveRemoteFleetStatus(input.hosts, warnings),
598
- manifestCount,
599
- hostCount: input.hosts.length,
600
- onlineHostCount,
601
- hosts: [...input.hosts].sort((left, right) => left.name.localeCompare(right.name)),
602
- warnings
603
- };
604
- }
605
- function toRunMode(interactionMode) {
606
- return interactionMode === "plan" ? "supervised" : "interactive";
607
- }
608
- function mapLegacySessionStatusToRunStatus(status) {
609
- switch (status) {
610
- case "starting":
611
- return "preparing";
612
- case "running":
613
- return "running";
614
- case "ready":
615
- return "completed";
616
- case "interrupted":
617
- return "paused";
618
- case "error":
619
- return "failed";
620
- case "stopped":
621
- return "stopped";
622
- default:
623
- return "created";
624
- }
625
- }
626
- function mapLegacySessionStatusToRuntimeStatus(status) {
627
- switch (status) {
628
- case "starting":
629
- return "starting";
630
- case "running":
631
- return "running";
632
- case "interrupted":
633
- return "interrupted";
634
- case "error":
635
- return "failed";
636
- default:
637
- return "exited";
638
- }
639
- }
640
- function mapTaskStatusFromRunStatus(status, fallback) {
641
- if (fallback === "blocked" || fallback === "cancelled" || fallback === "completed" || fallback === "closed" || fallback === "unknown") {
642
- return fallback === "closed" ? "completed" : fallback;
643
- }
644
- switch (status) {
645
- case "created":
646
- return fallback;
647
- case "queued":
648
- return "queued";
649
- case "preparing":
650
- case "running":
651
- case "waiting-approval":
652
- case "waiting-user-input":
653
- case "validating":
654
- case "paused":
655
- return "in_progress";
656
- case "reviewing":
657
- case "closing-out":
658
- return "under_review";
659
- case "needs-attention":
660
- return "blocked";
661
- case "completed":
662
- return "completed";
663
- case "failed":
664
- return "ready";
665
- case "stopped":
666
- return "cancelled";
667
- }
668
- }
669
- function withSnapshotMetadata(snapshot, event, next) {
670
- return {
671
- ...snapshot,
672
- ...next,
673
- snapshotSequence: Math.max(snapshot.snapshotSequence, event.sequence),
674
- updatedAt: maxIsoDate(snapshot.updatedAt, event.createdAt)
675
- };
676
- }
677
- function ensureConversation(snapshot, run) {
678
- const conversationId = conversationIdFromRunId(run.id);
679
- if (snapshot.conversations.some((conversation) => conversation.id === conversationId)) {
680
- return snapshot;
681
- }
682
- return {
683
- ...snapshot,
684
- conversations: [
685
- ...snapshot.conversations,
686
- {
687
- id: conversationId,
688
- runId: run.id,
689
- title: run.title,
690
- createdAt: run.createdAt,
691
- updatedAt: run.updatedAt
692
- }
693
- ]
694
- };
695
- }
696
- function updateTaskFromRun(snapshot, run) {
697
- if (!run.taskId) {
698
- return snapshot;
699
- }
700
- return {
701
- ...snapshot,
702
- tasks: patchById(snapshot.tasks, run.taskId, (task) => ({
703
- ...task,
704
- status: mapTaskStatusFromRunStatus(run.status, task.status),
705
- updatedAt: maxIsoDate(task.updatedAt, run.updatedAt)
706
- }))
707
- };
708
- }
709
- function applyRun(snapshot, run) {
710
- const withConversation = ensureConversation(snapshot, run);
711
- const nextRuns = upsertById(withConversation.runs, run);
712
- return updateTaskFromRun({ ...withConversation, runs: nextRuns }, run);
713
- }
714
- function applyRuntime(snapshot, runtime) {
715
- return {
716
- ...snapshot,
717
- runtimes: upsertById(snapshot.runtimes, runtime)
718
- };
719
- }
720
- function applyWorktree(snapshot, worktree) {
721
- return {
722
- ...snapshot,
723
- worktrees: upsertById(snapshot.worktrees, worktree)
724
- };
725
- }
726
- function countPendingApprovals(approvals, runId) {
727
- return approvals.filter((approval) => approval.runId === runId && approval.status === "pending").length;
728
- }
729
- function countPendingUserInputs(userInputs, actions, runId) {
730
- const persistedCount = (userInputs ?? []).filter((request) => request.runId === runId && request.status === "pending").length;
731
- if (persistedCount > 0) {
732
- return persistedCount;
733
- }
734
- const openRequestIds = new Set;
735
- for (const action of actions) {
736
- if (action.runId !== runId || !isRecord(action.payload)) {
737
- continue;
738
- }
739
- const requestId = readString(action.payload, "requestId");
740
- if (!requestId) {
741
- continue;
742
- }
743
- if (action.actionType === "user-input.requested") {
744
- openRequestIds.add(requestId);
745
- }
746
- if (action.actionType === "user-input.resolved") {
747
- openRequestIds.delete(requestId);
748
- }
749
- }
750
- return openRequestIds.size;
751
- }
752
- function reconcileRunCounts(snapshot, runId, statusOverride) {
753
- return {
754
- ...snapshot,
755
- runs: patchById(snapshot.runs, runId, (run) => {
756
- const pendingApprovalCount = countPendingApprovals(snapshot.approvals, runId);
757
- const pendingUserInputCount = countPendingUserInputs(snapshot.userInputs, snapshot.actions, runId);
758
- const nextStatus = statusOverride ?? (pendingApprovalCount > 0 ? "waiting-approval" : pendingUserInputCount > 0 ? "waiting-user-input" : run.status === "waiting-approval" || run.status === "waiting-user-input" ? "running" : run.status);
759
- return {
760
- ...run,
761
- pendingApprovalCount,
762
- pendingUserInputCount,
763
- status: nextStatus
764
- };
765
- })
766
- };
767
- }
768
- function applyApprovalActivity(approvals, runId, action) {
769
- if (!isRecord(action.payload)) {
770
- return approvals.slice();
771
- }
772
- const requestId = readString(action.payload, "requestId");
773
- if (!requestId) {
774
- return approvals.slice();
775
- }
776
- if (action.actionType === "approval.requested") {
777
- const requestKind = readString(action.payload, "requestKind") ?? "command";
778
- return upsertById(approvals, {
779
- id: requestId,
780
- runId,
781
- actionId: action.id,
782
- requestKind,
783
- status: "pending",
784
- payload: action.payload,
785
- createdAt: action.startedAt,
786
- resolvedAt: null
787
- });
788
- }
789
- const shouldResolve = action.actionType === "approval.resolved" || action.actionType === "provider.approval.respond.failed" && ((readString(action.payload, "detail") ?? "").includes("Unknown pending permission request") || (readString(action.payload, "message") ?? "").includes("Unknown pending permission request"));
790
- if (!shouldResolve) {
791
- return approvals.slice();
792
- }
793
- return approvals.map((approval) => approval.id === requestId ? {
794
- ...approval,
795
- status: "resolved",
796
- resolvedAt: action.completedAt ?? action.startedAt
797
- } : approval);
798
- }
799
- function makeRuntimeFromRun(run, occurredAt) {
800
- return {
801
- id: run.activeRuntimeId ?? runtimeIdFromRunId(run.id),
802
- workspaceId: run.workspaceId,
803
- runId: run.id,
804
- adapterKind: run.runtimeAdapter,
805
- executionTarget: run.executionTarget ?? "local",
806
- remoteHostId: run.remoteHostId ?? null,
807
- status: run.status === "failed" ? "failed" : run.status === "paused" ? "interrupted" : "starting",
808
- sandboxMode: "danger-full-access",
809
- isolationMode: run.worktreePath ? "worktree" : "env",
810
- workspaceDir: run.worktreePath,
811
- homeDir: null,
812
- tmpDir: null,
813
- cacheDir: null,
814
- logsDir: null,
815
- stateDir: null,
816
- sessionDir: null,
817
- sessionLogPath: null,
818
- pid: null,
819
- startedAt: run.startedAt ?? occurredAt,
820
- updatedAt: occurredAt,
821
- exitedAt: run.completedAt
822
- };
823
- }
824
- function applySyntheticRuntimePreparation(snapshot, event) {
825
- if (!isRecord(event.payload)) {
826
- return { status: "ignored", snapshot };
827
- }
828
- const runId = readString(event.payload, "runId") ?? event.aggregateId;
829
- const workspaceId = readString(event.payload, "workspaceId");
830
- const taskId = readString(event.payload, "taskId") ?? null;
831
- const workspaceDir = readNullableString(event.payload, "workspaceDir");
832
- const homeDir = readNullableString(event.payload, "homeDir");
833
- const tmpDir = readNullableString(event.payload, "tmpDir");
834
- const cacheDir = readNullableString(event.payload, "cacheDir");
835
- const branchName = readString(event.payload, "taskExternalId") ?? snapshot.runs.find((run) => run.id === runId)?.branch ?? "task";
836
- const failed = event.type.endsWith(".failed");
837
- const finished = event.type.endsWith(".finished");
838
- const existingRun = snapshot.runs.find((run) => run.id === runId);
839
- if (!existingRun || !workspaceId) {
840
- return { status: "ignored", snapshot };
841
- }
842
- const runtimeRunId = asRunId(runId);
843
- const nextTaskId = taskId ? asTaskId(taskId) : null;
844
- const nextWorkspaceId = asWorkspaceId(workspaceId);
845
- const nextRun = {
846
- ...existingRun,
847
- taskId: existingRun.taskId ?? nextTaskId,
848
- runKind: existingRun.runKind === "adhoc" && nextTaskId ? "task" : existingRun.runKind,
849
- runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
850
- initialPrompt: existingRun.initialPrompt ?? null,
851
- executionTarget: existingRun.executionTarget ?? "local",
852
- remoteHostId: existingRun.remoteHostId ?? null,
853
- activeRuntimeId: existingRun.activeRuntimeId ?? runtimeIdFromRunId(runtimeRunId),
854
- worktreePath: workspaceDir ?? existingRun.worktreePath,
855
- status: failed ? "failed" : finished ? "preparing" : "preparing",
856
- errorText: failed ? readString(event.payload, "message") ?? existingRun.errorText : existingRun.errorText,
857
- updatedAt: event.createdAt,
858
- completedAt: failed ? event.createdAt : existingRun.completedAt
859
- };
860
- const runtimeBase = snapshot.runtimes.find((runtime) => runtime.runId === runId) ?? makeRuntimeFromRun(nextRun, event.createdAt);
861
- const nextRuntime = {
862
- ...runtimeBase,
863
- workspaceId: nextWorkspaceId,
864
- runId: runtimeRunId,
865
- adapterKind: CANONICAL_RUNTIME_ADAPTER,
866
- executionTarget: existingRun.executionTarget ?? "local",
867
- remoteHostId: existingRun.remoteHostId ?? null,
868
- status: failed ? "failed" : finished ? "prepared" : "starting",
869
- isolationMode: "worktree",
870
- workspaceDir: workspaceDir ?? runtimeBase.workspaceDir,
871
- homeDir: homeDir ?? runtimeBase.homeDir,
872
- tmpDir: tmpDir ?? runtimeBase.tmpDir,
873
- cacheDir: cacheDir ?? runtimeBase.cacheDir,
874
- updatedAt: event.createdAt,
875
- exitedAt: failed ? event.createdAt : runtimeBase.exitedAt
876
- };
877
- let nextSnapshot = applyRun(snapshot, nextRun);
878
- nextSnapshot = applyRuntime(nextSnapshot, nextRuntime);
879
- if (workspaceDir) {
880
- nextSnapshot = applyWorktree(nextSnapshot, {
881
- id: worktreeIdFromRunId(runtimeRunId),
882
- workspaceId: nextWorkspaceId,
883
- runId: runtimeRunId,
884
- taskId: nextRun.taskId,
885
- branchName,
886
- path: workspaceDir,
887
- status: failed ? "failed" : finished ? "prepared" : "preparing",
888
- createdAt: existingRun.createdAt,
889
- cleanedAt: null
890
- });
891
- }
892
- return {
893
- status: "applied",
894
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
895
- };
896
- }
897
- function applySyntheticRuntimePrepared(snapshot, event) {
898
- if (!isRecord(event.payload)) {
899
- return { status: "ignored", snapshot };
900
- }
901
- const runId = readString(event.payload, "runId") ?? event.aggregateId;
902
- const workspaceId = readString(event.payload, "workspaceId");
903
- const worktreePath = readString(event.payload, "worktreePath");
904
- const existingRun = snapshot.runs.find((run) => run.id === runId);
905
- if (!existingRun || !workspaceId || !worktreePath) {
906
- return { status: "ignored", snapshot };
907
- }
908
- const runtimeRunId = asRunId(runId);
909
- const nextWorkspaceId = asWorkspaceId(workspaceId);
910
- let nextSnapshot = applyRuntime(snapshot, {
911
- ...snapshot.runtimes.find((runtime) => runtime.runId === runId) ?? makeRuntimeFromRun(existingRun, event.createdAt),
912
- workspaceId: nextWorkspaceId,
913
- runId: runtimeRunId,
914
- adapterKind: CANONICAL_RUNTIME_ADAPTER,
915
- executionTarget: existingRun.executionTarget ?? "local",
916
- remoteHostId: existingRun.remoteHostId ?? null,
917
- status: "prepared",
918
- isolationMode: "worktree",
919
- workspaceDir: worktreePath,
920
- updatedAt: event.createdAt
921
- });
922
- nextSnapshot = applyWorktree(nextSnapshot, {
923
- id: worktreeIdFromRunId(runtimeRunId),
924
- workspaceId: nextWorkspaceId,
925
- runId: runtimeRunId,
926
- taskId: existingRun.taskId,
927
- branchName: existingRun.branch ?? "task",
928
- path: worktreePath,
929
- status: "prepared",
930
- createdAt: existingRun.createdAt,
931
- cleanedAt: null
932
- });
933
- return {
934
- status: "applied",
935
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
936
- };
937
- }
938
- function applyLegacyProjectEvent(snapshot, event) {
939
- if (!isRecord(event.payload)) {
940
- return { status: "ignored", snapshot };
941
- }
942
- const payload = event.payload;
943
- if (event.type === "legacy.project.created") {
944
- const workspaceId = readString(payload, "projectId");
945
- const title = readString(payload, "title");
946
- const rootPath = readString(payload, "workspaceRoot");
947
- const createdAt = readString(payload, "createdAt");
948
- const updatedAt = readString(payload, "updatedAt");
949
- if (!workspaceId || !title || !rootPath || !createdAt || !updatedAt) {
950
- return { status: "ignored", snapshot };
951
- }
952
- const workspace = {
953
- id: asWorkspaceId(workspaceId),
954
- title,
955
- rootPath,
956
- sourceKind: "native",
957
- defaultRuntimeAdapter: CANONICAL_RUNTIME_ADAPTER,
958
- defaultModel: readNullableString(payload, "defaultModel") ?? null,
959
- createdAt,
960
- updatedAt
961
- };
962
- return {
963
- status: "applied",
964
- snapshot: withSnapshotMetadata(snapshot, event, {
965
- workspaces: upsertById(snapshot.workspaces, workspace)
966
- })
967
- };
968
- }
969
- if (event.type === "legacy.project.meta-updated") {
970
- const workspaceId = readString(payload, "projectId");
971
- if (!workspaceId) {
972
- return { status: "ignored", snapshot };
973
- }
974
- return {
975
- status: "applied",
976
- snapshot: withSnapshotMetadata(snapshot, event, {
977
- workspaces: patchById(snapshot.workspaces, workspaceId, (workspace) => ({
978
- ...workspace,
979
- title: readString(payload, "title") ?? workspace.title,
980
- rootPath: readString(payload, "workspaceRoot") ?? workspace.rootPath,
981
- defaultModel: readNullableString(payload, "defaultModel") ?? workspace.defaultModel,
982
- updatedAt: readString(payload, "updatedAt") ?? event.createdAt
983
- }))
984
- })
985
- };
986
- }
987
- if (event.type === "legacy.project.deleted") {
988
- const workspaceId = readString(payload, "projectId");
989
- if (!workspaceId) {
990
- return { status: "ignored", snapshot };
991
- }
992
- return {
993
- status: "applied",
994
- snapshot: withSnapshotMetadata(snapshot, event, {
995
- workspaces: removeById(snapshot.workspaces, workspaceId),
996
- graphs: snapshot.graphs.filter((graph) => graph.workspaceId !== workspaceId),
997
- tasks: snapshot.tasks.filter((task) => task.workspaceId !== workspaceId),
998
- runs: snapshot.runs.filter((run) => run.workspaceId !== workspaceId),
999
- runtimes: snapshot.runtimes.filter((runtime) => runtime.workspaceId !== workspaceId),
1000
- conversations: snapshot.conversations.filter((conversation) => {
1001
- const run = snapshot.runs.find((candidate) => candidate.id === conversation.runId);
1002
- return run?.workspaceId !== workspaceId;
1003
- }),
1004
- messages: snapshot.messages.filter((message) => {
1005
- const conversation = snapshot.conversations.find((candidate) => candidate.id === message.conversationId);
1006
- const run = snapshot.runs.find((candidate) => candidate.id === conversation?.runId);
1007
- return run?.workspaceId !== workspaceId;
1008
- }),
1009
- actions: snapshot.actions.filter((action) => {
1010
- const run = snapshot.runs.find((candidate) => candidate.id === action.runId);
1011
- return run?.workspaceId !== workspaceId;
1012
- }),
1013
- approvals: snapshot.approvals.filter((approval) => {
1014
- const run = snapshot.runs.find((candidate) => candidate.id === approval.runId);
1015
- return run?.workspaceId !== workspaceId;
1016
- }),
1017
- queue: snapshot.queue.filter((entry) => {
1018
- const task = snapshot.tasks.find((candidate) => candidate.id === entry.taskId);
1019
- return task?.workspaceId !== workspaceId;
1020
- }),
1021
- worktrees: snapshot.worktrees.filter((worktree) => worktree.workspaceId !== workspaceId)
1022
- })
1023
- };
1024
- }
1025
- return { status: "ignored", snapshot };
1026
- }
1027
- function applyLegacyThreadEvent(snapshot, event) {
1028
- if (!isRecord(event.payload)) {
1029
- return { status: "ignored", snapshot };
1030
- }
1031
- const payload = event.payload;
1032
- if (event.type === "legacy.thread.created") {
1033
- const runId = readString(payload, "threadId");
1034
- const workspaceId = readString(payload, "projectId");
1035
- const title = readString(payload, "title");
1036
- const model = readString(payload, "model");
1037
- const createdAt = readString(payload, "createdAt");
1038
- const updatedAt = readString(payload, "updatedAt");
1039
- if (!runId || !workspaceId || !title || !model || !createdAt || !updatedAt) {
1040
- return { status: "ignored", snapshot };
1041
- }
1042
- const run = {
1043
- id: asRunId(runId),
1044
- workspaceId: asWorkspaceId(workspaceId),
1045
- taskId: null,
1046
- title,
1047
- runKind: "adhoc",
1048
- mode: toRunMode(readString(payload, "interactionMode")),
1049
- runtimeMode: "full-access",
1050
- interactionMode: "default",
1051
- status: "created",
1052
- runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
1053
- model,
1054
- initialPrompt: null,
1055
- activeRuntimeId: null,
1056
- latestMessageId: null,
1057
- pendingApprovalCount: 0,
1058
- pendingUserInputCount: 0,
1059
- branch: readNullableString(payload, "branch") ?? null,
1060
- worktreePath: readNullableString(payload, "worktreePath") ?? null,
1061
- errorText: null,
1062
- createdAt,
1063
- updatedAt,
1064
- startedAt: null,
1065
- completedAt: null
1066
- };
1067
- let nextSnapshot = applyRun(snapshot, run);
1068
- if (run.branch && run.worktreePath) {
1069
- nextSnapshot = applyWorktree(nextSnapshot, {
1070
- id: worktreeIdFromRunId(asRunId(runId)),
1071
- workspaceId: asWorkspaceId(workspaceId),
1072
- runId: asRunId(runId),
1073
- taskId: null,
1074
- branchName: run.branch,
1075
- path: run.worktreePath,
1076
- status: "active",
1077
- createdAt,
1078
- cleanedAt: null
1079
- });
1080
- }
1081
- return {
1082
- status: "applied",
1083
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
1084
- };
1085
- }
1086
- if (event.type === "legacy.thread.deleted") {
1087
- const runId = readString(payload, "threadId");
1088
- if (!runId) {
1089
- return { status: "ignored", snapshot };
1090
- }
1091
- return {
1092
- status: "applied",
1093
- snapshot: withSnapshotMetadata(snapshot, event, {
1094
- runs: removeById(snapshot.runs, runId),
1095
- runtimes: snapshot.runtimes.filter((runtime) => runtime.runId !== runId),
1096
- conversations: snapshot.conversations.filter((conversation) => conversation.runId !== runId),
1097
- messages: snapshot.messages.filter((message) => message.conversationId !== runId),
1098
- actions: snapshot.actions.filter((action) => action.runId !== runId),
1099
- approvals: snapshot.approvals.filter((approval) => approval.runId !== runId),
1100
- worktrees: snapshot.worktrees.filter((worktree) => worktree.runId !== runId)
1101
- })
1102
- };
1103
- }
1104
- if (event.type === "legacy.thread.meta-updated") {
1105
- const runId = readString(payload, "threadId");
1106
- if (!runId) {
1107
- return { status: "ignored", snapshot };
1108
- }
1109
- let nextSnapshot = {
1110
- ...snapshot,
1111
- runs: patchById(snapshot.runs, runId, (run2) => ({
1112
- ...run2,
1113
- title: readString(payload, "title") ?? run2.title,
1114
- model: readString(payload, "model") ?? run2.model,
1115
- branch: readNullableString(payload, "branch") === undefined ? run2.branch : readNullableString(payload, "branch") ?? null,
1116
- worktreePath: readNullableString(payload, "worktreePath") === undefined ? run2.worktreePath : readNullableString(payload, "worktreePath") ?? null,
1117
- updatedAt: readString(payload, "updatedAt") ?? event.createdAt
1118
- })),
1119
- conversations: patchById(snapshot.conversations, runId, (conversation) => ({
1120
- ...conversation,
1121
- title: readString(payload, "title") ?? conversation.title,
1122
- updatedAt: readString(payload, "updatedAt") ?? event.createdAt
1123
- }))
1124
- };
1125
- const run = nextSnapshot.runs.find((candidate) => candidate.id === runId);
1126
- if (run?.worktreePath && run.branch) {
1127
- nextSnapshot = applyWorktree(nextSnapshot, {
1128
- id: worktreeIdFromRunId(run.id),
1129
- workspaceId: run.workspaceId,
1130
- runId: run.id,
1131
- taskId: run.taskId,
1132
- branchName: run.branch,
1133
- path: run.worktreePath,
1134
- status: "active",
1135
- createdAt: run.createdAt,
1136
- cleanedAt: null
1137
- });
1138
- }
1139
- if (run && run.worktreePath === null) {
1140
- nextSnapshot = {
1141
- ...nextSnapshot,
1142
- worktrees: nextSnapshot.worktrees.filter((worktree) => worktree.runId !== runId)
1143
- };
1144
- }
1145
- return {
1146
- status: "applied",
1147
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
1148
- };
1149
- }
1150
- if (event.type === "legacy.thread.interaction-mode-set") {
1151
- const runId = readString(payload, "threadId");
1152
- if (!runId) {
1153
- return { status: "ignored", snapshot };
1154
- }
1155
- return {
1156
- status: "applied",
1157
- snapshot: withSnapshotMetadata(snapshot, event, {
1158
- runs: patchById(snapshot.runs, runId, (run) => ({
1159
- ...run,
1160
- mode: toRunMode(readString(payload, "interactionMode")),
1161
- updatedAt: readString(payload, "updatedAt") ?? event.createdAt
1162
- }))
1163
- })
1164
- };
1165
- }
1166
- if (event.type === "legacy.thread.runtime-mode-set") {
1167
- const runId = readString(payload, "threadId");
1168
- if (!runId) {
1169
- return { status: "ignored", snapshot };
1170
- }
1171
- const runtimeMode = readString(payload, "runtimeMode");
1172
- return {
1173
- status: "applied",
1174
- snapshot: withSnapshotMetadata(snapshot, event, {
1175
- runtimes: patchById(snapshot.runtimes, runId, (runtime) => ({
1176
- ...runtime,
1177
- sandboxMode: runtimeMode === "approval-required" ? "workspace-write" : "danger-full-access",
1178
- updatedAt: readString(payload, "updatedAt") ?? event.createdAt
1179
- }))
1180
- })
1181
- };
1182
- }
1183
- if (event.type === "legacy.thread.message-sent") {
1184
- const runId = readString(payload, "threadId");
1185
- const messageId = readString(payload, "messageId");
1186
- const role = readString(payload, "role");
1187
- const text = readString(payload, "text");
1188
- const createdAt = readString(payload, "createdAt");
1189
- const updatedAt = readString(payload, "updatedAt");
1190
- if (!runId || !messageId || !role || text === undefined || !createdAt || !updatedAt) {
1191
- return { status: "ignored", snapshot };
1192
- }
1193
- const attachments = Array.isArray(payload.attachments) ? payload.attachments : [];
1194
- const streaming = payload.streaming === true;
1195
- const message = {
1196
- id: asMessageId(messageId),
1197
- conversationId: conversationIdFromRunId(asRunId(runId)),
1198
- role: role === "assistant" || role === "system" ? role : "user",
1199
- text,
1200
- attachments,
1201
- state: streaming ? "streaming" : "completed",
1202
- createdAt,
1203
- completedAt: streaming ? null : updatedAt
1204
- };
1205
- const existingRun = snapshot.runs.find((run) => run.id === runId);
1206
- if (!existingRun) {
1207
- return { status: "ignored", snapshot };
1208
- }
1209
- return {
1210
- status: "applied",
1211
- snapshot: withSnapshotMetadata(ensureConversation({
1212
- ...snapshot,
1213
- messages: upsertById(snapshot.messages, message),
1214
- runs: patchById(snapshot.runs, runId, (run) => ({
1215
- ...run,
1216
- latestMessageId: asMessageId(messageId),
1217
- updatedAt
1218
- }))
1219
- }, existingRun), event, {})
1220
- };
1221
- }
1222
- if (event.type === "legacy.thread.session-set") {
1223
- const runId = readString(event.payload, "threadId");
1224
- const session = readRecord(event.payload, "session");
1225
- if (!runId || !session) {
1226
- return { status: "ignored", snapshot };
1227
- }
1228
- const existingRun = snapshot.runs.find((run) => run.id === runId);
1229
- if (!existingRun) {
1230
- return { status: "ignored", snapshot };
1231
- }
1232
- const sessionUpdatedAt = readString(session, "updatedAt") ?? event.createdAt;
1233
- const providerName = readNullableString(session, "providerName");
1234
- const nextRun = {
1235
- ...existingRun,
1236
- runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
1237
- initialPrompt: existingRun.initialPrompt ?? null,
1238
- activeRuntimeId: runtimeIdFromRunId(asRunId(runId)),
1239
- status: mapLegacySessionStatusToRunStatus(readString(session, "status")),
1240
- errorText: readNullableString(session, "lastError") ?? existingRun.errorText,
1241
- updatedAt: sessionUpdatedAt,
1242
- completedAt: readString(session, "status") === "ready" || readString(session, "status") === "stopped" || readString(session, "status") === "error" ? sessionUpdatedAt : existingRun.completedAt
1243
- };
1244
- let nextSnapshot = applyRun(snapshot, nextRun);
1245
- nextSnapshot = applyRuntime(nextSnapshot, {
1246
- ...snapshot.runtimes.find((runtime) => runtime.runId === runId) ?? makeRuntimeFromRun(nextRun, sessionUpdatedAt),
1247
- workspaceId: existingRun.workspaceId,
1248
- runId: asRunId(runId),
1249
- adapterKind: CANONICAL_RUNTIME_ADAPTER,
1250
- status: mapLegacySessionStatusToRuntimeStatus(readString(session, "status")),
1251
- workspaceDir: existingRun.worktreePath,
1252
- isolationMode: existingRun.worktreePath ? "worktree" : "env",
1253
- updatedAt: sessionUpdatedAt,
1254
- exitedAt: readString(session, "status") === "ready" || readString(session, "status") === "stopped" || readString(session, "status") === "error" ? sessionUpdatedAt : null
1255
- });
1256
- return {
1257
- status: "applied",
1258
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
1259
- };
1260
- }
1261
- if (event.type === "legacy.thread.activity-appended") {
1262
- const runId = readString(event.payload, "threadId");
1263
- const activity = readRecord(event.payload, "activity");
1264
- if (!runId || !activity) {
1265
- return { status: "ignored", snapshot };
1266
- }
1267
- const actionId = readString(activity, "id");
1268
- const actionType = readString(activity, "kind");
1269
- const title = readString(activity, "summary");
1270
- const startedAt = readString(activity, "createdAt");
1271
- if (!actionId || !actionType || !title || !startedAt) {
1272
- return { status: "ignored", snapshot };
1273
- }
1274
- const payload2 = readRecord(activity, "payload") ?? {};
1275
- const detail = readString(payload2, "detail") ?? null;
1276
- const action = {
1277
- id: asActionId(actionId),
1278
- runId: asRunId(runId),
1279
- messageId: null,
1280
- actionType,
1281
- title,
1282
- detail,
1283
- state: "completed",
1284
- payload: payload2,
1285
- startedAt,
1286
- completedAt: startedAt
1287
- };
1288
- let nextSnapshot = {
1289
- ...snapshot,
1290
- actions: upsertById(snapshot.actions, action),
1291
- approvals: applyApprovalActivity(snapshot.approvals, asRunId(runId), action),
1292
- runs: patchById(snapshot.runs, runId, (run) => ({
1293
- ...run,
1294
- updatedAt: maxIsoDate(run.updatedAt, startedAt),
1295
- status: actionType === "engine.runtime.ready" ? "running" : actionType === "engine.runtime.failed" ? "failed" : run.status,
1296
- errorText: actionType === "engine.runtime.failed" && detail ? detail : run.errorText,
1297
- completedAt: actionType === "engine.runtime.failed" ? startedAt : run.completedAt
1298
- }))
1299
- };
1300
- nextSnapshot = reconcileRunCounts(nextSnapshot, asRunId(runId));
1301
- return {
1302
- status: "applied",
1303
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
1304
- };
1305
- }
1306
- if (event.type === "legacy.thread.reverted") {
1307
- return { status: "requires-resync", snapshot };
1308
- }
1309
- return { status: "ignored", snapshot };
1310
- }
1311
- function applyEngineEvent(snapshot, event) {
1312
- if (event.sequence <= snapshot.snapshotSequence) {
1313
- return { status: "ignored", snapshot };
1314
- }
1315
- if (event.sequence > snapshot.snapshotSequence + 1) {
1316
- return { status: "requires-resync", snapshot };
1317
- }
1318
- const base = {
1319
- ...snapshot,
1320
- snapshotSequence: event.sequence,
1321
- updatedAt: event.createdAt
1322
- };
1323
- const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
1324
- switch (event.type) {
1325
- case "WorkspaceRegistered":
1326
- case "RigWorkspaceImported": {
1327
- const workspace = {
1328
- id: payload.workspaceId,
1329
- title: payload.title,
1330
- rootPath: payload.rootPath,
1331
- sourceKind: payload.sourceKind,
1332
- defaultModel: payload.defaultModel ?? null,
1333
- topology: undefined,
1334
- remoteFleet: undefined,
1335
- serviceFabric: undefined,
1336
- createdAt: payload.createdAt,
1337
- updatedAt: payload.createdAt
1338
- };
1339
- return {
1340
- status: "applied",
1341
- snapshot: {
1342
- ...base,
1343
- workspaces: upsertById(base.workspaces, workspace)
1344
- }
1345
- };
1346
- }
1347
- case "RigStateHydrated": {
1348
- const workspaceRunIds = new Set([
1349
- ...base.runs.filter((run) => run.workspaceId === payload.workspaceId).map((run) => run.id),
1350
- ...(payload.runs ?? []).map((run) => run.id)
1351
- ]);
1352
- const withoutWorkspaceRunData = (items) => items.filter((item) => !workspaceRunIds.has(item.runId));
1353
- const workspaces = payload.rootPath ? patchById(base.workspaces, payload.workspaceId, (workspace) => ({
1354
- ...workspace,
1355
- rootPath: payload.rootPath,
1356
- updatedAt: payload.createdAt
1357
- })) : base.workspaces;
1358
- return {
1359
- status: "applied",
1360
- snapshot: {
1361
- ...base,
1362
- workspaces,
1363
- graphs: upsertById(base.graphs, payload.graph),
1364
- tasks: replaceWorkspaceSlice(base.tasks, payload.workspaceId, payload.tasks ?? []),
1365
- runs: replaceWorkspaceSlice(base.runs, payload.workspaceId, payload.runs ?? []),
1366
- runtimes: replaceWorkspaceSlice(base.runtimes, payload.workspaceId, payload.runtimes ?? []),
1367
- actions: mergeById(withoutWorkspaceRunData(base.actions), payload.actions ?? []),
1368
- logs: mergeById(withoutWorkspaceRunData(base.logs), payload.logs ?? []),
1369
- userInputs: (base.userInputs ?? []).filter((request) => !workspaceRunIds.has(request.runId)),
1370
- validations: mergeById(withoutWorkspaceRunData(base.validations), payload.validations ?? []),
1371
- reviews: mergeById(withoutWorkspaceRunData(base.reviews), payload.reviews ?? []),
1372
- artifacts: mergeById(withoutWorkspaceRunData(base.artifacts), payload.artifacts ?? []),
1373
- policyDecisions: mergeById(withoutWorkspaceRunData(base.policyDecisions), payload.policyDecisions ?? []),
1374
- queue: [...payload.queue ?? []]
1375
- }
1376
- };
1377
- }
1378
- case "WorkspaceTopologyCompiled": {
1379
- return {
1380
- status: "applied",
1381
- snapshot: {
1382
- ...base,
1383
- workspaces: patchById(base.workspaces, payload.workspaceId, (workspace) => ({
1384
- ...workspace,
1385
- rootPath: payload.rootPath ?? workspace.rootPath,
1386
- topology: payload.topology,
1387
- updatedAt: payload.createdAt
1388
- }))
1389
- }
1390
- };
1391
- }
1392
- case "WorkspaceRemoteFleetSynced": {
1393
- return {
1394
- status: "applied",
1395
- snapshot: {
1396
- ...base,
1397
- workspaces: patchById(base.workspaces, payload.workspaceId, (workspace) => ({
1398
- ...workspace,
1399
- rootPath: payload.rootPath ?? workspace.rootPath,
1400
- remoteFleet: payload.remoteFleet,
1401
- updatedAt: payload.createdAt
1402
- }))
1403
- }
1404
- };
1405
- }
1406
- case "WorkspaceServiceFabricSynced": {
1407
- return {
1408
- status: "applied",
1409
- snapshot: {
1410
- ...base,
1411
- workspaces: patchById(base.workspaces, payload.workspaceId, (workspace) => ({
1412
- ...workspace,
1413
- rootPath: payload.rootPath ?? workspace.rootPath,
1414
- serviceFabric: payload.serviceFabric,
1415
- updatedAt: payload.createdAt
1416
- }))
1417
- }
1418
- };
1419
- }
1420
- case "WorkspaceRemoteHostRegistered": {
1421
- return {
1422
- status: "applied",
1423
- snapshot: {
1424
- ...base,
1425
- workspaces: patchById(base.workspaces, payload.workspaceId, (workspace) => {
1426
- const withoutExisting = (workspace.remoteFleet?.hosts ?? []).filter((host) => host.id !== payload.host.id);
1427
- return {
1428
- ...workspace,
1429
- remoteFleet: buildRemoteFleetSummary({
1430
- fleet: workspace.remoteFleet,
1431
- hosts: [...withoutExisting, payload.host],
1432
- updatedAt: payload.createdAt
1433
- }),
1434
- updatedAt: payload.createdAt
1435
- };
1436
- })
1437
- }
1438
- };
1439
- }
1440
- case "WorkspaceRemoteHostStatusUpdated": {
1441
- return {
1442
- status: "applied",
1443
- snapshot: {
1444
- ...base,
1445
- workspaces: patchById(base.workspaces, payload.workspaceId, (workspace) => {
1446
- const fleet = workspace.remoteFleet;
1447
- if (!fleet) {
1448
- return {
1449
- ...workspace,
1450
- updatedAt: payload.createdAt
1451
- };
1452
- }
1453
- const hosts = fleet.hosts.map((host) => host.id !== payload.hostId ? host : {
1454
- ...host,
1455
- status: payload.status,
1456
- currentLeaseCount: typeof payload.currentLeaseCount === "number" ? payload.currentLeaseCount : host.currentLeaseCount,
1457
- lastHeartbeatAt: payload.lastHeartbeatAt ?? host.lastHeartbeatAt
1458
- });
1459
- return {
1460
- ...workspace,
1461
- remoteFleet: buildRemoteFleetSummary({
1462
- fleet,
1463
- hosts,
1464
- updatedAt: payload.createdAt
1465
- }),
1466
- updatedAt: payload.createdAt
1467
- };
1468
- })
1469
- }
1470
- };
1471
- }
1472
- case "RunCreated": {
1473
- const run = {
1474
- id: payload.runId,
1475
- workspaceId: payload.workspaceId,
1476
- taskId: payload.taskId ?? null,
1477
- title: payload.title,
1478
- runKind: payload.runKind,
1479
- mode: payload.mode,
1480
- runtimeMode: payload.runtimeMode,
1481
- interactionMode: payload.interactionMode,
1482
- status: "created",
1483
- runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
1484
- model: payload.model ?? null,
1485
- initialPrompt: payload.initialPrompt ?? null,
1486
- executionTarget: payload.executionTarget ?? (payload.remoteHostId ? "remote" : "local"),
1487
- remoteHostId: payload.remoteHostId ?? null,
1488
- remoteLeaseId: null,
1489
- remoteLeaseClaimedAt: null,
1490
- activeRuntimeId: null,
1491
- latestMessageId: null,
1492
- pendingApprovalCount: 0,
1493
- pendingUserInputCount: 0,
1494
- branch: null,
1495
- worktreePath: null,
1496
- errorText: null,
1497
- createdAt: payload.createdAt,
1498
- updatedAt: payload.createdAt,
1499
- startedAt: null,
1500
- completedAt: null
1501
- };
1502
- return {
1503
- status: "applied",
1504
- snapshot: { ...base, runs: upsertById(base.runs, run) }
1505
- };
1506
- }
1507
- case "RunInterrupted":
1508
- case "RunCancelled":
1509
- return {
1510
- status: "applied",
1511
- snapshot: {
1512
- ...base,
1513
- runs: patchById(base.runs, payload.runId, (run) => ({
1514
- ...run,
1515
- status: "stopped",
1516
- updatedAt: payload.createdAt,
1517
- completedAt: payload.createdAt
1518
- }))
1519
- }
1520
- };
1521
- case "RunCompleted":
1522
- return {
1523
- status: "applied",
1524
- snapshot: {
1525
- ...base,
1526
- runs: patchById(base.runs, payload.runId, (run) => ({
1527
- ...run,
1528
- status: "completed",
1529
- updatedAt: payload.createdAt,
1530
- completedAt: payload.createdAt
1531
- }))
1532
- }
1533
- };
1534
- case "RunStatusSet":
1535
- return {
1536
- status: "applied",
1537
- snapshot: {
1538
- ...base,
1539
- runs: patchById(base.runs, payload.runId, (run) => ({
1540
- ...run,
1541
- status: payload.status,
1542
- updatedAt: payload.createdAt,
1543
- completedAt: null
1544
- }))
1545
- }
1546
- };
1547
- case "RunFailed":
1548
- return {
1549
- status: "applied",
1550
- snapshot: {
1551
- ...base,
1552
- runs: patchById(base.runs, payload.runId, (run) => ({
1553
- ...run,
1554
- status: "failed",
1555
- errorText: payload.errorText ?? null,
1556
- updatedAt: payload.createdAt,
1557
- completedAt: payload.createdAt
1558
- }))
1559
- }
1560
- };
1561
- case "RuntimeModeSet":
1562
- return {
1563
- status: "applied",
1564
- snapshot: {
1565
- ...base,
1566
- runs: patchById(base.runs, payload.runId, (run) => ({
1567
- ...run,
1568
- runtimeMode: payload.runtimeMode,
1569
- updatedAt: payload.createdAt
1570
- }))
1571
- }
1572
- };
1573
- case "InteractionModeSet":
1574
- return {
1575
- status: "applied",
1576
- snapshot: {
1577
- ...base,
1578
- runs: patchById(base.runs, payload.runId, (run) => ({
1579
- ...run,
1580
- interactionMode: payload.interactionMode,
1581
- updatedAt: payload.createdAt
1582
- }))
1583
- }
1584
- };
1585
- case "ConversationAttached": {
1586
- const conversation = {
1587
- id: payload.conversationId,
1588
- runId: payload.runId,
1589
- title: payload.title,
1590
- createdAt: payload.createdAt,
1591
- updatedAt: payload.createdAt
1592
- };
1593
- return {
1594
- status: "applied",
1595
- snapshot: {
1596
- ...base,
1597
- conversations: upsertById(base.conversations, conversation)
1598
- }
1599
- };
1600
- }
1601
- case "MessageAppended": {
1602
- const conversation = base.conversations.find((item) => item.runId === payload.runId);
1603
- if (!conversation) {
1604
- return { status: "ignored", snapshot: base };
1605
- }
1606
- const conversationId = conversation.id;
1607
- const message = {
1608
- id: payload.messageId,
1609
- conversationId,
1610
- role: payload.role,
1611
- text: payload.text,
1612
- attachments: payload.attachments ?? [],
1613
- state: payload.state ?? "completed",
1614
- createdAt: payload.createdAt,
1615
- completedAt: payload.completedAt ?? payload.createdAt
1616
- };
1617
- return {
1618
- status: "applied",
1619
- snapshot: {
1620
- ...base,
1621
- messages: upsertById(base.messages, message),
1622
- runs: patchById(base.runs, payload.runId, (run) => ({
1623
- ...run,
1624
- latestMessageId: payload.messageId,
1625
- updatedAt: payload.createdAt
1626
- }))
1627
- }
1628
- };
1629
- }
1630
- case "ActionStarted": {
1631
- const action = {
1632
- id: payload.actionId,
1633
- runId: payload.runId,
1634
- messageId: payload.messageId ?? null,
1635
- actionType: payload.actionType,
1636
- title: payload.title,
1637
- detail: payload.detail ?? null,
1638
- state: payload.state ?? "running",
1639
- payload: payload.payload ?? null,
1640
- startedAt: payload.startedAt,
1641
- completedAt: null
1642
- };
1643
- return {
1644
- status: "applied",
1645
- snapshot: {
1646
- ...base,
1647
- actions: upsertById(base.actions, action)
1648
- }
1649
- };
1650
- }
1651
- case "ActionCompleted":
1652
- return {
1653
- status: "applied",
1654
- snapshot: {
1655
- ...base,
1656
- actions: patchById(base.actions, payload.actionId, (action) => ({
1657
- ...action,
1658
- state: payload.state,
1659
- detail: payload.detail ?? null,
1660
- payload: payload.payload,
1661
- completedAt: payload.completedAt
1662
- }))
1663
- }
1664
- };
1665
- case "RunLogAppended": {
1666
- const log = {
1667
- id: payload.logId,
1668
- runId: payload.runId,
1669
- title: payload.title,
1670
- detail: payload.detail ?? null,
1671
- tone: payload.tone,
1672
- status: payload.status ?? null,
1673
- payload: payload.payload ?? null,
1674
- createdAt: payload.createdAt
1675
- };
1676
- return {
1677
- status: "applied",
1678
- snapshot: {
1679
- ...base,
1680
- logs: upsertById(base.logs, log),
1681
- runs: patchById(base.runs, payload.runId, (run) => ({
1682
- ...run,
1683
- updatedAt: payload.createdAt
1684
- }))
1685
- }
1686
- };
1687
- }
1688
- case "ValidationRecorded":
1689
- return {
1690
- status: "applied",
1691
- snapshot: {
1692
- ...base,
1693
- validations: upsertById(base.validations, {
1694
- id: payload.id,
1695
- runId: payload.runId,
1696
- taskId: payload.taskId ?? null,
1697
- validatorKey: payload.validatorKey,
1698
- status: payload.status,
1699
- output: payload.output,
1700
- startedAt: payload.startedAt,
1701
- completedAt: payload.completedAt ?? null
1702
- })
1703
- }
1704
- };
1705
- case "ReviewRecorded":
1706
- return {
1707
- status: "applied",
1708
- snapshot: {
1709
- ...base,
1710
- reviews: upsertById(base.reviews, {
1711
- id: payload.id,
1712
- runId: payload.runId,
1713
- taskId: payload.taskId ?? null,
1714
- provider: payload.provider,
1715
- mode: payload.mode,
1716
- status: payload.status,
1717
- summary: payload.summary ?? null,
1718
- output: payload.output,
1719
- createdAt: payload.createdAt,
1720
- completedAt: payload.completedAt ?? null
1721
- })
1722
- }
1723
- };
1724
- case "ArtifactRegistered":
1725
- return {
1726
- status: "applied",
1727
- snapshot: {
1728
- ...base,
1729
- artifacts: upsertById(base.artifacts, {
1730
- id: payload.id,
1731
- runId: payload.runId,
1732
- taskId: payload.taskId ?? null,
1733
- kind: payload.kind,
1734
- label: payload.label,
1735
- path: payload.path ?? null,
1736
- url: payload.url ?? null,
1737
- metadata: payload.metadata ?? {},
1738
- createdAt: payload.createdAt
1739
- })
1740
- }
1741
- };
1742
- case "ApprovalRequested": {
1743
- const run = base.runs.find((item) => item.id === payload.runId);
1744
- const approval = {
1745
- id: payload.requestId,
1746
- runId: payload.runId,
1747
- actionId: payload.actionId ?? null,
1748
- requestKind: payload.requestKind,
1749
- status: "pending",
1750
- payload: payload.payload ?? null,
1751
- createdAt: payload.createdAt,
1752
- resolvedAt: null
1753
- };
1754
- return {
1755
- status: "applied",
1756
- snapshot: {
1757
- ...base,
1758
- approvals: upsertById(base.approvals, approval),
1759
- runs: patchById(base.runs, payload.runId, (item) => ({
1760
- ...item,
1761
- pendingApprovalCount: (run?.pendingApprovalCount ?? 0) + 1,
1762
- status: "waiting-approval",
1763
- updatedAt: payload.createdAt
1764
- }))
1765
- }
1766
- };
1767
- }
1768
- case "ApprovalResolved": {
1769
- const approval = base.approvals.find((item) => item.id === payload.requestId && item.runId === payload.runId);
1770
- if (!approval) {
1771
- return { status: "applied", snapshot: base };
1772
- }
1773
- const run = base.runs.find((item) => item.id === payload.runId);
1774
- const pendingApprovalCount = run ? Math.max(0, Number(run.pendingApprovalCount) - 1) : 0;
1775
- const nextStatus = run?.status === "waiting-approval" && pendingApprovalCount === 0 ? "running" : run?.status;
1776
- return {
1777
- status: "applied",
1778
- snapshot: {
1779
- ...base,
1780
- approvals: patchById(base.approvals, payload.requestId, (item) => ({
1781
- ...item,
1782
- status: "resolved",
1783
- resolvedAt: payload.createdAt
1784
- })),
1785
- runs: patchById(base.runs, payload.runId, (item) => ({
1786
- ...item,
1787
- pendingApprovalCount,
1788
- ...nextStatus ? { status: nextStatus } : {},
1789
- updatedAt: payload.createdAt
1790
- }))
1791
- }
1792
- };
1793
- }
1794
- case "UserInputRequested": {
1795
- const run = base.runs.find((item) => item.id === payload.runId);
1796
- return {
1797
- status: "applied",
1798
- snapshot: {
1799
- ...base,
1800
- userInputs: upsertById(base.userInputs ?? [], {
1801
- id: payload.requestId,
1802
- runId: payload.runId,
1803
- status: "pending",
1804
- payload: payload.payload,
1805
- createdAt: payload.createdAt,
1806
- resolvedAt: null
1807
- }),
1808
- runs: patchById(base.runs, payload.runId, (item) => ({
1809
- ...item,
1810
- pendingUserInputCount: (run?.pendingUserInputCount ?? 0) + 1,
1811
- status: "waiting-user-input",
1812
- updatedAt: payload.createdAt
1813
- }))
1814
- }
1815
- };
1816
- }
1817
- case "UserInputResolved": {
1818
- const run = base.runs.find((item) => item.id === payload.runId);
1819
- const pendingUserInputCount = run ? Math.max(0, Number(run.pendingUserInputCount) - 1) : 0;
1820
- const nextStatus = run?.status === "waiting-user-input" && pendingUserInputCount === 0 ? "running" : run?.status;
1821
- return {
1822
- status: "applied",
1823
- snapshot: {
1824
- ...base,
1825
- userInputs: patchById(base.userInputs ?? [], payload.requestId, (item) => ({
1826
- ...item,
1827
- status: "resolved",
1828
- resolvedAt: payload.createdAt
1829
- })),
1830
- runs: patchById(base.runs, payload.runId, (item) => ({
1831
- ...item,
1832
- pendingUserInputCount,
1833
- ...nextStatus ? { status: nextStatus } : {},
1834
- updatedAt: payload.createdAt
1835
- }))
1836
- }
1837
- };
1838
- }
1839
- case "RuntimePrepared": {
1840
- const runtime = {
1841
- id: payload.runtimeId,
1842
- workspaceId: payload.workspaceId,
1843
- runId: payload.runId,
1844
- adapterKind: payload.adapter,
1845
- executionTarget: payload.executionTarget ?? "local",
1846
- remoteHostId: payload.remoteHostId ?? null,
1847
- status: "prepared",
1848
- sandboxMode: "read-only",
1849
- isolationMode: "none",
1850
- workspaceDir: null,
1851
- homeDir: null,
1852
- tmpDir: null,
1853
- cacheDir: null,
1854
- logsDir: null,
1855
- stateDir: null,
1856
- sessionDir: null,
1857
- sessionLogPath: null,
1858
- pid: null,
1859
- startedAt: null,
1860
- updatedAt: payload.createdAt,
1861
- exitedAt: null
1862
- };
1863
- return {
1864
- status: "applied",
1865
- snapshot: {
1866
- ...base,
1867
- runtimes: upsertById(base.runtimes, runtime),
1868
- runs: patchById(base.runs, payload.runId, (run) => ({
1869
- ...run,
1870
- status: "preparing",
1871
- executionTarget: payload.executionTarget ?? run.executionTarget ?? "local",
1872
- remoteHostId: payload.remoteHostId ?? run.remoteHostId ?? null,
1873
- updatedAt: payload.createdAt
1874
- }))
1875
- }
1876
- };
1877
- }
1878
- case "RunRemoteLeaseClaimed":
1879
- return {
1880
- status: "applied",
1881
- snapshot: {
1882
- ...base,
1883
- runs: patchById(base.runs, payload.runId, (run) => ({
1884
- ...run,
1885
- remoteHostId: payload.hostId,
1886
- remoteLeaseId: payload.leaseId,
1887
- remoteLeaseClaimedAt: payload.createdAt,
1888
- updatedAt: payload.createdAt
1889
- }))
1890
- }
1891
- };
1892
- case "RunRemoteLeaseReleased":
1893
- return {
1894
- status: "applied",
1895
- snapshot: {
1896
- ...base,
1897
- runs: patchById(base.runs, payload.runId, (run) => ({
1898
- ...run,
1899
- remoteLeaseId: null,
1900
- remoteLeaseClaimedAt: null,
1901
- updatedAt: payload.createdAt
1902
- }))
1903
- }
1904
- };
1905
- case "RuntimeAttached": {
1906
- const tasks = payload.taskId == null ? base.tasks : patchById(base.tasks, payload.taskId, (task) => ({
1907
- ...task,
1908
- status: "running",
1909
- updatedAt: payload.createdAt
1910
- }));
1911
- return {
1912
- status: "applied",
1913
- snapshot: {
1914
- ...base,
1915
- runs: patchById(base.runs, payload.runId, (run) => ({
1916
- ...run,
1917
- activeRuntimeId: payload.runtimeId,
1918
- status: "running",
1919
- startedAt: payload.createdAt,
1920
- updatedAt: payload.createdAt
1921
- })),
1922
- runtimes: patchById(base.runtimes, payload.runtimeId, (runtime) => ({
1923
- ...runtime,
1924
- status: "running",
1925
- startedAt: payload.createdAt,
1926
- updatedAt: payload.createdAt
1927
- })),
1928
- tasks
1929
- }
1930
- };
1931
- }
1932
- case "RuntimeDetached":
1933
- return {
1934
- status: "applied",
1935
- snapshot: {
1936
- ...base,
1937
- runs: patchById(base.runs, payload.runId, (run) => ({
1938
- ...run,
1939
- activeRuntimeId: null,
1940
- updatedAt: payload.createdAt
1941
- })),
1942
- runtimes: patchById(base.runtimes, payload.runtimeId, (runtime) => ({
1943
- ...runtime,
1944
- status: "exited",
1945
- exitedAt: payload.createdAt,
1946
- updatedAt: payload.createdAt
1947
- }))
1948
- }
1949
- };
1950
- case "RuntimeMetadataUpdated":
1951
- return {
1952
- status: "applied",
1953
- snapshot: {
1954
- ...base,
1955
- runs: patchById(base.runs, payload.runId, (run) => ({
1956
- ...run,
1957
- ...payload.branch !== undefined ? { branch: payload.branch } : {},
1958
- ...payload.worktreePath !== undefined ? { worktreePath: payload.worktreePath } : {},
1959
- updatedAt: payload.createdAt
1960
- })),
1961
- runtimes: patchById(base.runtimes, payload.runtimeId, (runtime) => ({
1962
- ...runtime,
1963
- ...payload.sandboxMode !== undefined ? { sandboxMode: payload.sandboxMode } : {},
1964
- ...payload.isolationMode !== undefined ? { isolationMode: payload.isolationMode } : {},
1965
- ...payload.workspaceDir !== undefined ? { workspaceDir: payload.workspaceDir } : {},
1966
- ...payload.homeDir !== undefined ? { homeDir: payload.homeDir } : {},
1967
- ...payload.tmpDir !== undefined ? { tmpDir: payload.tmpDir } : {},
1968
- ...payload.cacheDir !== undefined ? { cacheDir: payload.cacheDir } : {},
1969
- ...payload.logsDir !== undefined ? { logsDir: payload.logsDir } : {},
1970
- ...payload.stateDir !== undefined ? { stateDir: payload.stateDir } : {},
1971
- ...payload.sessionDir !== undefined ? { sessionDir: payload.sessionDir } : {},
1972
- ...payload.sessionLogPath !== undefined ? { sessionLogPath: payload.sessionLogPath } : {},
1973
- ...payload.pid !== undefined ? { pid: payload.pid } : {},
1974
- updatedAt: payload.createdAt
1975
- }))
1976
- }
1977
- };
1978
- case "TaskStatusChanged": {
1979
- const queue = payload.status === "queued" ? base.queue : withQueuePositions(base.queue.filter((item) => item.taskId !== payload.taskId));
1980
- return {
1981
- status: "applied",
1982
- snapshot: {
1983
- ...base,
1984
- tasks: patchById(base.tasks, payload.taskId, (task) => ({
1985
- ...task,
1986
- status: payload.status,
1987
- updatedAt: payload.createdAt
1988
- })),
1989
- queue
1990
- }
1991
- };
1992
- }
1993
- case "TaskEnqueued": {
1994
- const entry = {
1995
- taskId: payload.taskId,
1996
- score: payload.score,
1997
- unblockCount: 0,
1998
- position: base.queue.length
1999
- };
2000
- const positionedQueue = withQueuePositions([
2001
- ...base.queue.filter((item) => item.taskId !== payload.taskId),
2002
- entry
2003
- ]);
2004
- return {
2005
- status: "applied",
2006
- snapshot: {
2007
- ...base,
2008
- tasks: patchById(base.tasks, payload.taskId, (task) => ({
2009
- ...task,
2010
- status: "queued",
2011
- updatedAt: payload.createdAt
2012
- })),
2013
- queue: positionedQueue
2014
- }
2015
- };
2016
- }
2017
- case "RemoteEndpointRegistered": {
2018
- const endpoint = payload.endpoint;
2019
- if (!endpoint || !endpoint.id) {
2020
- return { status: "ignored", snapshot: base };
2021
- }
2022
- return {
2023
- status: "applied",
2024
- snapshot: {
2025
- ...base,
2026
- remoteEndpoints: upsertById(base.remoteEndpoints, endpoint)
2027
- }
2028
- };
2029
- }
2030
- case "RemoteEndpointRemoved": {
2031
- const endpointId = payload.endpointId;
2032
- if (!endpointId) {
2033
- return { status: "ignored", snapshot: base };
2034
- }
2035
- return {
2036
- status: "applied",
2037
- snapshot: {
2038
- ...base,
2039
- remoteEndpoints: removeById(base.remoteEndpoints, endpointId),
2040
- remoteConnections: base.remoteConnections.filter((conn) => conn.endpointId !== endpointId),
2041
- remoteOrchestrations: base.remoteOrchestrations.filter((orch) => orch.endpointId !== endpointId)
2042
- }
2043
- };
2044
- }
2045
- case "RemoteEndpointUpdated": {
2046
- const endpointId = payload.endpointId;
2047
- const updates = payload.updates;
2048
- if (!endpointId || !updates) {
2049
- return { status: "ignored", snapshot: base };
2050
- }
2051
- return {
2052
- status: "applied",
2053
- snapshot: {
2054
- ...base,
2055
- remoteEndpoints: patchById(base.remoteEndpoints, endpointId, (endpoint) => ({
2056
- ...endpoint,
2057
- ...updates
2058
- }))
2059
- }
2060
- };
2061
- }
2062
- case "RemoteConnectionChanged": {
2063
- const endpointId = payload.endpointId;
2064
- const status = payload.status;
2065
- if (!endpointId || !status) {
2066
- return { status: "ignored", snapshot: base };
2067
- }
2068
- const existing = base.remoteConnections.find((conn) => conn.endpointId === endpointId);
2069
- const connection = {
2070
- ...existing ?? {
2071
- endpointId,
2072
- status,
2073
- error: null,
2074
- connectedAt: null,
2075
- tokenExpiresAt: null,
2076
- latencyMs: null,
2077
- subscribedEvents: []
2078
- },
2079
- endpointId,
2080
- status,
2081
- ...payload.error !== undefined ? { error: payload.error ?? null } : {},
2082
- ...status === "connected" ? { connectedAt: event.createdAt } : {}
2083
- };
2084
- return {
2085
- status: "applied",
2086
- snapshot: {
2087
- ...base,
2088
- remoteConnections: upsertByKey(base.remoteConnections, connection, "endpointId")
2089
- }
2090
- };
2091
- }
2092
- case "RemoteWorkspaceHydrated": {
2093
- const endpointId = payload.endpointId;
2094
- const workspace = payload.workspace;
2095
- const tasks = payload.tasks;
2096
- const graph = payload.graph;
2097
- if (!endpointId || !workspace) {
2098
- return { status: "ignored", snapshot: base };
2099
- }
2100
- return {
2101
- status: "applied",
2102
- snapshot: {
2103
- ...base,
2104
- workspaces: upsertById(base.workspaces, workspace),
2105
- tasks: Array.isArray(tasks) ? replaceWorkspaceSlice(base.tasks, workspace.id, tasks) : base.tasks,
2106
- graphs: graph ? upsertById(base.graphs, graph) : base.graphs
2107
- }
2108
- };
2109
- }
2110
- case "RemoteStateRefreshed": {
2111
- const endpointId = payload.endpointId;
2112
- const tasks = payload.tasks;
2113
- if (!endpointId) {
2114
- return { status: "ignored", snapshot: base };
2115
- }
2116
- const remoteWorkspace = base.workspaces.find((ws) => ws.id === `remote-workspace:${endpointId}`);
2117
- return {
2118
- status: "applied",
2119
- snapshot: {
2120
- ...base,
2121
- tasks: Array.isArray(tasks) && remoteWorkspace ? replaceWorkspaceSlice(base.tasks, remoteWorkspace.id, tasks) : base.tasks
2122
- }
2123
- };
2124
- }
2125
- case "RemoteEventReceived":
2126
- return { status: "applied", snapshot: base };
2127
- case "RemoteOrchestrationStarted": {
2128
- const orchestration = payload.orchestration;
2129
- if (!orchestration || !orchestration.orchestrationId) {
2130
- return { status: "ignored", snapshot: base };
2131
- }
2132
- return {
2133
- status: "applied",
2134
- snapshot: {
2135
- ...base,
2136
- remoteOrchestrations: upsertByKey(base.remoteOrchestrations, orchestration, "orchestrationId")
2137
- }
2138
- };
2139
- }
2140
- case "RemoteOrchestrationUpdated": {
2141
- const orchestrationId = payload.orchestrationId;
2142
- const state = payload.state;
2143
- if (!orchestrationId) {
2144
- return { status: "ignored", snapshot: base };
2145
- }
2146
- return {
2147
- status: "applied",
2148
- snapshot: {
2149
- ...base,
2150
- remoteOrchestrations: patchByKey(base.remoteOrchestrations, orchestrationId, "orchestrationId", (orch) => ({
2151
- ...orch,
2152
- ...isRecord(state) ? state : {}
2153
- }))
2154
- }
2155
- };
2156
- }
2157
- }
2158
- if (event.type === "workspace.imported") {
2159
- return { status: "requires-resync", snapshot };
2160
- }
2161
- if (event.type === "task.run-linked") {
2162
- const payload2 = isRecord(event.payload) ? event.payload : {};
2163
- const runId = readString(payload2, "runId");
2164
- const taskId = readString(payload2, "taskId");
2165
- const workspaceId = readString(payload2, "workspaceId");
2166
- if (!runId || !taskId || !workspaceId) {
2167
- return { status: "ignored", snapshot };
2168
- }
2169
- const baseRun = snapshot.runs.find((run) => run.id === runId) ?? {
2170
- id: asRunId(runId),
2171
- workspaceId: asWorkspaceId(workspaceId),
2172
- taskId: asTaskId(taskId),
2173
- title: "Task run",
2174
- runKind: "task",
2175
- mode: "interactive",
2176
- runtimeMode: "full-access",
2177
- interactionMode: "default",
2178
- status: "created",
2179
- runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
2180
- model: null,
2181
- initialPrompt: null,
2182
- activeRuntimeId: runtimeIdFromRunId(asRunId(runId)),
2183
- latestMessageId: null,
2184
- pendingApprovalCount: 0,
2185
- pendingUserInputCount: 0,
2186
- branch: null,
2187
- worktreePath: null,
2188
- errorText: null,
2189
- createdAt: event.createdAt,
2190
- updatedAt: event.createdAt,
2191
- startedAt: event.createdAt,
2192
- completedAt: null
2193
- };
2194
- const nextSnapshot = applyRun(snapshot, {
2195
- ...baseRun,
2196
- workspaceId: asWorkspaceId(workspaceId),
2197
- taskId: asTaskId(taskId),
2198
- runKind: "task",
2199
- runtimeAdapter: CANONICAL_RUNTIME_ADAPTER,
2200
- activeRuntimeId: runtimeIdFromRunId(asRunId(runId)),
2201
- updatedAt: event.createdAt
2202
- });
2203
- return {
2204
- status: "applied",
2205
- snapshot: withSnapshotMetadata(nextSnapshot, event, {})
2206
- };
2207
- }
2208
- if (event.type.startsWith("runtime.prepare.")) {
2209
- return applySyntheticRuntimePreparation(snapshot, event);
2210
- }
2211
- if (event.type === "runtime.prepared") {
2212
- return applySyntheticRuntimePrepared(snapshot, event);
2213
- }
2214
- if (event.type.startsWith("legacy.project.")) {
2215
- return applyLegacyProjectEvent(snapshot, event);
2216
- }
2217
- if (event.type.startsWith("legacy.thread.")) {
2218
- return applyLegacyThreadEvent(snapshot, event);
2219
- }
2220
- return { status: "ignored", snapshot: base };
2221
- }
2222
- function applyEngineEvents(snapshot, events) {
2223
- let nextSnapshot = snapshot;
2224
- let requiresResync = false;
2225
- let applied = false;
2226
- for (const event of events) {
2227
- const result = applyEngineEvent(nextSnapshot, event);
2228
- nextSnapshot = result.snapshot;
2229
- if (result.status === "requires-resync") {
2230
- requiresResync = true;
2231
- }
2232
- if (result.status === "applied") {
2233
- applied = true;
2234
- }
2235
- }
2236
- return {
2237
- status: requiresResync ? "requires-resync" : applied ? "applied" : "ignored",
2238
- snapshot: nextSnapshot
2239
- };
2240
- }
2241
- function pruneQueueEntries(snapshot) {
2242
- return snapshot.queue.filter((entry) => {
2243
- const task = snapshot.tasks.find((candidate) => candidate.id === entry.taskId);
2244
- return task?.status !== "running" && task?.status !== "in_progress" && task?.status !== "under_review";
2245
- });
2246
- }
2247
- // packages/core/src/rigSelectors.ts
2248
- function selectWorkspaces(snapshot) {
2249
- return snapshot?.workspaces ?? [];
2250
- }
2251
- function selectPrimaryWorkspace(snapshot) {
2252
- const workspaces = selectWorkspaces(snapshot);
2253
- return workspaces.find((workspace) => workspace.sourceKind === "rig-import") ?? workspaces[0] ?? null;
2254
- }
2255
- function pickDefaultWorkspaceId(snapshot) {
2256
- return selectPrimaryWorkspace(snapshot)?.id ?? null;
2257
- }
2258
- function selectWorkspace(snapshot, workspaceId) {
2259
- if (!workspaceId)
2260
- return null;
2261
- return snapshot?.workspaces.find((workspace) => workspace.id === workspaceId) ?? null;
2262
- }
2263
- function selectTask(snapshot, taskId) {
2264
- if (!taskId)
2265
- return null;
2266
- return snapshot?.tasks.find((task) => task.id === taskId) ?? null;
2267
- }
2268
- function selectTasksByWorkspace(snapshot, workspaceId) {
2269
- if (!workspaceId)
2270
- return [];
2271
- return snapshot?.tasks.filter((task) => task.workspaceId === workspaceId) ?? [];
2272
- }
2273
- var selectTasksForWorkspace = selectTasksByWorkspace;
2274
- function selectTasksByStatus(snapshot, status) {
2275
- return snapshot?.tasks.filter((task) => task.status === status) ?? [];
2276
- }
2277
- function projectTaskStatusForGrouping(status) {
2278
- switch (status) {
2279
- case "failed":
2280
- return "ready";
2281
- case "closed":
2282
- return "completed";
2283
- case "running":
2284
- case "in_progress":
2285
- case "under_review":
2286
- return "running";
2287
- case null:
2288
- case undefined:
2289
- case "":
2290
- return "unknown";
2291
- default:
2292
- return status;
2293
- }
2294
- }
2295
- function projectRunStatusForTaskGrouping(status) {
2296
- switch (status) {
2297
- case "created":
2298
- case "queued":
2299
- case "preparing":
2300
- return "queued";
2301
- case "running":
2302
- case "waiting-approval":
2303
- case "waiting-user-input":
2304
- case "paused":
2305
- case "validating":
2306
- case "reviewing":
2307
- case "closing-out":
2308
- return "running";
2309
- case "needs-attention":
2310
- return "blocked";
2311
- case "failed":
2312
- case "stopped":
2313
- return "ready";
2314
- case "completed":
2315
- return "completed";
2316
- case null:
2317
- case undefined:
2318
- return null;
2319
- }
2320
- }
2321
- var STATUS_PRIORITY = [
2322
- "running",
2323
- "blocked",
2324
- "queued",
2325
- "ready",
2326
- "open",
2327
- "draft",
2328
- "unknown",
2329
- "cancelled",
2330
- "completed"
2331
- ];
2332
- function taskIdFromSession(session) {
2333
- return session.taskId ?? session.record.taskId ?? null;
2334
- }
2335
- function latestSessionByTask(sessions) {
2336
- const byTask = new Map;
2337
- for (const session of sessions) {
2338
- const taskId = taskIdFromSession(session);
2339
- if (!taskId)
2340
- continue;
2341
- const existing = byTask.get(taskId);
2342
- if (!existing || (session.lastEventAt ?? "").localeCompare(existing.lastEventAt ?? "") >= 0) {
2343
- byTask.set(taskId, session);
2344
- }
2345
- }
2346
- return byTask;
2347
- }
2348
- function projectTaskStatusWithSessions(task, sessionsByTask = new Map) {
2349
- const sessionStatus = projectRunStatusForTaskGrouping(sessionsByTask.get(task.id)?.status);
2350
- return sessionStatus ?? projectTaskStatusForGrouping(task.status);
2351
- }
2352
- function groupTasksByProjectedStatus(tasks, sessions = []) {
2353
- const sessionsByTask = latestSessionByTask(sessions);
2354
- const byStatus = new Map;
2355
- for (const task of tasks) {
2356
- const projectedStatus = projectTaskStatusWithSessions(task, sessionsByTask);
2357
- const group = byStatus.get(projectedStatus);
2358
- if (group) {
2359
- group.push(task);
2360
- } else {
2361
- byStatus.set(projectedStatus, [task]);
2362
- }
2363
- }
2364
- const result = [];
2365
- for (const status of STATUS_PRIORITY) {
2366
- const group = byStatus.get(status);
2367
- if (group && group.length > 0) {
2368
- result.push({ status, tasks: group });
2369
- }
2370
- }
2371
- const overflowStatuses = Array.from(byStatus.keys()).filter((status) => !STATUS_PRIORITY.includes(status)).sort();
2372
- for (const status of overflowStatuses) {
2373
- const group = byStatus.get(status);
2374
- if (group && group.length > 0) {
2375
- result.push({ status, tasks: group });
2376
- }
2377
- }
2378
- return result;
2379
- }
2380
- function isProjectionGroupingInput(value) {
2381
- return Boolean(value && typeof value === "object" && !("snapshotSequence" in value) && Array.isArray(value.tasks));
2382
- }
2383
- function selectTasksGroupedByStatus(input, workspaceId) {
2384
- if (isProjectionGroupingInput(input)) {
2385
- const filteredTasks = input.workspaceId ? input.tasks.filter((task) => {
2386
- const workspaceTask = task;
2387
- return workspaceTask.workspaceId === input.workspaceId;
2388
- }) : input.tasks;
2389
- return groupTasksByProjectedStatus(filteredTasks, input.sessions);
2390
- }
2391
- const tasks = selectTasksByWorkspace(input, workspaceId ?? null);
2392
- return groupTasksByProjectedStatus(tasks, []);
2393
- }
2394
- function isObjectRecord(value) {
2395
- return typeof value === "object" && value !== null && !Array.isArray(value);
2396
- }
2397
- function normalizeLogin(value) {
2398
- return value.trim().replace(/^@+/, "").toLowerCase();
2399
- }
2400
- function assigneeLoginsFromValue(value) {
2401
- if (!Array.isArray(value))
2402
- return [];
2403
- return value.flatMap((entry) => {
2404
- if (typeof entry === "string" && entry.trim())
2405
- return [normalizeLogin(entry)];
2406
- if (isObjectRecord(entry) && typeof entry.login === "string" && entry.login.trim()) {
2407
- return [normalizeLogin(entry.login)];
2408
- }
2409
- return [];
2410
- });
2411
- }
2412
- function normalizeTaskAssigneeFilter(assignee, currentUserLogin) {
2413
- const trimmed = assignee?.trim();
2414
- if (!trimmed)
2415
- return null;
2416
- if (trimmed === "@me" || trimmed.toLowerCase() === "me") {
2417
- return currentUserLogin?.trim() ? normalizeLogin(currentUserLogin) : null;
2418
- }
2419
- return normalizeLogin(trimmed);
2420
- }
2421
- function readTaskAssigneeLogins(task) {
2422
- const taskRecord = task;
2423
- const metadata = isObjectRecord(task.metadata) ? task.metadata : null;
2424
- const raw = isObjectRecord(metadata?.raw) ? metadata.raw : null;
2425
- return Array.from(new Set([
2426
- ...assigneeLoginsFromValue(taskRecord.assignees),
2427
- ...assigneeLoginsFromValue(metadata?.assignees),
2428
- ...assigneeLoginsFromValue(raw?.assignees)
2429
- ]));
2430
- }
2431
- function taskMatchesAssigneeFilter(task, assignee, options = {}) {
2432
- const normalized = normalizeTaskAssigneeFilter(assignee, options.currentUserLogin);
2433
- if (!normalized)
2434
- return false;
2435
- return readTaskAssigneeLogins(task).includes(normalized);
2436
- }
2437
- function selectTasksAssignedTo(tasks, assignee, options = {}) {
2438
- return tasks.filter((task) => taskMatchesAssigneeFilter(task, assignee, options));
2439
- }
2440
- function selectTasksAssignedToMe(tasks, currentUserLogin) {
2441
- return selectTasksAssignedTo(tasks, "@me", currentUserLogin === undefined ? {} : { currentUserLogin });
2442
- }
2443
- function selectRun(snapshot, runId) {
2444
- if (!runId)
2445
- return null;
2446
- return snapshot?.runs.find((run) => run.id === runId) ?? null;
2447
- }
2448
- function selectRunsByTask(snapshot, taskId) {
2449
- if (!taskId)
2450
- return [];
2451
- return snapshot?.runs.filter((run) => run.taskId === taskId) ?? [];
2452
- }
2453
- var selectRunsForTask = selectRunsByTask;
2454
- function selectRunsForWorkspace(snapshot, workspaceId) {
2455
- if (!workspaceId)
2456
- return [];
2457
- return snapshot?.runs.filter((run) => run.workspaceId === workspaceId) ?? [];
2458
- }
2459
- function selectAdhocRuns(snapshot) {
2460
- return snapshot?.runs.filter((run) => run.taskId === null) ?? [];
2461
- }
2462
- function selectAdhocRunsForWorkspace(snapshot, workspaceId) {
2463
- if (!workspaceId)
2464
- return [];
2465
- return snapshot?.runs.filter((run) => run.taskId === null && run.workspaceId === workspaceId) ?? [];
2466
- }
2467
- function selectGraphsForWorkspace(snapshot, workspaceId) {
2468
- if (!workspaceId)
2469
- return [];
2470
- return snapshot?.graphs.filter((graph) => graph.workspaceId === workspaceId) ?? [];
2471
- }
2472
- function selectQueueForWorkspace(snapshot, workspaceId) {
2473
- if (!snapshot || !workspaceId)
2474
- return [];
2475
- const taskIds = new Set(selectTasksByWorkspace(snapshot, workspaceId).map((task) => task.id));
2476
- return snapshot.queue.filter((entry) => taskIds.has(entry.taskId));
2477
- }
2478
- function selectPendingApprovals(snapshot) {
2479
- return snapshot?.approvals.filter((approval) => approval.status === "pending") ?? [];
2480
- }
2481
- function selectApprovalsForRun(snapshot, runId) {
2482
- if (!runId)
2483
- return [];
2484
- return snapshot?.approvals.filter((approval) => approval.runId === runId) ?? [];
2485
- }
2486
- function selectApprovalsForWorkspace(snapshot, workspaceId) {
2487
- if (!snapshot || !workspaceId)
2488
- return [];
2489
- const runIds = new Set(selectRunsForWorkspace(snapshot, workspaceId).map((run) => run.id));
2490
- return snapshot.approvals.filter((approval) => runIds.has(approval.runId));
2491
- }
2492
- function selectUserInputsForRun(snapshot, runId) {
2493
- if (!runId)
2494
- return [];
2495
- return (snapshot?.userInputs ?? []).filter((request) => request.runId === runId);
2496
- }
2497
- function selectUserInputsForWorkspace(snapshot, workspaceId) {
2498
- if (!snapshot || !workspaceId)
2499
- return [];
2500
- const runIds = new Set(selectRunsForWorkspace(snapshot, workspaceId).map((run) => run.id));
2501
- return (snapshot.userInputs ?? []).filter((request) => runIds.has(request.runId));
2502
- }
2503
- // packages/core/src/taskScore.ts
2504
- var ROLE_WEIGHT = {
2505
- architect: 25,
2506
- extractor: 10
2507
- };
2508
- var CRITICALITY_WEIGHT = {
2509
- core: 20,
2510
- high: 10,
2511
- normal: 0
2512
- };
2513
- function finiteNumber(value, fallback) {
2514
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
2515
- }
2516
- function scoreTask(input) {
2517
- const priority = finiteNumber(input.priority, 3);
2518
- const unblockCount = Math.max(0, finiteNumber(input.unblockCount, 0));
2519
- const roleWeight = input.role ? ROLE_WEIGHT[input.role] ?? 0 : 0;
2520
- const criticalityWeight = input.criticality ? CRITICALITY_WEIGHT[input.criticality] ?? 0 : 0;
2521
- const validationWeight = (input.validation ?? []).some((entry) => entry.includes("test-contract") || entry.includes("test-boundary")) ? 8 : 0;
2522
- const queueWeight = finiteNumber(input.queueWeight, 0);
2523
- return unblockCount * 100 + (10 - priority) + roleWeight + criticalityWeight + validationWeight + queueWeight;
2524
- }
2525
- function rankTasks(tasks, scoreInput, idOf) {
2526
- return tasks.map((task) => {
2527
- const input = scoreInput(task);
2528
- return {
2529
- task,
2530
- score: scoreTask(input),
2531
- priority: finiteNumber(input.priority, 3),
2532
- unblockCount: Math.max(0, finiteNumber(input.unblockCount, 0))
2533
- };
2534
- }).toSorted((left, right) => {
2535
- const scoreDelta = right.score - left.score;
2536
- if (scoreDelta !== 0)
2537
- return scoreDelta;
2538
- const unblockDelta = right.unblockCount - left.unblockCount;
2539
- if (unblockDelta !== 0)
2540
- return unblockDelta;
2541
- const priorityDelta = left.priority - right.priority;
2542
- if (priorityDelta !== 0)
2543
- return priorityDelta;
2544
- return idOf(left.task).localeCompare(idOf(right.task));
2545
- });
2546
- }
2547
-
2548
- // packages/core/src/taskGraph.ts
2549
- function isObjectRecord2(value) {
2550
- return typeof value === "object" && value !== null && !Array.isArray(value);
2551
- }
2552
- function readStringList(value) {
2553
- return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
2554
- }
2555
- function unique(values) {
2556
- return Array.from(new Set(values));
2557
- }
2558
- function readTaskMetadataStringList(task, key) {
2559
- const taskRecord = task;
2560
- const topLevel = readStringList(taskRecord[key]);
2561
- if (topLevel.length > 0)
2562
- return topLevel;
2563
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2564
- const metadataList = readStringList(metadata?.[key]);
2565
- if (metadataList.length > 0)
2566
- return metadataList;
2567
- if (key === "dependencies") {
2568
- return readStringList(metadata?.deps);
2569
- }
2570
- return [];
2571
- }
2572
- function readTaskBlockingDependencyRefs(task) {
2573
- return readTaskMetadataStringList(task, "dependencies");
2574
- }
2575
- function readTaskDependencyRefs(task) {
2576
- return unique([
2577
- ...readTaskMetadataStringList(task, "dependencies"),
2578
- ...readTaskMetadataStringList(task, "parentChildDeps")
2579
- ]);
2580
- }
2581
- function readTaskSourceIssueId(task) {
2582
- if (typeof task.sourceIssueId === "string" && task.sourceIssueId.length > 0) {
2583
- return task.sourceIssueId;
2584
- }
2585
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2586
- if (typeof metadata?.sourceIssueId === "string" && metadata.sourceIssueId.length > 0) {
2587
- return metadata.sourceIssueId;
2588
- }
2589
- const rigMetadata = isObjectRecord2(metadata?._rig) ? metadata._rig : null;
2590
- return typeof rigMetadata?.sourceIssueId === "string" && rigMetadata.sourceIssueId.length > 0 ? rigMetadata.sourceIssueId : null;
2591
- }
2592
- function readTaskScope(task) {
2593
- const taskRecord = task;
2594
- const topLevel = readStringList(taskRecord.scope);
2595
- if (topLevel.length > 0)
2596
- return unique(topLevel.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
2597
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2598
- const metadataScope = readStringList(metadata?.scope);
2599
- if (metadataScope.length > 0)
2600
- return unique(metadataScope.map((entry) => entry.trim()).filter((entry) => entry.length > 0));
2601
- return unique([
2602
- ...readStringList(metadata?.files),
2603
- ...readStringList(metadata?.paths)
2604
- ].map((entry) => entry.trim()).filter((entry) => entry.length > 0));
2605
- }
2606
- function isScopeList(input) {
2607
- return Array.isArray(input);
2608
- }
2609
- function normalizeScopeInput(input) {
2610
- return isScopeList(input) ? unique(input.map((entry) => entry.trim()).filter((entry) => entry.length > 0)) : readTaskScope(input);
2611
- }
2612
- function disjointScope(left, right) {
2613
- const leftScope = new Set(normalizeScopeInput(left));
2614
- if (leftScope.size === 0)
2615
- return true;
2616
- return normalizeScopeInput(right).every((entry) => !leftScope.has(entry));
2617
- }
2618
- function resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId) {
2619
- if (tasksById.has(ref))
2620
- return ref;
2621
- return taskIdBySourceIssueId.get(ref) ?? taskIdByExternalRef.get(ref) ?? null;
2622
- }
2623
- function buildTaskReferenceIndex(tasks) {
2624
- return {
2625
- tasksById: new Map(tasks.map((task) => [task.id, task])),
2626
- taskIdByExternalRef: new Map(tasks.flatMap((task) => task.externalId ? [[task.externalId, task.id]] : [])),
2627
- taskIdBySourceIssueId: new Map(tasks.flatMap((task) => {
2628
- const sourceIssueId = readTaskSourceIssueId(task);
2629
- return sourceIssueId ? [[sourceIssueId, task.id]] : [];
2630
- }))
2631
- };
2632
- }
2633
- function computeTaskBlockingDepths(tasks) {
2634
- const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
2635
- const memo = new Map;
2636
- const visit = (taskId, stack) => {
2637
- const cached = memo.get(taskId);
2638
- if (cached !== undefined)
2639
- return cached;
2640
- if (stack.has(taskId))
2641
- return 0;
2642
- const task = tasksById.get(taskId);
2643
- if (!task)
2644
- return 0;
2645
- stack.add(taskId);
2646
- const blockers = readTaskBlockingDependencyRefs(task).map((ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId)).filter((ref) => ref !== null && ref !== taskId);
2647
- const depth = blockers.length === 0 ? 0 : Math.max(...blockers.map((blockerId) => visit(blockerId, stack) + 1));
2648
- stack.delete(taskId);
2649
- memo.set(taskId, depth);
2650
- return depth;
2651
- };
2652
- for (const task of tasks) {
2653
- visit(task.id, new Set);
2654
- }
2655
- return memo;
2656
- }
2657
- function isTaskTerminalStatus(status) {
2658
- switch (status) {
2659
- case "closed":
2660
- case "completed":
2661
- case "done":
2662
- case "cancelled":
2663
- case "canceled":
2664
- return true;
2665
- default:
2666
- return false;
2667
- }
2668
- }
2669
- function isTaskBlockedStatus(status) {
2670
- return status === "blocked";
2671
- }
2672
- function isTaskRunnableStatus(status) {
2673
- if (status === null || status === undefined || status === "")
2674
- return true;
2675
- if (isTaskTerminalStatus(status) || isTaskBlockedStatus(status))
2676
- return false;
2677
- switch (status) {
2678
- case "ready":
2679
- case "open":
2680
- case "failed":
2681
- return true;
2682
- default:
2683
- return false;
2684
- }
2685
- }
2686
- function priorityValue(task) {
2687
- return typeof task.priority === "number" && Number.isFinite(task.priority) ? task.priority : Number.MAX_SAFE_INTEGER;
2688
- }
2689
- function readTaskRole(task) {
2690
- if (typeof task.role === "string" && task.role.trim())
2691
- return task.role.trim();
2692
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2693
- return typeof metadata?.role === "string" && metadata.role.trim() ? metadata.role.trim() : null;
2694
- }
2695
- function readTaskCriticality(task) {
2696
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2697
- return typeof metadata?.criticality === "string" && metadata.criticality.trim() ? metadata.criticality.trim() : null;
2698
- }
2699
- function readTaskValidationKeys(task) {
2700
- const taskRecord = task;
2701
- const topLevel = readStringList(taskRecord.validationKeys);
2702
- if (topLevel.length > 0)
2703
- return topLevel;
2704
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2705
- return readStringList(metadata?.validation);
2706
- }
2707
- function readTaskQueueWeight(task) {
2708
- const metadata = isObjectRecord2(task.metadata) ? task.metadata : null;
2709
- const queueWeight = metadata?.queueWeight ?? metadata?.queue_weight;
2710
- return typeof queueWeight === "number" && Number.isFinite(queueWeight) ? queueWeight : 0;
2711
- }
2712
- function scoreInputForTask(task, unblockCount) {
2713
- return {
2714
- priority: task.priority,
2715
- unblockCount,
2716
- role: readTaskRole(task),
2717
- criticality: readTaskCriticality(task),
2718
- validation: readTaskValidationKeys(task),
2719
- queueWeight: readTaskQueueWeight(task)
2720
- };
2721
- }
2722
- function computeTaskDependencyBadges(tasks) {
2723
- const index = buildTaskReferenceIndex(tasks);
2724
- const blockingDepths = computeTaskBlockingDepths(tasks);
2725
- const dependencyIdsByTask = new Map;
2726
- const unresolvedRefsByTask = new Map;
2727
- const blocksByTask = new Map;
2728
- for (const task of tasks) {
2729
- const dependencyIds = [];
2730
- const unresolvedRefs = [];
2731
- for (const ref of readTaskBlockingDependencyRefs(task)) {
2732
- const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
2733
- if (dependencyId && dependencyId !== task.id) {
2734
- dependencyIds.push(dependencyId);
2735
- const blocks = blocksByTask.get(dependencyId);
2736
- if (blocks) {
2737
- blocks.push(task.id);
2738
- } else {
2739
- blocksByTask.set(dependencyId, [task.id]);
2740
- }
2741
- } else {
2742
- unresolvedRefs.push(ref);
2743
- }
2744
- }
2745
- dependencyIdsByTask.set(task.id, unique(dependencyIds));
2746
- unresolvedRefsByTask.set(task.id, unique(unresolvedRefs));
2747
- }
2748
- const summaries = new Map;
2749
- for (const task of tasks) {
2750
- const dependencyIds = dependencyIdsByTask.get(task.id) ?? [];
2751
- const unresolvedDependencyRefs = unresolvedRefsByTask.get(task.id) ?? [];
2752
- const blockedBy = dependencyIds.filter((dependencyId) => {
2753
- const dependency = index.tasksById.get(dependencyId);
2754
- return dependency ? !isTaskTerminalStatus(dependency.status) : false;
2755
- });
2756
- const blocks = unique(blocksByTask.get(task.id) ?? []);
2757
- const blocked = isTaskBlockedStatus(task.status) || blockedBy.length > 0;
2758
- const ready = isTaskRunnableStatus(task.status) && !blocked;
2759
- const badges = [];
2760
- if (blocked) {
2761
- badges.push({
2762
- kind: "blocked",
2763
- label: blockedBy.length > 0 ? `blocked \xD7${blockedBy.length}` : "blocked",
2764
- description: blockedBy.length > 0 ? `Waiting on ${blockedBy.join(", ")}.` : "Task source marks this task blocked.",
2765
- ...blockedBy.length > 0 ? { count: blockedBy.length } : {},
2766
- taskIds: blockedBy
2767
- });
2768
- } else if (ready) {
2769
- badges.push({
2770
- kind: "ready",
2771
- label: "ready",
2772
- description: "No open dependencies block this task."
2773
- });
2774
- }
2775
- if (dependencyIds.length > 0 || blocks.length > 0 || unresolvedDependencyRefs.length > 0) {
2776
- badges.push({
2777
- kind: "dependency",
2778
- label: `deps ${dependencyIds.length}/${blocks.length}`,
2779
- description: [
2780
- dependencyIds.length > 0 ? `Depends on ${dependencyIds.join(", ")}.` : null,
2781
- blocks.length > 0 ? `Blocks ${blocks.join(", ")}.` : null,
2782
- unresolvedDependencyRefs.length > 0 ? `Unresolved refs: ${unresolvedDependencyRefs.join(", ")}.` : null
2783
- ].filter((part) => part !== null).join(" "),
2784
- count: dependencyIds.length + blocks.length,
2785
- taskIds: unique([...dependencyIds, ...blocks])
2786
- });
2787
- }
2788
- summaries.set(task.id, {
2789
- taskId: task.id,
2790
- blockingDepth: blockingDepths.get(task.id) ?? 0,
2791
- dependencyIds,
2792
- unresolvedDependencyRefs,
2793
- blockedBy,
2794
- blocks,
2795
- blocked,
2796
- ready,
2797
- dependencyCount: dependencyIds.length,
2798
- dependentCount: blocks.length,
2799
- badges
2800
- });
2801
- }
2802
- return summaries;
2803
- }
2804
- function selectNextReadyTaskByPriority(tasks, options = {}) {
2805
- const excluded = new Set(options.excludeTaskIds ?? []);
2806
- const badges = computeTaskDependencyBadges(tasks);
2807
- const candidates = tasks.filter((task) => !excluded.has(task.id)).filter((task) => options.filter?.(task) ?? true).filter((task) => badges.get(task.id)?.ready === true).toSorted((left, right) => {
2808
- const priorityDelta = priorityValue(left) - priorityValue(right);
2809
- if (priorityDelta !== 0)
2810
- return priorityDelta;
2811
- const createdDelta = (left.createdAt ?? "").localeCompare(right.createdAt ?? "");
2812
- if (createdDelta !== 0)
2813
- return createdDelta;
2814
- return left.id.localeCompare(right.id);
2815
- });
2816
- return candidates[0] ?? null;
2817
- }
2818
- function rankReadyTasks(tasks, options = {}) {
2819
- const excluded = new Set(options.excludeTaskIds ?? []);
2820
- const activeTaskIds = new Set(options.activeTaskIds ?? []);
2821
- const badges = computeTaskDependencyBadges(tasks);
2822
- const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
2823
- const openBlockCount = (task) => (badges.get(task.id)?.blocks ?? []).filter((blockedTaskId) => {
2824
- const blockedTask = tasksById.get(blockedTaskId);
2825
- return blockedTask ? !isTaskTerminalStatus(blockedTask.status) : false;
2826
- }).length;
2827
- const readyTasks = tasks.filter((task) => !excluded.has(task.id)).filter((task) => !activeTaskIds.has(task.id)).filter((task) => options.filter?.(task) ?? true).filter((task) => badges.get(task.id)?.ready === true);
2828
- const maxUnblockCount = Math.max(0, ...readyTasks.map(openBlockCount));
2829
- const selectedByMode = readyTasks.filter((task) => {
2830
- const unblockCount = openBlockCount(task);
2831
- if (options.selection === "blocking-only")
2832
- return unblockCount > 0;
2833
- if (options.selection === "max-unblock")
2834
- return maxUnblockCount > 0 && unblockCount === maxUnblockCount;
2835
- return true;
2836
- });
2837
- return rankTasks(selectedByMode, (task) => scoreInputForTask(task, openBlockCount(task)), (task) => task.id).map((entry) => ({
2838
- task: entry.task,
2839
- score: entry.score,
2840
- priority: entry.priority,
2841
- unblockCount: entry.unblockCount,
2842
- scope: readTaskScope(entry.task)
2843
- }));
2844
- }
2845
- function selectRankedReadyTasks(tasks, options = {}) {
2846
- const ranked = rankReadyTasks(tasks, options);
2847
- if (options.requireDisjointScopes !== true && !options.disjointWithScopes) {
2848
- return ranked.slice(0, options.limit).map((entry) => entry.task);
2849
- }
2850
- const occupiedScopes = new Set(options.disjointWithScopes ?? []);
2851
- const selected = [];
2852
- for (const entry of ranked) {
2853
- if (entry.scope.some((scope) => occupiedScopes.has(scope)))
2854
- continue;
2855
- selected.push(entry.task);
2856
- for (const scope of entry.scope)
2857
- occupiedScopes.add(scope);
2858
- if (options.limit !== undefined && selected.length >= options.limit)
2859
- break;
2860
- }
2861
- return selected;
2862
- }
2863
- // packages/core/src/taskGraphCodes.ts
2864
- var TASK_CODE_RE = /^\[([A-Z0-9]+(?:-[A-Z0-9]+)*)\]\s*/;
2865
- function extractTaskCode(title) {
2866
- const match = title.match(TASK_CODE_RE);
2867
- return match?.[1] ?? null;
2868
- }
2869
- function extractTaskGroupKey(title) {
2870
- const code = extractTaskCode(title);
2871
- if (!code)
2872
- return null;
2873
- const parts = code.split("-");
2874
- const suffix = parts.at(-1) ?? "";
2875
- if (/^\d+$/.test(suffix)) {
2876
- return parts.slice(0, -1).join("-");
2877
- }
2878
- return parts[0] ?? code;
2879
- }
2880
- function stripTaskCode(label) {
2881
- return label.replace(TASK_CODE_RE, "");
2882
- }
2883
- // packages/core/src/taskGraphLayout.ts
2884
- var CARD_WIDTH = 200;
2885
- var CARD_HEIGHT = 110;
2886
- var CELL_V_PAD = 12;
2887
- var CELL_H_PAD = 12;
2888
- var ROW_GAP = 28;
2889
- var COL_GAP = 40;
2890
- var LANE_LABEL_W = 120;
2891
- var STAGE_HDR_H = 32;
2892
- var PALETTE = [
2893
- { bg: "#3a2d12", border: "#8d6b19", edge: "#d6a11d" },
2894
- { bg: "#102642", border: "#245fbf", edge: "#66a2ff" },
2895
- { bg: "#2c173f", border: "#7b39d4", edge: "#a76df5" },
2896
- { bg: "#112d1c", border: "#2d8f4e", edge: "#62d882" },
2897
- { bg: "#3a2314", border: "#c86d1c", edge: "#e69654" },
2898
- { bg: "#31152b", border: "#bf3d88", edge: "#f07ebb" },
2899
- { bg: "#132c35", border: "#1783a6", edge: "#53c4e5" },
2900
- { bg: "#26310f", border: "#6d9a19", edge: "#a7da42" }
2901
- ];
2902
- function isObjectRecord3(value) {
2903
- return typeof value === "object" && value !== null && !Array.isArray(value);
2904
- }
2905
- function readIssueType(task) {
2906
- const metadata = isObjectRecord3(task.metadata) ? task.metadata : null;
2907
- if (typeof metadata?.issueType === "string")
2908
- return metadata.issueType;
2909
- const raw = isObjectRecord3(metadata?.raw) ? metadata.raw : null;
2910
- return typeof raw?.issueType === "string" ? raw.issueType : null;
2911
- }
2912
- function isGraphTask(task) {
2913
- return readIssueType(task) !== "epic";
2914
- }
2915
- function findEpicAncestor(task, resolve, tasksById) {
2916
- const visited = new Set;
2917
- let current = task;
2918
- let epic = null;
2919
- while (!visited.has(current.id)) {
2920
- visited.add(current.id);
2921
- const parentRef = readTaskMetadataStringList(current, "parentChildDeps")[0];
2922
- if (!parentRef)
2923
- break;
2924
- const parentId = resolve(parentRef);
2925
- if (!parentId)
2926
- break;
2927
- const parent = tasksById.get(parentId);
2928
- if (!parent)
2929
- break;
2930
- if (readIssueType(parent) === "epic") {
2931
- epic = parent;
2932
- }
2933
- current = parent;
2934
- }
2935
- return epic;
2936
- }
2937
- function getRowKey(task, resolve, tasksById) {
2938
- const epic = findEpicAncestor(task, resolve, tasksById);
2939
- if (epic) {
2940
- return `group:${epic.id}`;
2941
- }
2942
- const codeGroup = extractTaskGroupKey(task.title);
2943
- if (codeGroup) {
2944
- return `code:${codeGroup}`;
2945
- }
2946
- return `type:${readIssueType(task) ?? "task"}`;
2947
- }
2948
- function getRowLabel(task, rowKey, resolve, tasksById) {
2949
- if (rowKey.startsWith("group:")) {
2950
- const groupId = rowKey.slice("group:".length);
2951
- return tasksById.get(groupId)?.title ?? groupId;
2952
- }
2953
- if (rowKey.startsWith("code:")) {
2954
- return rowKey.slice("code:".length);
2955
- }
2956
- const code = extractTaskCode(task.title);
2957
- if (code)
2958
- return code;
2959
- const issueType = readIssueType(task);
2960
- if (issueType === "task")
2961
- return "Tasks";
2962
- if (issueType)
2963
- return `${issueType[0]?.toUpperCase() ?? ""}${issueType.slice(1)}`;
2964
- const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
2965
- if (parentRef) {
2966
- const parentId = resolve(parentRef);
2967
- if (parentId) {
2968
- return tasksById.get(parentId)?.title ?? "Tasks";
2969
- }
2970
- }
2971
- return "Tasks";
2972
- }
2973
- function computeDepths(ids, edges) {
2974
- const blockers = new Map;
2975
- for (const edge of edges) {
2976
- if (!ids.has(edge.source) || !ids.has(edge.target))
2977
- continue;
2978
- const current = blockers.get(edge.target) ?? [];
2979
- current.push(edge.source);
2980
- blockers.set(edge.target, current);
2981
- }
2982
- const memo = new Map;
2983
- const visit = (id, stack) => {
2984
- const cached = memo.get(id);
2985
- if (cached !== undefined)
2986
- return cached;
2987
- if (stack.has(id))
2988
- return 0;
2989
- stack.add(id);
2990
- const deps = blockers.get(id);
2991
- if (!deps || deps.length === 0) {
2992
- memo.set(id, 0);
2993
- return 0;
2994
- }
2995
- let maxDepth = 0;
2996
- for (const dep of deps) {
2997
- maxDepth = Math.max(maxDepth, visit(dep, stack) + 1);
2998
- }
2999
- stack.delete(id);
3000
- memo.set(id, maxDepth);
3001
- return maxDepth;
3002
- };
3003
- for (const id of ids) {
3004
- visit(id, new Set);
3005
- }
3006
- return memo;
3007
- }
3008
- function buildTaskGraphLayout(snapshot, tasks, options) {
3009
- const showParentChild = options?.showParentChild ?? false;
3010
- const graphTasks = tasks.filter(isGraphTask);
3011
- if (graphTasks.length === 0) {
3012
- return {
3013
- lanes: [],
3014
- stages: [],
3015
- nodes: [],
3016
- edges: [],
3017
- totalWidth: 0,
3018
- totalHeight: 0,
3019
- taskCount: 0
3020
- };
3021
- }
3022
- const { tasksById, taskIdByExternalRef, taskIdBySourceIssueId } = buildTaskReferenceIndex(tasks);
3023
- const resolve = (ref) => resolveTaskReference(ref, tasksById, taskIdByExternalRef, taskIdBySourceIssueId);
3024
- const rows = new Map;
3025
- const rowLabelByKey = new Map;
3026
- for (const task of graphTasks) {
3027
- const rowKey = getRowKey(task, resolve, tasksById);
3028
- const current = rows.get(rowKey) ?? [];
3029
- current.push(task);
3030
- rows.set(rowKey, current);
3031
- if (!rowLabelByKey.has(rowKey)) {
3032
- rowLabelByKey.set(rowKey, getRowLabel(task, rowKey, resolve, tasksById));
3033
- }
3034
- }
3035
- const orderedRows = [...rows.entries()].sort((left, right) => {
3036
- if (left[1].length !== right[1].length)
3037
- return right[1].length - left[1].length;
3038
- return (rowLabelByKey.get(left[0]) ?? left[0]).localeCompare(rowLabelByKey.get(right[0]) ?? right[0]);
3039
- });
3040
- const rowIndexByKey = new Map(orderedRows.map(([key], index) => [key, index]));
3041
- const rowIndexByTaskId = new Map;
3042
- for (const task of graphTasks) {
3043
- rowIndexByTaskId.set(task.id, rowIndexByKey.get(getRowKey(task, resolve, tasksById)) ?? 0);
3044
- }
3045
- const blockingEdgesRaw = [];
3046
- const depsIn = new Map;
3047
- const depsOut = new Map;
3048
- const edges = [];
3049
- for (const task of graphTasks) {
3050
- for (const ref of readTaskMetadataStringList(task, "dependencies")) {
3051
- const sourceId = resolve(ref);
3052
- if (!sourceId)
3053
- continue;
3054
- blockingEdgesRaw.push({ source: sourceId, target: task.id });
3055
- depsOut.set(sourceId, (depsOut.get(sourceId) ?? 0) + 1);
3056
- depsIn.set(task.id, (depsIn.get(task.id) ?? 0) + 1);
3057
- const color = PALETTE[(rowIndexByTaskId.get(sourceId) ?? 0) % PALETTE.length].edge;
3058
- edges.push({
3059
- id: `blocking:${sourceId}:${task.id}`,
3060
- sourceId,
3061
- targetId: task.id,
3062
- color,
3063
- kind: "blocking"
3064
- });
3065
- }
3066
- if (!showParentChild)
3067
- continue;
3068
- for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
3069
- const sourceId = resolve(ref);
3070
- if (!sourceId)
3071
- continue;
3072
- edges.push({
3073
- id: `parent:${sourceId}:${task.id}`,
3074
- sourceId,
3075
- targetId: task.id,
3076
- color: "#5b6472",
3077
- kind: "parent-child"
3078
- });
3079
- }
3080
- }
3081
- const graphTaskIds = new Set(graphTasks.map((task) => task.id));
3082
- const depths = computeDepths(graphTaskIds, blockingEdgesRaw);
3083
- const maxStage = Math.max(0, ...depths.values());
3084
- const rowMaxStack = new Array(orderedRows.length).fill(0);
3085
- const cells = new Map;
3086
- for (const task of graphTasks) {
3087
- const rowIndex = rowIndexByTaskId.get(task.id) ?? 0;
3088
- const stage = depths.get(task.id) ?? 0;
3089
- const key = `${rowIndex}:${stage}`;
3090
- const current = cells.get(key) ?? [];
3091
- current.push(task);
3092
- cells.set(key, current);
3093
- }
3094
- for (const [cellKey, cellTasks] of cells) {
3095
- const [rowIndexText] = cellKey.split(":");
3096
- const rowIndex = Number.parseInt(rowIndexText ?? "0", 10) || 0;
3097
- cellTasks.sort((left, right) => {
3098
- const leftFanout = depsOut.get(left.id) ?? 0;
3099
- const rightFanout = depsOut.get(right.id) ?? 0;
3100
- if (leftFanout !== rightFanout)
3101
- return rightFanout - leftFanout;
3102
- return left.title.localeCompare(right.title);
3103
- });
3104
- rowMaxStack[rowIndex] = Math.max(rowMaxStack[rowIndex] ?? 0, cellTasks.length);
3105
- }
3106
- const rowHeights = rowMaxStack.map((count) => Math.max(count, 1) * (CARD_HEIGHT + CELL_V_PAD) - CELL_V_PAD + CELL_V_PAD * 2);
3107
- const colWidths = new Array(maxStage + 1).fill(CARD_WIDTH + CELL_H_PAD * 2);
3108
- const colX = [];
3109
- let currentX = LANE_LABEL_W;
3110
- for (let index = 0;index <= maxStage; index += 1) {
3111
- colX.push(currentX);
3112
- currentX += (colWidths[index] ?? 0) + COL_GAP;
3113
- }
3114
- const totalWidth = currentX - COL_GAP;
3115
- const rowY = [];
3116
- let currentY = STAGE_HDR_H;
3117
- for (let rowIndex = 0;rowIndex < orderedRows.length; rowIndex += 1) {
3118
- rowY.push(currentY);
3119
- currentY += (rowHeights[rowIndex] ?? 0) + ROW_GAP;
3120
- }
3121
- const totalHeight = currentY - ROW_GAP;
3122
- const lanes = orderedRows.map(([rowKey, rowTasks], rowIndex) => ({
3123
- key: rowKey,
3124
- label: rowLabelByKey.get(rowKey) ?? rowKey,
3125
- rowIndex,
3126
- x: LANE_LABEL_W - 6,
3127
- y: (rowY[rowIndex] ?? 0) - 6,
3128
- width: totalWidth - LANE_LABEL_W + 12,
3129
- height: (rowHeights[rowIndex] ?? 0) + 12,
3130
- color: PALETTE[rowIndex % PALETTE.length].border,
3131
- taskCount: rowTasks.length
3132
- }));
3133
- const stages = Array.from({ length: maxStage + 1 }, (_, index) => ({
3134
- index,
3135
- label: index === 0 ? "Roots" : `Stage ${index}`,
3136
- x: (colX[index] ?? 0) + CELL_H_PAD,
3137
- width: CARD_WIDTH
3138
- }));
3139
- const pendingApprovalRunIds = new Set(selectPendingApprovals(snapshot).map((approval) => approval.runId));
3140
- const nodes = [];
3141
- for (const [rowIndex, [rowKey]] of orderedRows.entries()) {
3142
- for (let stage = 0;stage <= maxStage; stage += 1) {
3143
- const cellTasks = cells.get(`${rowIndex}:${stage}`) ?? [];
3144
- const baseX = (colX[stage] ?? 0) + CELL_H_PAD;
3145
- const baseY = (rowY[rowIndex] ?? 0) + CELL_V_PAD;
3146
- const palette = PALETTE[rowIndex % PALETTE.length];
3147
- for (const [stackIndex, task] of cellTasks.entries()) {
3148
- const runs = selectRunsByTask(snapshot, task.id);
3149
- const runIds = new Set(runs.map((run) => run.id));
3150
- const hasApprovals = runs.some((run) => pendingApprovalRunIds.has(run.id));
3151
- const hasPendingUserInput = runs.some((run) => selectUserInputsForRun(snapshot, run.id).some((request) => request.status === "pending"));
3152
- const hasRejectedReview = (snapshot?.reviews ?? []).some((review) => runIds.has(review.runId) && review.status === "rejected");
3153
- const hasFailedValidations = (snapshot?.validations ?? []).some((validation) => runIds.has(validation.runId) && validation.status === "failed");
3154
- const artifactCount = (snapshot?.artifacts ?? []).filter((artifact) => runIds.has(artifact.runId)).length;
3155
- nodes.push({
3156
- id: task.id,
3157
- taskId: task.id,
3158
- task,
3159
- rowKey,
3160
- rowLabel: rowLabelByKey.get(rowKey) ?? rowKey,
3161
- rowIndex,
3162
- stage,
3163
- x: baseX,
3164
- y: baseY + stackIndex * (CARD_HEIGHT + CELL_V_PAD),
3165
- width: CARD_WIDTH,
3166
- height: CARD_HEIGHT,
3167
- color: palette.border,
3168
- taskCode: extractTaskCode(task.title),
3169
- strippedTitle: stripTaskCode(task.title),
3170
- depsIn: depsIn.get(task.id) ?? 0,
3171
- depsOut: depsOut.get(task.id) ?? 0,
3172
- runCount: runs.length,
3173
- hasApprovals,
3174
- hasPendingUserInput,
3175
- hasRejectedReview,
3176
- hasFailedValidations,
3177
- artifactCount
3178
- });
3179
- }
3180
- }
3181
- }
3182
- return {
3183
- lanes,
3184
- stages,
3185
- nodes,
3186
- edges,
3187
- totalWidth,
3188
- totalHeight,
3189
- taskCount: graphTasks.length
3190
- };
3191
- }
3192
- // packages/core/src/stageResolve.ts
3193
- class PipelineUnresolvableError extends Error {
3194
- cycles;
3195
- contributors;
3196
- constructor(message, cycles, contributors) {
3197
- super(message);
3198
- this.name = "PipelineUnresolvableError";
3199
- this.cycles = cycles;
3200
- this.contributors = contributors;
3201
- }
3202
- }
3203
- function uniqueSorted(values) {
3204
- return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
3205
- }
3206
- function wrapperForMutation(mutation) {
3207
- return "wrapper" in mutation ? mutation.wrapper : mutation.around;
3208
- }
3209
- function contributorOf(mutation) {
3210
- if (mutation.contributedBy?.trim())
3211
- return mutation.contributedBy;
3212
- if (mutation.op === "wrap") {
3213
- const wrapper = wrapperForMutation(mutation);
3214
- if (wrapper.id?.trim())
3215
- return wrapper.id;
3216
- }
3217
- return "anonymous";
3218
- }
3219
- function stableMutationCompare(left, right) {
3220
- const leftTarget = left.op === "insert" ? left.stage.id : left.id;
3221
- const rightTarget = right.op === "insert" ? right.stage.id : right.id;
3222
- const targetDelta = leftTarget.localeCompare(rightTarget);
3223
- if (targetDelta !== 0)
3224
- return targetDelta;
3225
- return contributorOf(left).localeCompare(contributorOf(right));
3226
- }
3227
- function ensureUniqueStageIds(stages) {
3228
- const seen = new Set;
3229
- for (const stage of stages) {
3230
- if (seen.has(stage.id)) {
3231
- throw new PipelineUnresolvableError(`Duplicate stage id: ${stage.id}`, [], []);
3232
- }
3233
- seen.add(stage.id);
3234
- }
3235
- }
3236
- function assertNoDuplicateMutationTargets(mutations, op) {
3237
- const seen = new Map;
3238
- for (const mutation of mutations.filter((entry) => entry.op === op)) {
3239
- const id = mutation.op === "insert" ? mutation.stage.id : mutation.id;
3240
- const previous = seen.get(id);
3241
- if (previous) {
3242
- throw new PipelineUnresolvableError(`Duplicate ${op} mutation for stage ${id}: ${previous}, ${contributorOf(mutation)}`, [], uniqueSorted([previous, contributorOf(mutation)]));
3243
- }
3244
- seen.set(id, contributorOf(mutation));
3245
- }
3246
- }
3247
- function mergeAnchors(current, incoming) {
3248
- return uniqueSorted([...current, ...incoming ?? []]);
3249
- }
3250
- function stagePriority(stage) {
3251
- return typeof stage.priority === "number" && Number.isFinite(stage.priority) ? stage.priority : 0;
3252
- }
3253
- function readyCompare(states) {
3254
- return (left, right) => {
3255
- const leftState = states.get(left);
3256
- const rightState = states.get(right);
3257
- const priorityDelta = stagePriority(rightState?.stage ?? { id: right }) - stagePriority(leftState?.stage ?? { id: left });
3258
- if (priorityDelta !== 0)
3259
- return priorityDelta;
3260
- if (leftState?.baseIndex !== null && rightState?.baseIndex !== null && leftState?.baseIndex !== rightState?.baseIndex) {
3261
- return (leftState?.baseIndex ?? 0) - (rightState?.baseIndex ?? 0);
3262
- }
3263
- if (leftState?.baseIndex !== null && rightState?.baseIndex === null)
3264
- return -1;
3265
- if (leftState?.baseIndex === null && rightState?.baseIndex !== null)
3266
- return 1;
3267
- return left.localeCompare(right);
3268
- };
3269
- }
3270
- function findCycles(nodes, edges) {
3271
- const adjacency = new Map;
3272
- for (const node of nodes)
3273
- adjacency.set(node, []);
3274
- for (const edge of edges)
3275
- adjacency.get(edge.from)?.push(edge.to);
3276
- for (const targets of adjacency.values())
3277
- targets.sort((left, right) => left.localeCompare(right));
3278
- let nextIndex = 0;
3279
- const indexByNode = new Map;
3280
- const lowByNode = new Map;
3281
- const stack = [];
3282
- const onStack = new Set;
3283
- const cycles = [];
3284
- const visit = (node) => {
3285
- indexByNode.set(node, nextIndex);
3286
- lowByNode.set(node, nextIndex);
3287
- nextIndex += 1;
3288
- stack.push(node);
3289
- onStack.add(node);
3290
- for (const target of adjacency.get(node) ?? []) {
3291
- if (!indexByNode.has(target)) {
3292
- visit(target);
3293
- lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
3294
- } else if (onStack.has(target)) {
3295
- lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
3296
- }
3297
- }
3298
- if (lowByNode.get(node) !== indexByNode.get(node))
3299
- return;
3300
- const component = [];
3301
- let current;
3302
- do {
3303
- current = stack.pop();
3304
- if (current) {
3305
- onStack.delete(current);
3306
- component.push(current);
3307
- }
3308
- } while (current && current !== node);
3309
- const hasSelfLoop = edges.some((edge) => edge.from === node && edge.to === node);
3310
- if (component.length > 1 || hasSelfLoop) {
3311
- cycles.push(component.sort((left, right) => left.localeCompare(right)));
3312
- }
3313
- };
3314
- for (const node of [...nodes].sort((left, right) => left.localeCompare(right))) {
3315
- if (!indexByNode.has(node))
3316
- visit(node);
3317
- }
3318
- return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
3319
- }
3320
- function topologicalOrder(states, edges) {
3321
- const nodes = [...states.keys()];
3322
- const outgoing = new Map;
3323
- const indegree = new Map;
3324
- for (const node of nodes) {
3325
- outgoing.set(node, []);
3326
- indegree.set(node, 0);
3327
- }
3328
- for (const edge of edges) {
3329
- outgoing.get(edge.from)?.push(edge.to);
3330
- indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
3331
- }
3332
- for (const targets of outgoing.values())
3333
- targets.sort((left, right) => left.localeCompare(right));
3334
- const compare = readyCompare(states);
3335
- const ready = nodes.filter((node) => (indegree.get(node) ?? 0) === 0).sort(compare);
3336
- const order = [];
3337
- while (ready.length > 0) {
3338
- const node = ready.shift();
3339
- if (!node)
3340
- break;
3341
- order.push(node);
3342
- for (const target of outgoing.get(node) ?? []) {
3343
- const next = (indegree.get(target) ?? 0) - 1;
3344
- indegree.set(target, next);
3345
- if (next === 0) {
3346
- ready.push(target);
3347
- ready.sort(compare);
3348
- }
3349
- }
3350
- }
3351
- if (order.length === nodes.length)
3352
- return order;
3353
- const cycles = findCycles(nodes, edges);
3354
- const cycleMembers = new Set(cycles.flat());
3355
- const contributors = uniqueSorted(edges.filter((edge) => cycleMembers.has(edge.from) && cycleMembers.has(edge.to)).flatMap((edge) => edge.contributors));
3356
- throw new PipelineUnresolvableError(`Stage pipeline has unresolved cycle: ${cycles.map((cycle) => cycle.join(" -> ")).join("; ")}`, cycles, contributors);
3357
- }
3358
- function composeStageWrappers(state) {
3359
- const wrappers = state.wrappers.toSorted((left, right) => {
3360
- const priorityDelta = (right.wrapper.priority ?? 0) - (left.wrapper.priority ?? 0);
3361
- if (priorityDelta !== 0)
3362
- return priorityDelta;
3363
- return left.contributedBy.localeCompare(right.contributedBy);
3364
- });
3365
- let stage = state.stage;
3366
- for (const wrapper of wrappers.toReversed()) {
3367
- if (wrapper.wrapper.apply)
3368
- stage = wrapper.wrapper.apply(stage);
3369
- }
3370
- return stage;
3371
- }
3372
- function resolveStagePipeline(input) {
3373
- ensureUniqueStageIds(input.defaultStages);
3374
- const mutations = [...input.mutations ?? []];
3375
- assertNoDuplicateMutationTargets(mutations, "insert");
3376
- assertNoDuplicateMutationTargets(mutations, "replace");
3377
- const states = new Map;
3378
- const removedStates = new Map;
3379
- for (const [index, stage] of input.defaultStages.entries()) {
3380
- states.set(stage.id, {
3381
- stage: { ...stage, protected: false },
3382
- before: [...stage.before ?? []],
3383
- after: [...stage.after ?? []],
3384
- baseIndex: index,
3385
- contributedBy: "default",
3386
- wrappers: [],
3387
- droppedAnchors: [],
3388
- isProtected: false
3389
- });
3390
- }
3391
- for (const mutation of mutations.filter((entry) => entry.op === "remove").sort(stableMutationCompare)) {
3392
- const state = states.get(mutation.id);
3393
- const contributor = contributorOf(mutation);
3394
- if (!state)
3395
- continue;
3396
- const removedState = {
3397
- ...state,
3398
- removedBy: contributor
3399
- };
3400
- removedStates.set(mutation.id, removedState);
3401
- states.delete(mutation.id);
3402
- }
3403
- for (const mutation of mutations.filter((entry) => entry.op === "replace").sort(stableMutationCompare)) {
3404
- const state = states.get(mutation.id);
3405
- const contributor = contributorOf(mutation);
3406
- if (!state)
3407
- continue;
3408
- const replacement = { ...mutation.stage, id: mutation.id, protected: false };
3409
- states.set(mutation.id, {
3410
- ...state,
3411
- stage: replacement,
3412
- before: mutation.stage.before ? [...mutation.stage.before] : state.before,
3413
- after: mutation.stage.after ? [...mutation.stage.after] : state.after,
3414
- replacedBy: contributor,
3415
- contributedBy: state.contributedBy,
3416
- isProtected: false
3417
- });
3418
- }
3419
- for (const mutation of mutations.filter((entry) => entry.op === "insert").sort(stableMutationCompare)) {
3420
- const contributor = contributorOf(mutation);
3421
- if (states.has(mutation.stage.id)) {
3422
- throw new PipelineUnresolvableError(`Inserted stage ${mutation.stage.id} conflicts with an existing stage`, [], [contributor]);
3423
- }
3424
- states.set(mutation.stage.id, {
3425
- stage: { ...mutation.stage, protected: false },
3426
- before: [...mutation.stage.before ?? []],
3427
- after: [...mutation.stage.after ?? []],
3428
- baseIndex: null,
3429
- contributedBy: contributor,
3430
- wrappers: [],
3431
- droppedAnchors: [],
3432
- isProtected: false
3433
- });
3434
- }
3435
- for (const mutation of mutations.filter((entry) => entry.op === "reorder").sort(stableMutationCompare)) {
3436
- const state = states.get(mutation.id);
3437
- if (!state)
3438
- continue;
3439
- states.set(mutation.id, {
3440
- ...state,
3441
- before: mergeAnchors(state.before, mutation.before),
3442
- after: mergeAnchors(state.after, mutation.after)
3443
- });
3444
- }
3445
- for (const mutation of mutations.filter((entry) => entry.op === "wrap").sort(stableMutationCompare)) {
3446
- const state = states.get(mutation.id);
3447
- const contributor = contributorOf(mutation);
3448
- const wrapper = wrapperForMutation(mutation);
3449
- if (!state)
3450
- continue;
3451
- states.set(mutation.id, {
3452
- ...state,
3453
- wrappers: [...state.wrappers, { contributedBy: contributor, wrapper }]
3454
- });
3455
- }
3456
- const contributorsForAnchor = (stageId, direction, anchor, state) => {
3457
- const contributors = new Set;
3458
- if (state.replacedBy)
3459
- contributors.add(state.replacedBy);
3460
- for (const mutation of mutations) {
3461
- if (mutation.op === "reorder" && mutation.id === stageId && (direction === "before" ? mutation.before : mutation.after)?.includes(anchor)) {
3462
- contributors.add(contributorOf(mutation));
3463
- }
3464
- if (mutation.op === "insert" && mutation.stage.id === stageId && (direction === "before" ? mutation.stage.before : mutation.stage.after)?.includes(anchor)) {
3465
- contributors.add(contributorOf(mutation));
3466
- }
3467
- }
3468
- if (contributors.size === 0)
3469
- contributors.add(state.contributedBy);
3470
- return uniqueSorted(contributors);
3471
- };
3472
- const edges = [];
3473
- for (const [stageId, state] of states) {
3474
- const before = [];
3475
- const after = [];
3476
- for (const anchor of state.before) {
3477
- if (states.has(anchor))
3478
- before.push(anchor);
3479
- else {
3480
- const removed = removedStates.get(anchor);
3481
- state.droppedAnchors.push({
3482
- stageId,
3483
- anchor,
3484
- direction: "before",
3485
- reason: removed ? "removed" : "missing",
3486
- ...removed?.removedBy ? { removedBy: removed.removedBy } : {}
3487
- });
3488
- }
3489
- }
3490
- for (const anchor of state.after) {
3491
- if (states.has(anchor))
3492
- after.push(anchor);
3493
- else {
3494
- const removed = removedStates.get(anchor);
3495
- state.droppedAnchors.push({
3496
- stageId,
3497
- anchor,
3498
- direction: "after",
3499
- reason: removed ? "removed" : "missing",
3500
- ...removed?.removedBy ? { removedBy: removed.removedBy } : {}
3501
- });
3502
- }
3503
- }
3504
- state.before = uniqueSorted(before);
3505
- state.after = uniqueSorted(after);
3506
- for (const target of state.before)
3507
- edges.push({ from: stageId, to: target, contributors: contributorsForAnchor(stageId, "before", target, state) });
3508
- for (const source of state.after)
3509
- edges.push({ from: source, to: stageId, contributors: contributorsForAnchor(stageId, "after", source, state) });
3510
- }
3511
- const order = topologicalOrder(states, edges);
3512
- const stages = [];
3513
- const record = [];
3514
- for (const id of order) {
3515
- const state = states.get(id);
3516
- if (!state)
3517
- continue;
3518
- stages.push(composeStageWrappers(state));
3519
- const wrappedBy = state.wrappers.toSorted((left, right) => {
3520
- const priorityDelta = (right.wrapper.priority ?? 0) - (left.wrapper.priority ?? 0);
3521
- if (priorityDelta !== 0)
3522
- return priorityDelta;
3523
- return left.contributedBy.localeCompare(right.contributedBy);
3524
- }).map((wrapper) => wrapper.contributedBy);
3525
- record.push({
3526
- stageId: id,
3527
- contributedBy: state.contributedBy,
3528
- ...state.replacedBy ? { replacedBy: state.replacedBy } : {},
3529
- ...wrappedBy.length > 0 ? { wrappedBy } : {},
3530
- ...state.droppedAnchors.length > 0 ? { droppedAnchors: state.droppedAnchors.toSorted((left, right) => left.anchor.localeCompare(right.anchor)) } : {},
3531
- isProtected: state.isProtected
3532
- });
3533
- }
3534
- record.push(...[...removedStates.entries()].toSorted((left, right) => {
3535
- const leftIndex = left[1].baseIndex ?? Number.MAX_SAFE_INTEGER;
3536
- const rightIndex = right[1].baseIndex ?? Number.MAX_SAFE_INTEGER;
3537
- if (leftIndex !== rightIndex)
3538
- return leftIndex - rightIndex;
3539
- return left[0].localeCompare(right[0]);
3540
- }).map(([stageId, state]) => ({
3541
- stageId,
3542
- contributedBy: state.contributedBy,
3543
- ...state.removedBy ? { removedBy: state.removedBy } : {},
3544
- isProtected: state.isProtected
3545
- })));
3546
- return { stages, order, record, cycles: [] };
3547
- }
3548
- // packages/core/src/dependencyGraph.ts
3549
- function uniqueSorted2(values) {
3550
- return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
3551
- }
3552
- function deriveGraphId(tasks) {
3553
- const basis = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right)).join("|");
3554
- let hash = 2166136261;
3555
- for (let index = 0;index < basis.length; index += 1) {
3556
- hash ^= basis.charCodeAt(index);
3557
- hash = Math.imul(hash, 16777619);
3558
- }
3559
- return `graph-${(hash >>> 0).toString(16).padStart(8, "0")}`;
3560
- }
3561
- function dedupeEdges(edges) {
3562
- const byKey = new Map;
3563
- for (const edge of edges) {
3564
- byKey.set(`${edge.type}\x00${edge.fromTaskId}\x00${edge.toTaskId}`, edge);
3565
- }
3566
- return [...byKey.values()].toSorted((left, right) => {
3567
- const typeDelta = left.type.localeCompare(right.type);
3568
- if (typeDelta !== 0)
3569
- return typeDelta;
3570
- const fromDelta = left.fromTaskId.localeCompare(right.fromTaskId);
3571
- if (fromDelta !== 0)
3572
- return fromDelta;
3573
- return left.toTaskId.localeCompare(right.toTaskId);
3574
- });
3575
- }
3576
- function detectBlockingCycles(tasks, edges) {
3577
- const taskIds = tasks.map((task) => String(task.id)).toSorted((left, right) => left.localeCompare(right));
3578
- const adjacency = new Map;
3579
- for (const taskId of taskIds)
3580
- adjacency.set(taskId, []);
3581
- for (const edge of edges) {
3582
- if (edge.type === "blocks")
3583
- adjacency.get(edge.fromTaskId)?.push(edge.toTaskId);
3584
- }
3585
- for (const targets of adjacency.values())
3586
- targets.sort((left, right) => left.localeCompare(right));
3587
- let nextIndex = 0;
3588
- const indexByNode = new Map;
3589
- const lowByNode = new Map;
3590
- const stack = [];
3591
- const onStack = new Set;
3592
- const cycles = [];
3593
- const visit = (node) => {
3594
- indexByNode.set(node, nextIndex);
3595
- lowByNode.set(node, nextIndex);
3596
- nextIndex += 1;
3597
- stack.push(node);
3598
- onStack.add(node);
3599
- for (const target of adjacency.get(node) ?? []) {
3600
- if (!indexByNode.has(target)) {
3601
- visit(target);
3602
- lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, lowByNode.get(target) ?? 0));
3603
- } else if (onStack.has(target)) {
3604
- lowByNode.set(node, Math.min(lowByNode.get(node) ?? 0, indexByNode.get(target) ?? 0));
3605
- }
3606
- }
3607
- if (lowByNode.get(node) !== indexByNode.get(node))
3608
- return;
3609
- const component = [];
3610
- let current;
3611
- do {
3612
- current = stack.pop();
3613
- if (current) {
3614
- onStack.delete(current);
3615
- component.push(current);
3616
- }
3617
- } while (current && current !== node);
3618
- const selfLoop = (adjacency.get(node) ?? []).includes(node);
3619
- if (component.length > 1 || selfLoop)
3620
- cycles.push(component.sort((left, right) => left.localeCompare(right)));
3621
- };
3622
- for (const taskId of taskIds) {
3623
- if (!indexByNode.has(taskId))
3624
- visit(taskId);
3625
- }
3626
- return cycles.sort((left, right) => left.join("\x00").localeCompare(right.join("\x00")));
3627
- }
3628
- function buildDependencyGraphModel(tasks, options = {}) {
3629
- const badges = computeTaskDependencyBadges(tasks);
3630
- const index = buildTaskReferenceIndex(tasks);
3631
- const edges = [];
3632
- const unresolvedRefs = [];
3633
- for (const task of tasks) {
3634
- for (const ref of readTaskMetadataStringList(task, "dependencies")) {
3635
- const dependencyId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
3636
- if (dependencyId)
3637
- edges.push({ fromTaskId: dependencyId, toTaskId: String(task.id), type: "blocks" });
3638
- else
3639
- unresolvedRefs.push(ref);
3640
- }
3641
- for (const ref of readTaskMetadataStringList(task, "parentChildDeps")) {
3642
- const parentId = resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
3643
- if (parentId)
3644
- edges.push({ fromTaskId: parentId, toTaskId: String(task.id), type: "parent-child" });
3645
- else
3646
- unresolvedRefs.push(ref);
3647
- }
3648
- }
3649
- const dedupedEdges = dedupeEdges(edges);
3650
- const nodes = tasks.map((task) => {
3651
- const summary = badges.get(task.id);
3652
- const assignees = readTaskAssigneeLogins(task);
3653
- const groupKey = extractTaskGroupKey(task.title);
3654
- return {
3655
- taskId: String(task.id),
3656
- title: task.title,
3657
- status: task.status,
3658
- priority: task.priority,
3659
- assignee: assignees[0] ?? null,
3660
- blockedBy: summary?.blockedBy ?? [],
3661
- blocks: summary?.blocks ?? [],
3662
- blockingDepth: summary?.blockingDepth ?? 0,
3663
- blockerClass: null,
3664
- actionRiskTier: null,
3665
- epicKey: groupKey,
3666
- groupKey,
3667
- externalId: task.externalId,
3668
- sourceIssueId: task.sourceIssueId ?? null,
3669
- scope: task.scope,
3670
- validationKeys: task.validationKeys
3671
- };
3672
- }).toSorted((left, right) => left.taskId.localeCompare(right.taskId));
3673
- return {
3674
- graphId: options.graphId ?? deriveGraphId(tasks),
3675
- nodes,
3676
- edges: dedupedEdges,
3677
- layout: buildTaskGraphLayout(options.snapshot ?? null, tasks, { showParentChild: options.showParentChild ?? true }),
3678
- cycles: detectBlockingCycles(tasks, dedupedEdges),
3679
- unresolvedRefs: uniqueSorted2(unresolvedRefs),
3680
- degraded: false,
3681
- generatedAt: options.generatedAt ?? new Date().toISOString()
3682
- };
3683
- }
3684
- // packages/core/src/rollups.ts
3685
- import { isOperatorActiveRunStatus } from "@rig/contracts";
3686
- var UNASSIGNED_EPIC = "(unassigned-epic)";
3687
- var UNASSIGNED_ASSIGNEE = "(unassigned)";
3688
- var HUMAN_BLOCKER_CLASSES = {
3689
- "human-decision": true,
3690
- "human-approval": true,
3691
- "external-input": true,
3692
- unknown: true
3693
- };
3694
- function isObjectRecord4(value) {
3695
- return typeof value === "object" && value !== null && !Array.isArray(value);
3696
- }
3697
- function readIssueType2(task) {
3698
- const metadata = isObjectRecord4(task.metadata) ? task.metadata : null;
3699
- const raw = isObjectRecord4(metadata?.raw) ? metadata.raw : null;
3700
- const value = raw?.issueType ?? metadata?.issueType;
3701
- return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : null;
3702
- }
3703
- function isEpicTask(task) {
3704
- return readIssueType2(task) === "epic";
3705
- }
3706
- function isInFlightTaskStatus(status) {
3707
- switch (status) {
3708
- case "queued":
3709
- case "running":
3710
- case "in_progress":
3711
- case "under_review":
3712
- return true;
3713
- default:
3714
- return false;
3715
- }
3716
- }
3717
- function epicKeyForTask(task, tasksById, resolve) {
3718
- const parentRef = readTaskMetadataStringList(task, "parentChildDeps")[0];
3719
- const parentId = parentRef ? resolve(parentRef) : null;
3720
- const parent = parentId ? tasksById.get(parentId) : null;
3721
- if (parent)
3722
- return extractTaskGroupKey(parent.title) ?? parent.title ?? parent.id;
3723
- return extractTaskGroupKey(task.title) ?? UNASSIGNED_EPIC;
3724
- }
3725
- function rollupByEpic(tasks, classifications = new Map) {
3726
- const index = buildTaskReferenceIndex(tasks);
3727
- const badges = computeTaskDependencyBadges(tasks);
3728
- const resolve = (ref) => resolveTaskReference(ref, index.tasksById, index.taskIdByExternalRef, index.taskIdBySourceIssueId);
3729
- const buckets = new Map;
3730
- for (const task of tasks) {
3731
- if (isEpicTask(task))
3732
- continue;
3733
- const epicKey = epicKeyForTask(task, index.tasksById, resolve);
3734
- const current = buckets.get(epicKey) ?? {
3735
- total: 0,
3736
- completed: 0,
3737
- blockedCount: 0,
3738
- humanBlockedCount: 0,
3739
- inFlightCount: 0,
3740
- byStatus: {}
3741
- };
3742
- current.total += 1;
3743
- if (isTaskTerminalStatus(task.status))
3744
- current.completed += 1;
3745
- if (badges.get(task.id)?.blocked === true)
3746
- current.blockedCount += 1;
3747
- if (isInFlightTaskStatus(task.status))
3748
- current.inFlightCount += 1;
3749
- const classification = classifications.get(task.id);
3750
- if (classification && HUMAN_BLOCKER_CLASSES[classification])
3751
- current.humanBlockedCount += 1;
3752
- current.byStatus[task.status] = (current.byStatus[task.status] ?? 0) + 1;
3753
- buckets.set(epicKey, current);
3754
- }
3755
- return [...buckets.entries()].map(([epicKey, bucket]) => ({
3756
- epicKey,
3757
- total: bucket.total,
3758
- percentComplete: bucket.total === 0 ? 0 : Math.round(100 * bucket.completed / bucket.total),
3759
- blockedCount: bucket.blockedCount,
3760
- humanBlockedCount: bucket.humanBlockedCount,
3761
- inFlightCount: bucket.inFlightCount,
3762
- byStatus: Object.fromEntries(Object.entries(bucket.byStatus).sort(([left], [right]) => left.localeCompare(right)))
3763
- })).toSorted((left, right) => left.epicKey.localeCompare(right.epicKey));
3764
- }
3765
- function assigneesForTask(task) {
3766
- const assignees = readTaskAssigneeLogins(task);
3767
- return assignees.length > 0 ? assignees : [UNASSIGNED_ASSIGNEE];
3768
- }
3769
- function rollupByAssignee(tasks, runs) {
3770
- const tasksById = new Map(tasks.map((task) => [String(task.id), task]));
3771
- const badges = computeTaskDependencyBadges(tasks);
3772
- const buckets = new Map;
3773
- const ensureBucket = (assignee) => {
3774
- const existing = buckets.get(assignee);
3775
- if (existing)
3776
- return existing;
3777
- const created = { openTaskCount: 0, inFlightRunIds: new Set, prsAwaitingReview: 0, blockers: new Set };
3778
- buckets.set(assignee, created);
3779
- return created;
3780
- };
3781
- for (const task of tasks) {
3782
- if (isEpicTask(task))
3783
- continue;
3784
- for (const assignee of assigneesForTask(task)) {
3785
- const bucket = ensureBucket(assignee);
3786
- if (!isTaskTerminalStatus(task.status))
3787
- bucket.openTaskCount += 1;
3788
- if (badges.get(task.id)?.blocked === true)
3789
- bucket.blockers.add(task.id);
3790
- if (task.status === "under_review")
3791
- bucket.prsAwaitingReview += 1;
3792
- }
3793
- }
3794
- for (const run of runs) {
3795
- const taskId = run.record.taskId;
3796
- const task = taskId ? tasksById.get(taskId) : null;
3797
- if (!task)
3798
- continue;
3799
- if (isEpicTask(task))
3800
- continue;
3801
- for (const assignee of assigneesForTask(task)) {
3802
- const bucket = ensureBucket(assignee);
3803
- if (run.status && isOperatorActiveRunStatus(run.status))
3804
- bucket.inFlightRunIds.add(run.record.runId ?? `${String(task.id)}:${run.lastSeq}`);
3805
- if (run.status === "reviewing" && run.record.prUrl)
3806
- bucket.prsAwaitingReview += 1;
3807
- }
3808
- }
3809
- return [...buckets.entries()].map(([assignee, bucket]) => ({
3810
- assignee,
3811
- openTaskCount: bucket.openTaskCount,
3812
- inFlightRunCount: bucket.inFlightRunIds.size,
3813
- prsAwaitingReview: bucket.prsAwaitingReview,
3814
- blockers: [...bucket.blockers].toSorted((left, right) => String(left).localeCompare(String(right)))
3815
- })).toSorted((left, right) => left.assignee.localeCompare(right.assignee));
3816
- }
3817
205
 
3818
206
  // packages/core/src/index.ts
3819
207
  var RIG_CORE_PACKAGE = "@rig/core";
3820
208
  export {
3821
- stripTaskCode,
3822
- selectWorkspaces,
3823
- selectWorkspace,
3824
- selectUserInputsForWorkspace,
3825
- selectUserInputsForRun,
3826
- selectTasksGroupedByStatus,
3827
- selectTasksForWorkspace,
3828
- selectTasksByWorkspace,
3829
- selectTasksByStatus,
3830
- selectTasksAssignedToMe,
3831
- selectTasksAssignedTo,
3832
- selectTask,
3833
- selectRunsForWorkspace,
3834
- selectRunsForTask,
3835
- selectRunsByTask,
3836
- selectRun,
3837
- selectRankedReadyTasks,
3838
- selectQueueForWorkspace,
3839
- selectPrimaryWorkspace,
3840
- selectPendingApprovals,
3841
- selectNextReadyTaskByPriority,
3842
- selectGraphsForWorkspace,
3843
- selectApprovalsForWorkspace,
3844
- selectApprovalsForRun,
3845
- selectAdhocRunsForWorkspace,
3846
- selectAdhocRuns,
3847
- scoreTask,
3848
- rollupByEpic,
3849
- rollupByAssignee,
3850
- resolveTaskReference,
3851
- resolveStagePipeline,
3852
- readTaskSourceIssueId,
3853
- readTaskScope,
3854
- readTaskMetadataStringList,
3855
- readTaskDependencyRefs,
3856
- readTaskBlockingDependencyRefs,
3857
- readTaskAssigneeLogins,
3858
- rankTasks,
3859
- rankReadyTasks,
3860
- pruneQueueEntries,
3861
- projectTaskStatusWithSessions,
3862
- projectTaskStatusForGrouping,
3863
- projectRunStatusForTaskGrouping,
3864
- pickDefaultWorkspaceId,
3865
- normalizeTaskAssigneeFilter,
3866
- isTaskTerminalStatus,
3867
- extractTaskGroupKey,
3868
- extractTaskCode,
3869
- disjointScope,
3870
209
  definePlugin,
3871
210
  defineConfig,
3872
211
  createPluginHost,
3873
- computeTaskDependencyBadges,
3874
- computeTaskBlockingDepths,
3875
- buildTaskReferenceIndex,
3876
- buildTaskGraphLayout,
3877
- buildRigInitConfigSource,
3878
- buildDependencyGraphModel,
3879
- applyEngineEvent as applyRigEvent,
3880
- applyEngineEvents,
3881
- applyEngineEvent,
3882
- RIG_CORE_PACKAGE,
3883
- PipelineUnresolvableError
212
+ RIG_CORE_PACKAGE
3884
213
  };