agent-relay-server 0.111.0 → 0.113.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +3 -3
  3. package/public/assets/{MessageBubble-WYqSyKPM.js → MessageBubble-CFujR6_L.js} +3 -3
  4. package/public/assets/{MessageBubble-WYqSyKPM.js.map → MessageBubble-CFujR6_L.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-Dvu17jGe.js → PlanGraphPanel-BfxvqmNS.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-Dvu17jGe.js.map → PlanGraphPanel-BfxvqmNS.js.map} +1 -1
  7. package/public/assets/{activity-wAN_upn7.js → activity-C7-gIF9O.js} +2 -2
  8. package/public/assets/{activity-wAN_upn7.js.map → activity-C7-gIF9O.js.map} +1 -1
  9. package/public/assets/{agent-profiles-D8WLdTCs.js → agent-profiles-BGPa2-BZ.js} +2 -2
  10. package/public/assets/{agent-profiles-D8WLdTCs.js.map → agent-profiles-BGPa2-BZ.js.map} +1 -1
  11. package/public/assets/{agents-C5FajNPw.js → agents-CWCbBuq6.js} +2 -2
  12. package/public/assets/{agents-C5FajNPw.js.map → agents-CWCbBuq6.js.map} +1 -1
  13. package/public/assets/{analytics-Q6Dl_CE9.js → analytics-BhGpT0JR.js} +2 -2
  14. package/public/assets/{analytics-Q6Dl_CE9.js.map → analytics-BhGpT0JR.js.map} +1 -1
  15. package/public/assets/automation-DnpahVqs.js +2 -0
  16. package/public/assets/automation-DnpahVqs.js.map +1 -0
  17. package/public/assets/{branch-state-badge-BRjUWyMT.js → branch-state-badge-CcAq2Igv.js} +2 -2
  18. package/public/assets/{branch-state-badge-BRjUWyMT.js.map → branch-state-badge-CcAq2Igv.js.map} +1 -1
  19. package/public/assets/{channels-DOYB1iiA.js → channels-DBP3-Gg0.js} +2 -2
  20. package/public/assets/{channels-DOYB1iiA.js.map → channels-DBP3-Gg0.js.map} +1 -1
  21. package/public/assets/{chat-aRCk0pLJ.js → chat-Bue-qNx_.js} +2 -2
  22. package/public/assets/{chat-aRCk0pLJ.js.map → chat-Bue-qNx_.js.map} +1 -1
  23. package/public/assets/{connectors-C5CeK-2M.js → connectors-BJxbuxoU.js} +2 -2
  24. package/public/assets/{connectors-C5CeK-2M.js.map → connectors-BJxbuxoU.js.map} +1 -1
  25. package/public/assets/{formatted-body-impl-DZc7C4-2.js → formatted-body-impl-CTz6wch8.js} +2 -2
  26. package/public/assets/formatted-body-impl-CTz6wch8.js.map +1 -0
  27. package/public/assets/{index-DXEYz_JV.js → index-BMAblAKQ.js} +7 -7
  28. package/public/assets/index-BMAblAKQ.js.map +1 -0
  29. package/public/assets/{insights-FzTX2wNh.js → insights-CZ_JrJ8M.js} +2 -2
  30. package/public/assets/{insights-FzTX2wNh.js.map → insights-CZ_JrJ8M.js.map} +1 -1
  31. package/public/assets/{integrations-CztQfrI7.js → integrations-L75xhZd5.js} +2 -2
  32. package/public/assets/{integrations-CztQfrI7.js.map → integrations-L75xhZd5.js.map} +1 -1
  33. package/public/assets/{maintenance-C8Uw8XiI.js → maintenance-Dpb87NaB.js} +2 -2
  34. package/public/assets/{maintenance-C8Uw8XiI.js.map → maintenance-Dpb87NaB.js.map} +1 -1
  35. package/public/assets/{managed-agents-DVbJ2hqh.js → managed-agents-DyGbrKwJ.js} +2 -2
  36. package/public/assets/{managed-agents-DVbJ2hqh.js.map → managed-agents-DyGbrKwJ.js.map} +1 -1
  37. package/public/assets/{markdown-preview-impl-C5D-B-9S.js → markdown-preview-impl-Bx4AcI8G.js} +2 -2
  38. package/public/assets/{markdown-preview-impl-C5D-B-9S.js.map → markdown-preview-impl-Bx4AcI8G.js.map} +1 -1
  39. package/public/assets/{memory-Bp-D_V80.js → memory-2l6G_m1p.js} +2 -2
  40. package/public/assets/{memory-Bp-D_V80.js.map → memory-2l6G_m1p.js.map} +1 -1
  41. package/public/assets/{messages-DE1QSeHI.js → messages-DC-QXNwz.js} +2 -2
  42. package/public/assets/messages-DC-QXNwz.js.map +1 -0
  43. package/public/assets/{orchestrators-B0u2lQfw.js → orchestrators-D_X3a0dm.js} +2 -2
  44. package/public/assets/{orchestrators-B0u2lQfw.js.map → orchestrators-D_X3a0dm.js.map} +1 -1
  45. package/public/assets/{overview-DpaEvvLT.js → overview-76JPEbGZ.js} +2 -2
  46. package/public/assets/{overview-DpaEvvLT.js.map → overview-76JPEbGZ.js.map} +1 -1
  47. package/public/assets/{pairs-DRp_qAu8.js → pairs-IeLXl-Z8.js} +2 -2
  48. package/public/assets/{pairs-DRp_qAu8.js.map → pairs-IeLXl-Z8.js.map} +1 -1
  49. package/public/assets/{projects-cOeuiMjl.js → projects-D8ClXDUg.js} +2 -2
  50. package/public/assets/{projects-cOeuiMjl.js.map → projects-D8ClXDUg.js.map} +1 -1
  51. package/public/assets/{prompt-settings-CNoBiymk.js → prompt-settings-BRduaHOj.js} +2 -2
  52. package/public/assets/{prompt-settings-CNoBiymk.js.map → prompt-settings-BRduaHOj.js.map} +1 -1
  53. package/public/assets/{registry-0A4QFMWC.js → registry-CXyaPToV.js} +2 -2
  54. package/public/assets/{registry-0A4QFMWC.js.map → registry-CXyaPToV.js.map} +1 -1
  55. package/public/assets/{security-C9P6yiQ3.js → security-8n8VEsf5.js} +2 -2
  56. package/public/assets/{security-C9P6yiQ3.js.map → security-8n8VEsf5.js.map} +1 -1
  57. package/public/assets/{select-B1PVUj6D.js → select-D-wcg_2U.js} +2 -2
  58. package/public/assets/{select-B1PVUj6D.js.map → select-D-wcg_2U.js.map} +1 -1
  59. package/public/assets/{settings-TnHlB9-t.js → settings-BhnCGXgo.js} +2 -2
  60. package/public/assets/{settings-TnHlB9-t.js.map → settings-BhnCGXgo.js.map} +1 -1
  61. package/public/assets/{store-DDWg67Pg.js → store-Du8k5mLJ.js} +3 -3
  62. package/public/assets/{store-DDWg67Pg.js.map → store-Du8k5mLJ.js.map} +1 -1
  63. package/public/assets/{tasks-1oK_ggFC.js → tasks-DEKQqgPB.js} +2 -2
  64. package/public/assets/tasks-DEKQqgPB.js.map +1 -0
  65. package/public/assets/{teams-BBb0ZYiF.js → teams-DoRSWZRQ.js} +2 -2
  66. package/public/assets/{teams-BBb0ZYiF.js.map → teams-DoRSWZRQ.js.map} +1 -1
  67. package/public/assets/{terminal-viewer-impl-DzvaRjkp.js → terminal-viewer-impl-C_o3BOpu.js} +2 -2
  68. package/public/assets/{terminal-viewer-impl-DzvaRjkp.js.map → terminal-viewer-impl-C_o3BOpu.js.map} +1 -1
  69. package/public/assets/types-DSwhOFcJ.js.map +1 -1
  70. package/public/assets/{user-avatar-X1pfizLv.js → user-avatar-BQoM4jPT.js} +2 -2
  71. package/public/assets/{user-avatar-X1pfizLv.js.map → user-avatar-BQoM4jPT.js.map} +1 -1
  72. package/public/assets/work-queue-CWnf1_iE.js +2 -0
  73. package/public/assets/work-queue-CWnf1_iE.js.map +1 -0
  74. package/public/assets/{workspaces-BNLP4IlV.js → workspaces-C_fFCoCu.js} +2 -2
  75. package/public/assets/{workspaces-BNLP4IlV.js.map → workspaces-C_fFCoCu.js.map} +1 -1
  76. package/public/index.html +2 -2
  77. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  78. package/runner/plugins/claude/skills/task/SKILL.md +28 -0
  79. package/runner/plugins/codex/skills/task/SKILL.md +28 -0
  80. package/src/agent-lifecycle-events.ts +13 -7
  81. package/src/db/mappers.ts +2 -0
  82. package/src/db/migrations.ts +4 -1
  83. package/src/db/provisioning-registry.ts +148 -2
  84. package/src/db/schema.ts +2 -0
  85. package/src/db/task-entity.ts +37 -12
  86. package/src/db/task-memory.ts +1 -1
  87. package/src/mcp/tools-spawn.ts +3 -0
  88. package/src/mcp-dispatch-tools.ts +2 -2
  89. package/src/mcp-task-tools.ts +11 -4
  90. package/src/prompt-resolver.ts +7 -7
  91. package/src/routes/agents-spawn.ts +8 -1
  92. package/src/routes/tasks.ts +6 -2
  93. package/src/services/plan-graph.ts +1 -1
  94. package/src/services/spawn-agent.ts +82 -35
  95. package/src/services/spawn-registration-watchdog.ts +57 -21
  96. package/src/services/task-context-hydration.ts +1 -1
  97. package/src/services/task-done-when.ts +21 -0
  98. package/src/services/task-entity.ts +10 -1
  99. package/src/services/task-issue-weave.ts +2 -2
  100. package/src/services/task-lifecycle.ts +11 -41
  101. package/src/services/task-semantic-events.ts +4 -4
  102. package/src/token-db.ts +1 -1
  103. package/src/validation.ts +1 -1
  104. package/public/assets/automation-ShdXg0pX.js +0 -2
  105. package/public/assets/automation-ShdXg0pX.js.map +0 -1
  106. package/public/assets/formatted-body-impl-DZc7C4-2.js.map +0 -1
  107. package/public/assets/index-DXEYz_JV.js.map +0 -1
  108. package/public/assets/messages-DE1QSeHI.js.map +0 -1
  109. package/public/assets/tasks-1oK_ggFC.js.map +0 -1
  110. package/public/assets/work-queue-DQMbkSD8.js +0 -2
  111. package/public/assets/work-queue-DQMbkSD8.js.map +0 -1
@@ -41,7 +41,7 @@ import type { ProjectSkillsPolicy } from "../project-skills";
41
41
  import { applyDefaultSharedMcp, type SharedMcpSpawnOverride } from "../shared-mcp";
42
42
  import { autoSeedProjectForCwd } from "./project";
43
43
  import { hydrateTaskContext, projectTaskHydration } from "./task-context-hydration";
44
- import { recordTaskEntityDispatchDecision } from "./task-entity";
44
+ import { createTaskEntity, recordTaskEntityDispatchDecision } from "./task-entity";
45
45
  import { autoSubscribeTaskCoordinator } from "./task-semantic-events";
46
46
  import { dispatchTask } from "../task-dispatcher";
47
47
  import { hasComponentScope, isComponentAuthorizedFor } from "../security";
@@ -138,6 +138,11 @@ export interface SpawnAgentInput {
138
138
  * work-shape (#839). Absent = today's behavior, byte-identical.
139
139
  */
140
140
  taskId?: number;
141
+ /**
142
+ * #867 — explicit escape hatch for utility/standby spawns that carry a prompt but are not
143
+ * dispatching deliverable work. Known utility profiles are also exempt by default.
144
+ */
145
+ taskless?: boolean;
141
146
  /** #838 — the latest one-line steer; the actionable instruction layered over the hydrated Task context. */
142
147
  steer?: string;
143
148
  /**
@@ -218,6 +223,7 @@ export interface SpawnAgentResult {
218
223
  agentId: string | null;
219
224
  registered: boolean;
220
225
  command: Command;
226
+ task?: { id: number; autoScaffolded: boolean; nudge: string };
221
227
  }
222
228
 
223
229
  /** Reject when a component token's signed resource constraints disallow the target (#323). */
@@ -248,6 +254,30 @@ function taskSpawnLabel(task: { id: number; title: string } | undefined, fallbac
248
254
  return label.length <= 120 ? label : `${label.slice(0, 117)}...`;
249
255
  }
250
256
 
257
+ const WORK_DISPATCH_PROFILES = new Set(["default-relay", "lean-worker", "worker"]);
258
+ const TASKLESS_PROFILES = new Set(["coordinator", "isolated-research", "minimal", "ops-assistant", "reviewer", "vanilla"]);
259
+ const AUTO_SCAFFOLD_TASK_NUDGE = "No taskId was provided, so Relay auto-scaffolded a Task from the spawn brief and bound this spawn to it. Pass taskId for intentional task reuse, or taskless:true for utility/standby spawns.";
260
+
261
+ function spawnBrief(input: SpawnAgentInput): string | undefined {
262
+ const candidates = [input.steer, input.prompt];
263
+ for (const value of candidates) {
264
+ const cleaned = value?.trim();
265
+ if (cleaned) return cleaned;
266
+ }
267
+ return undefined;
268
+ }
269
+
270
+ function taskTitleFromBrief(brief: string): string {
271
+ const firstLine = brief.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "Spawned work";
272
+ return firstLine.length <= 500 ? firstLine : `${firstLine.slice(0, 497)}...`;
273
+ }
274
+
275
+ function shouldAutoScaffoldTask(input: SpawnAgentInput, profileName: string): boolean {
276
+ if (input.taskId !== undefined || input.taskless === true) return false;
277
+ if (TASKLESS_PROFILES.has(profileName)) return false;
278
+ return WORK_DISPATCH_PROFILES.has(profileName) && spawnBrief(input) !== undefined;
279
+ }
280
+
251
281
  /**
252
282
  * Spawn a provider agent. Transport-agnostic: every gate + side effect lives here so the
253
283
  * MCP and HTTP paths cannot drift. Throws `McpAuthError` (→403) on a gate denial,
@@ -260,7 +290,9 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
260
290
  // the #221 gate AND the #328/#331/#324 caller-context inheritance — one lookup, reused.
261
291
  const callerId = ctx.callerAgentId;
262
292
  const caller: AgentCard | undefined = (callerId ? getAgent(callerId) : undefined) ?? undefined;
263
- const linkedTask = input.taskId !== undefined ? getTaskDetail(input.taskId) ?? undefined : undefined;
293
+ const profileName = input.profile ?? DEFAULT_SPAWN_PROFILE_NAME;
294
+ let autoScaffoldedTask: SpawnAgentResult["task"] | undefined;
295
+ let linkedTask = input.taskId !== undefined ? getTaskDetail(input.taskId) ?? undefined : undefined;
264
296
  const taskCreatorAgentId = !callerId ? agentIdFromTaskCreator(linkedTask?.createdBy) : undefined;
265
297
  const lineageAgentId = callerId ?? taskCreatorAgentId;
266
298
  const lineageAgent: AgentCard | undefined = (lineageAgentId ? getAgent(lineageAgentId) : undefined) ?? undefined;
@@ -285,6 +317,53 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
285
317
  const inheritedCwd = callerCwd && isPathWithinBase(callerCwd, orchestrator.baseDir) ? callerCwd : undefined;
286
318
  const resolvedCwd = requestedCwd || inheritedCwd || orchestrator.baseDir;
287
319
 
320
+ // Resolve + validate the named agent profile server-side (was HTTP-only; the MCP path passed
321
+ // the name through unvalidated). Omitted profile uses Relay's spawn default so coordinator
322
+ // spawns still get the bundled Relay context/affordances. Explicit profile always wins.
323
+ let agentProfile = withResolvedProvisioning(getAgentProfile(profileName)?.value, input.provider);
324
+ if (!agentProfile) throw new McpNotFoundError(input.profile ? "agent profile not found" : `default agent profile ${DEFAULT_SPAWN_PROFILE_NAME} not found`);
325
+
326
+ // --- THE GATE (uniform across transports) ---------------------------------
327
+ // Coarse capability: spawning requires command:spawn. The MCP dispatcher already enforced
328
+ // this; the HTTP route did not — asserting here makes it transport-invariant.
329
+ if (!canSpawn(ctx)) {
330
+ throw new McpAuthError(`spawn requires the ${SPAWN_SCOPE} scope`);
331
+ }
332
+ // #221 — a real spawning agent: no grandchildren, and within its maxSpawnedAgents quota.
333
+ // Server/admin/dashboard tokens carry no caller identity → unrestricted by design.
334
+ if (callerId) {
335
+ if (caller?.spawnedBy) {
336
+ throw new McpAuthError("spawned agents cannot spawn further agents (no grandchildren)");
337
+ }
338
+ const quota = ctx.constraints?.maxSpawnedAgents ?? 0;
339
+ const live = countLiveSpawnedAgents(callerId);
340
+ if (live >= quota) {
341
+ throw new ValidationError(`spawn quota reached (${live}/${quota} live children) — shut one down or wait for one to exit`);
342
+ }
343
+ }
344
+ // Gate the child spawn on the token's legit bounds: the target orchestrator AND the spawn cwd
345
+ // (`cwdPrefixes`/`cwd` constraint). Excludes the #323 footguns (spawnRequestIds/policies) — those
346
+ // are self-scoping per-spawn ids the CHILD gets fresh, so gating on them makes the quota
347
+ // unreachable. The cwd bound was enforced by the old HTTP route (resource.cwd) but NOT by the MCP
348
+ // path; unifying here converges UP to the strict union — a scoped dashboard token still can't spawn
349
+ // outside its cwd prefix, and MCP now honors the same bound.
350
+ assertResourceAllowed(ctx, { scope: "agent:write", resource: { orchestratorId: orchestrator.id, cwd: resolvedCwd } });
351
+
352
+ if (shouldAutoScaffoldTask(input, profileName)) {
353
+ const brief = spawnBrief(input)!;
354
+ const taskRequestedBy = callerId ?? input.requestedBy ?? ctx.actor.id;
355
+ linkedTask = createTaskEntity({
356
+ title: taskTitleFromBrief(brief),
357
+ intent: brief,
358
+ latestSteer: input.steer,
359
+ createdBy: taskRequestedBy,
360
+ source: "spawn",
361
+ target: taskRequestedBy,
362
+ });
363
+ input = { ...input, taskId: linkedTask.id };
364
+ autoScaffoldedTask = { id: linkedTask.id, autoScaffolded: true, nudge: AUTO_SCAFFOLD_TASK_NUDGE };
365
+ }
366
+
288
367
  // #838 — spawn-with-task hydration. When a taskId is given, relay assembles the worker's
289
368
  // first-turn context FROM the durable Task (intent + linked-issue spec/acceptance + recorded
290
369
  // decisions + history + latest steer), resolved server-side ONCE, and projects it onto the
@@ -346,13 +425,6 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
346
425
 
347
426
  const spawnRequestId = input.spawnRequestId ?? generateSpawnRequestId();
348
427
 
349
- // Resolve + validate the named agent profile server-side (was HTTP-only; the MCP path passed
350
- // the name through unvalidated). Omitted profile uses Relay's spawn default so coordinator
351
- // spawns still get the bundled Relay context/affordances. Explicit profile always wins.
352
- const profileName = input.profile ?? DEFAULT_SPAWN_PROFILE_NAME;
353
- let agentProfile = withResolvedProvisioning(getAgentProfile(profileName)?.value, input.provider);
354
- if (!agentProfile) throw new McpNotFoundError(input.profile ? "agent profile not found" : `default agent profile ${DEFAULT_SPAWN_PROFILE_NAME} not found`);
355
-
356
428
  // #667 — project `.mcp.json` projection: a per-spawn `projectMcp` overrides the profile
357
429
  // default. The runner does the filesystem read + injection from `agentProfile.projectMcp`
358
430
  // (it owns the spawn cwd), so thread the resolved decision onto the in-flight profile copy
@@ -368,32 +440,6 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
368
440
  // Explicit opt-in still works for zero-host profiles.
369
441
  agentProfile = applyDefaultSharedMcp(agentProfile, input.sharedMcp);
370
442
 
371
- // --- THE GATE (uniform across transports) ---------------------------------
372
- // Coarse capability: spawning requires command:spawn. The MCP dispatcher already enforced
373
- // this; the HTTP route did not — asserting here makes it transport-invariant.
374
- if (!canSpawn(ctx)) {
375
- throw new McpAuthError(`spawn requires the ${SPAWN_SCOPE} scope`);
376
- }
377
- // #221 — a real spawning agent: no grandchildren, and within its maxSpawnedAgents quota.
378
- // Server/admin/dashboard tokens carry no caller identity → unrestricted by design.
379
- if (callerId) {
380
- if (caller?.spawnedBy) {
381
- throw new McpAuthError("spawned agents cannot spawn further agents (no grandchildren)");
382
- }
383
- const quota = ctx.constraints?.maxSpawnedAgents ?? 0;
384
- const live = countLiveSpawnedAgents(callerId);
385
- if (live >= quota) {
386
- throw new ValidationError(`spawn quota reached (${live}/${quota} live children) — shut one down or wait for one to exit`);
387
- }
388
- }
389
- // Gate the child spawn on the token's legit bounds: the target orchestrator AND the spawn cwd
390
- // (`cwdPrefixes`/`cwd` constraint). Excludes the #323 footguns (spawnRequestIds/policies) — those
391
- // are self-scoping per-spawn ids the CHILD gets fresh, so gating on them makes the quota
392
- // unreachable. The cwd bound was enforced by the old HTTP route (resource.cwd) but NOT by the MCP
393
- // path; unifying here converges UP to the strict union — a scoped dashboard token still can't spawn
394
- // outside its cwd prefix, and MCP now honors the same bound.
395
- assertResourceAllowed(ctx, { scope: "agent:write", resource: { orchestratorId: orchestrator.id, cwd: resolvedCwd } });
396
-
397
443
  // #635 — validate and enrich resumeWorkspace before dispatching to the orchestrator.
398
444
  // The relay has the workspace DB records; the orchestrator has disk access. We do the
399
445
  // "is this a real managed workspace and is it free?" check here, the orchestrator
@@ -606,6 +652,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
606
652
  agentId,
607
653
  registered: agentId !== null,
608
654
  command,
655
+ ...(autoScaffoldedTask ? { task: autoScaffoldedTask } : {}),
609
656
  };
610
657
  }
611
658
 
@@ -9,7 +9,15 @@ interface SpawnCommandRow {
9
9
  id: string;
10
10
  params: string;
11
11
  correlation_id: string | null;
12
- created_at: number;
12
+ updated_at: number;
13
+ }
14
+
15
+ interface SpawnCommandGroup {
16
+ spawnRequestId: string;
17
+ parent: string;
18
+ provider?: string;
19
+ latestUpdatedAt: number;
20
+ rows: SpawnCommandRow[];
13
21
  }
14
22
 
15
23
  interface SpawnRegistrationWatchdogResult extends Record<string, unknown> {
@@ -26,45 +34,73 @@ export function reapUnregisteredSpawnCommands(
26
34
  const cutoff = now - deadlineMs;
27
35
  const notifyCutoff = now - notifyMaxAgeMs;
28
36
  const rows = getDb().query(`
29
- SELECT id, params, correlation_id, created_at
37
+ SELECT id, params, correlation_id, updated_at
30
38
  FROM commands
31
39
  WHERE type = 'agent.spawn'
32
40
  AND status = 'succeeded'
33
- AND created_at <= ?
34
- ORDER BY created_at ASC
35
- `).all(cutoff) as SpawnCommandRow[];
41
+ ORDER BY updated_at ASC
42
+ `).all() as SpawnCommandRow[];
36
43
 
37
44
  const failedSpawnCommandIds: string[] = [];
38
45
  const notifiedSpawnCommandIds: string[] = [];
39
46
  const silencedSpawnCommandIds: string[] = [];
40
- for (const row of rows) {
41
- const params = parseParams(row.params);
42
- const spawnRequestId = stringParam(params, "spawnRequestId") ?? row.correlation_id ?? undefined;
43
- const parent = stringParam(params, "spawnedBy");
44
- if (!spawnRequestId || !parent) continue;
45
- if (hasEverRegisteredAgent(spawnRequestId)) continue;
47
+ for (const group of groupSpawnCommands(rows)) {
48
+ if (group.latestUpdatedAt > cutoff) continue;
49
+ if (hasEverRegisteredAgent(group.spawnRequestId)) continue;
46
50
 
47
51
  const reason = `spawned agent did not register within ${deadlineMs}ms`;
48
- const failed = updateCommand(row.id, { status: "failed", error: reason });
49
- if (!failed || failed.status !== "failed") continue;
50
- failedSpawnCommandIds.push(failed.id);
51
- if (row.created_at >= notifyCutoff) {
52
- notifiedSpawnCommandIds.push(failed.id);
52
+ const failedCommands = group.rows
53
+ .map((row) => updateCommand(row.id, { status: "failed", error: reason }))
54
+ .filter((command): command is NonNullable<typeof command> => Boolean(command && command.status === "failed"));
55
+ if (failedCommands.length === 0) continue;
56
+ failedSpawnCommandIds.push(...failedCommands.map((command) => command.id));
57
+ if (group.latestUpdatedAt >= notifyCutoff) {
58
+ notifiedSpawnCommandIds.push(failedCommands[failedCommands.length - 1]!.id);
53
59
  notifyAgentSpawnFailed({
54
- parent,
55
- spawnRequestId,
56
- provider: stringParam(params, "provider"),
60
+ parent: group.parent,
61
+ spawnRequestId: group.spawnRequestId,
62
+ provider: group.provider,
57
63
  reason,
58
64
  });
59
65
  } else {
60
- silencedSpawnCommandIds.push(failed.id);
66
+ silencedSpawnCommandIds.push(...failedCommands.map((command) => command.id));
67
+ }
68
+ for (const failed of failedCommands) {
69
+ emitCommandEvent(failed, "command.failed");
61
70
  }
62
- emitCommandEvent(failed, "command.failed");
63
71
  }
64
72
 
65
73
  return { failedSpawnCommandIds, notifiedSpawnCommandIds, silencedSpawnCommandIds };
66
74
  }
67
75
 
76
+ function groupSpawnCommands(rows: SpawnCommandRow[]): SpawnCommandGroup[] {
77
+ const groups = new Map<string, SpawnCommandGroup>();
78
+ for (const row of rows) {
79
+ const params = parseParams(row.params);
80
+ const spawnRequestId = stringParam(params, "spawnRequestId") ?? row.correlation_id ?? undefined;
81
+ const parent = stringParam(params, "spawnedBy");
82
+ if (!spawnRequestId || !parent) continue;
83
+ const existing = groups.get(spawnRequestId);
84
+ if (existing) {
85
+ existing.rows.push(row);
86
+ if (row.updated_at >= existing.latestUpdatedAt) {
87
+ existing.parent = parent;
88
+ existing.provider = stringParam(params, "provider");
89
+ existing.latestUpdatedAt = row.updated_at;
90
+ }
91
+ continue;
92
+ }
93
+ groups.set(spawnRequestId, {
94
+ spawnRequestId,
95
+ parent,
96
+ provider: stringParam(params, "provider"),
97
+ latestUpdatedAt: row.updated_at,
98
+ rows: [row],
99
+ });
100
+ }
101
+ return [...groups.values()];
102
+ }
103
+
68
104
  function hasEverRegisteredAgent(spawnRequestId: string): boolean {
69
105
  const row = getDb().query(`
70
106
  SELECT 1
@@ -170,7 +170,7 @@ function hasTaskMemory(envelope: TaskMemoryEnvelope): boolean {
170
170
  function renderTaskMemory(envelope: TaskMemoryEnvelope): string | undefined {
171
171
  if (!hasTaskMemory(envelope)) return undefined;
172
172
  const rendered = renderContinuation(boundedTaskMemoryEnvelope(envelope), {
173
- intro: `Task-bound memory — generation ${envelope.generation}. This is Relay-owned memory bound to Task #${envelope.taskId}; carry these dead ends, decisions, and working state forward.`,
173
+ intro: `Task-bound memory — generation ${envelope.generation}. This is Relay-owned memory bound to Task T${envelope.taskId}; carry these dead ends, decisions, and working state forward.`,
174
174
  });
175
175
  return truncateTaskMemorySection(rendered, TASK_MEMORY_SECTION_LIMIT);
176
176
  }
@@ -0,0 +1,21 @@
1
+ import { TASK_DELIVERABLE_STAGES } from "agent-relay-sdk";
2
+ import type { TaskDeliverableStage } from "../types";
3
+
4
+ const STAGE_INDEX = new Map<string, number>(TASK_DELIVERABLE_STAGES.map((stage, i) => [stage, i]));
5
+
6
+ // `doneWhen` is free text, but these lifecycle tokens have durable semantics.
7
+ export function doneWhenTargetStage(doneWhen: string | undefined): TaskDeliverableStage {
8
+ const value = doneWhen?.trim().toLowerCase();
9
+ if (!value) return "landed-on-main";
10
+ if (value === "deliverable-attached" || value.includes("deliverable attached")) return "committed-on-branch";
11
+ if (value === "landed" || value === "landed-on-main" || value.includes("landed")) return "landed-on-main";
12
+ if (value.includes("deploying")) return "deploying";
13
+ if (value.includes("deploy")) return "deployed";
14
+ return "landed-on-main";
15
+ }
16
+
17
+ export function stageSatisfiesDoneWhen(stage: TaskDeliverableStage, doneWhen: string | undefined): boolean {
18
+ const current = STAGE_INDEX.get(stage) ?? 0;
19
+ const target = STAGE_INDEX.get(doneWhenTargetStage(doneWhen)) ?? STAGE_INDEX.get("landed-on-main")!;
20
+ return current >= target;
21
+ }
@@ -30,6 +30,7 @@ import {
30
30
  import { emitTaskChanged } from "../sse";
31
31
  import type { Task, TaskDetail, TaskEvent, TaskGateState } from "../types";
32
32
  import { removeSubscriptionsForTask } from "../db/notification-subscriptions";
33
+ import { stageSatisfiesDoneWhen } from "./task-done-when";
33
34
  import { autoSubscribeTaskCoordinator, emitTaskSemanticNotification, taskTransitionForStage, taskTransitionForStatus } from "./task-semantic-events";
34
35
 
35
36
  export function createTaskEntity(input: CreateDeliverableTaskInput): TaskDetail {
@@ -82,7 +83,7 @@ function isTerminalTaskStatus(status: Task["status"]): boolean {
82
83
 
83
84
  export function setTaskEntityStage(id: number, input: SetTaskStageInput): TaskDetail {
84
85
  const before = getTaskDetail(id);
85
- const detail = setTaskStage(id, input);
86
+ let detail = setTaskStage(id, input);
86
87
  emitTaskChanged(detail, "task.stage");
87
88
  const transition = taskTransitionForStage(detail.deliverableStage);
88
89
  if (input.actor !== "task-lifecycle" && transition && before?.deliverableStage !== detail.deliverableStage) {
@@ -94,6 +95,14 @@ export function setTaskEntityStage(id: number, input: SetTaskStageInput): TaskDe
94
95
  occurrenceKey: latestTaskEventOccurrence(detail.id, `stage:${detail.deliverableStage}`),
95
96
  });
96
97
  }
98
+ const deliverableStage = detail.deliverableStage ?? "none";
99
+ if (detail.status !== "done" && stageSatisfiesDoneWhen(deliverableStage, detail.doneWhen)) {
100
+ detail = setTaskEntityStatus(id, {
101
+ status: "done",
102
+ note: `doneWhen reached at stage ${deliverableStage}`,
103
+ actor: input.actor,
104
+ });
105
+ }
97
106
  return detail;
98
107
  }
99
108
 
@@ -112,8 +112,8 @@ interface TaskCommentPayload {
112
112
  }
113
113
 
114
114
  const HEADINGS: Record<TaskCommentKind, (p: TaskCommentPayload) => string> = {
115
- discussion: (p) => `💬 **Comment on Task #${p.taskId}**${p.author ? ` — _${p.author}_` : ""}`,
116
- milestone: (p) => `🏁 **Task #${p.taskId} — ${p.milestone ?? "milestone"}**`,
115
+ discussion: (p) => `💬 **Comment on T${p.taskId}**${p.author ? ` — _${p.author}_` : ""}`,
116
+ milestone: (p) => `🏁 **T${p.taskId} — ${p.milestone ?? "milestone"}**`,
117
117
  };
118
118
 
119
119
  // The payload is base64url-encoded inside the marker, NOT raw JSON: a user/route-supplied
@@ -21,7 +21,7 @@
21
21
  // owns execution telemetry. Issue CLOSE is the canonical #482/#474 path (which runs just before
22
22
  // this in `notifyBranchLanded`) — we do NOT re-close; we PROJECT it via #833's `taskCloseBridge`.
23
23
  // The one write-through we make is the single milestone summary (#833's milestone exception).
24
- import { errMessage, TASK_DELIVERABLE_STAGES } from "agent-relay-sdk";
24
+ import { errMessage } from "agent-relay-sdk";
25
25
  import {
26
26
  deliverableTaskIdsByBranch,
27
27
  deliverableTaskIdsByOwnerAgent,
@@ -30,12 +30,13 @@ import {
30
30
  getTaskDetail,
31
31
  markTaskAgentReaped,
32
32
  } from "../db";
33
- import { setTaskEntityStage, setTaskEntityStatus } from "./task-entity";
33
+ import { setTaskEntityStage } from "./task-entity";
34
34
  import { postTaskMilestoneComment, taskCloseBridge, type TaskCloseBridgeEntry } from "./task-issue-weave";
35
35
  import { requestTransientAgentShutdown } from "./transient-agent-reaper";
36
36
  import { emitRelayEvent } from "../events";
37
37
  import { routeNotification } from "../notification-router";
38
38
  import { resolveCoordinatorAgentId } from "../steward-identity";
39
+ import { stageSatisfiesDoneWhen } from "./task-done-when";
39
40
  import { emitTaskSemanticNotification, type TaskSemanticTransition } from "./task-semantic-events";
40
41
  import type { IssueRef, TaskDeliverableStage, TaskGateState, WorkspaceRecord } from "../types";
41
42
  import type { IssueTrackerAdapter } from "../issue-tracker";
@@ -120,33 +121,12 @@ function landedDiffSummary(input: LandedTaskLifecycleInput, log: LogFn): string
120
121
  }
121
122
  }
122
123
 
123
- // --- doneWhen → deliverable-stage mapping -------------------------------------------------------
124
- const STAGE_INDEX = new Map<string, number>(TASK_DELIVERABLE_STAGES.map((stage, i) => [stage, i]));
125
-
126
- // Which deliverable stage satisfies a Task's `doneWhen`. `doneWhen` is free text (#834): we map
127
- // the small known vocabulary (`landed` | `deployed` | `deliverable-attached`, with synonyms) and
128
- // default an UNSET doneWhen to `landed-on-main` — the common case where a Task is done when it
129
- // lands. An unrecognized value falls back to the landed default rather than blocking forever.
130
- function doneWhenTargetStage(doneWhen: string | undefined): TaskDeliverableStage {
131
- const value = doneWhen?.trim().toLowerCase();
132
- if (!value) return "landed-on-main";
133
- if (value.includes("deploying")) return "deploying";
134
- if (value.includes("deploy")) return "deployed";
135
- return "landed-on-main";
136
- }
137
-
138
- function stageSatisfiesDoneWhen(stage: TaskDeliverableStage, doneWhen: string | undefined): boolean {
139
- const current = STAGE_INDEX.get(stage) ?? 0;
140
- const target = STAGE_INDEX.get(doneWhenTargetStage(doneWhen)) ?? STAGE_INDEX.get("landed-on-main")!;
141
- return current >= target;
142
- }
143
-
144
124
  // --- gate verdict ------------------------------------------------------------------------------
145
- // "Gates passed" is the signal that frees the agent (reap) AND closes the Task (doneWhen). The
125
+ // "Gates passed" is the signal that frees the agent (reap). The
146
126
  // merge landing green is necessary but NOT sufficient (Codex #831 review): a worker that crashed
147
127
  // before reporting gates, or reported only a subset, must NOT read as green on a vacuous `[]`.
148
128
  // So a pass REQUIRES the expected gates present-and-passing AND no recorded gate failing. An
149
- // empty / missing / partial / failed ledger is NOT passed → no reap, no close, no green report.
129
+ // empty / missing / partial / failed ledger is NOT passed → no reap, no green report.
150
130
  export const EXPECTED_GATES = ["typecheck", "test"] as const;
151
131
 
152
132
  interface GateVerdict {
@@ -310,20 +290,10 @@ async function driveTaskOnLand(
310
290
  const gatesPassed = gateVerdict.passed;
311
291
 
312
292
  // 3 (doneWhen axis) — reaching the Task's doneWhen target CLOSES the Task (execution status →
313
- // done) but ONLY with verified gates (Codex #831): a failed/incomplete gate keeps the Task
314
- // open, never `done`, even at/after the doneWhen stage. Otherwise it stays where it was ("landed
315
- // but not done", e.g. doneWhen=deployed waits on deploy). Idempotent: an already-done Task isn't
316
- // re-written.
293
+ // done). Gates are intentionally separate: they guard reaping and green reporting, not the
294
+ // execution status completion predicate. Otherwise empty gates can strand a landed Task forever.
317
295
  const doneWhenSatisfied = stageSatisfiesDoneWhen("landed-on-main", before.doneWhen);
318
- let statusAdvanced = false;
319
- if (doneWhenSatisfied && gatesPassed && before.status !== "done") {
320
- detail = setTaskEntityStatus(taskId, {
321
- status: "done",
322
- note: `doneWhen reached on land (${input.mergedSha?.slice(0, 12) ?? input.workspace.branch ?? "merged"})`,
323
- actor: "task-lifecycle",
324
- });
325
- statusAdvanced = true;
326
- }
296
+ const statusAdvanced = before.status !== "done" && detail.status === "done";
327
297
 
328
298
  // 3b (close projection) — the linked issue is closed by the canonical #482/#474 path (which ran
329
299
  // immediately before this in notifyBranchLanded). We PROJECT that ledger via #833, never re-close.
@@ -341,7 +311,7 @@ async function driveTaskOnLand(
341
311
  const summaryBits = [`Landed to \`${input.workspace.baseRef ?? "main"}\``];
342
312
  if (diffSummary) summaryBits.push(diffSummary);
343
313
  if (!gatesPassed) summaryBits.push(`gates not verified (${gateVerdict.reason ?? "incomplete"})`);
344
- summaryBits.push(statusAdvanced ? "Task done (doneWhen reached)" : `Task at \`landed-on-main\` (doneWhen \`${before.doneWhen ?? "landed"}\` not yet reached)`);
314
+ summaryBits.push(doneWhenSatisfied ? "Task done (doneWhen reached)" : `Task at \`landed-on-main\` (doneWhen \`${before.doneWhen ?? "landed"}\` not yet reached)`);
345
315
  const milestone = await postTaskMilestoneComment(
346
316
  {
347
317
  taskId,
@@ -440,8 +410,8 @@ function emitDeliverableErrorReceipt(input: LandedTaskLifecycleInput, outcome: T
440
410
  recipients: [{ agentId: coordinator, matchedRule: "task-stakeholder" }],
441
411
  ttlMs: null,
442
412
  message: {
443
- subject: `Task #${outcome.taskId} land lifecycle failed`,
444
- body: `Task #${outcome.taskId} landed, but Relay could not complete the task lifecycle automation: ${outcome.error}`,
413
+ subject: `Task T${outcome.taskId} land lifecycle failed`,
414
+ body: `Task T${outcome.taskId} landed, but Relay could not complete the task lifecycle automation: ${outcome.error}`,
445
415
  payload: {
446
416
  kind: "task.lifecycle-error",
447
417
  notification: "task-lifecycle-error",
@@ -271,7 +271,7 @@ function compactStatusPulseItems(items: StatusPulseDigestItem[]): StatusPulseDig
271
271
  function taskStatusPulseBody(items: StatusPulseDigestItem[]): string {
272
272
  const lines = ["Task status pulse:"];
273
273
  for (const item of items) {
274
- const bits = [`#${item.taskId}`, item.transition, item.title];
274
+ const bits = [`T${item.taskId}`, item.transition, item.title];
275
275
  const issue = issueLabel(item.issue);
276
276
  if (issue) bits.push(issue);
277
277
  if (item.branch) bits.push(`branch ${item.branch}`);
@@ -303,13 +303,13 @@ function taskNotificationScope(input: TaskSemanticNotificationInput): Notificati
303
303
  }
304
304
 
305
305
  function taskNotificationSubject(input: TaskSemanticNotificationInput): string {
306
- return `Task #${input.task.id} ${input.transition}`;
306
+ return `Task T${input.task.id} ${input.transition}`;
307
307
  }
308
308
 
309
309
  function taskNotificationBody(input: TaskSemanticNotificationInput): string {
310
310
  const issue = issueLabel(primaryIssue(input.task));
311
311
  if (input.transition === "landed-on-main") {
312
- const bits = [`Task #${input.task.id} landed on main: ${input.task.title}`];
312
+ const bits = [`Task T${input.task.id} landed on main: ${input.task.title}`];
313
313
  if (input.sha) bits.push(`sha ${input.sha.slice(0, 12)}`);
314
314
  const gatesPassed = input.outcome?.gatesPassed;
315
315
  if (typeof gatesPassed === "boolean") bits.push(gatesPassed ? "gates passed" : "gates not verified");
@@ -318,7 +318,7 @@ function taskNotificationBody(input: TaskSemanticNotificationInput): string {
318
318
  return bits.join("; ");
319
319
  }
320
320
  const needed = taskNotificationNeeded(input);
321
- return `Task #${input.task.id} ${input.transition}: ${input.task.title}${issue ? ` (${issue})` : ""}${needed ? `\nNeeded: ${needed}` : ""}`;
321
+ return `Task T${input.task.id} ${input.transition}: ${input.task.title}${issue ? ` (${issue})` : ""}${needed ? `\nNeeded: ${needed}` : ""}`;
322
322
  }
323
323
 
324
324
  function taskNotificationNeeded(input: TaskSemanticNotificationInput): string {
package/src/token-db.ts CHANGED
@@ -67,7 +67,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
67
67
  name: "Orchestrator Host",
68
68
  description: "Host supervisor access for spawn, command, log, and terminal operations.",
69
69
  role: "orchestrator",
70
- scope: ["agent:read", "agent:write", "command:read", "command:write", "task:read", "task:write", "logs:read", "terminal:attach", "quota:write"],
70
+ scope: ["agent:read", "agent:write", "command:read", "command:write", "task:read", "task:write", "logs:read", "terminal:attach", "quota:write", "provisioning:read"],
71
71
  constraints: { logsRead: true, terminalAttach: true },
72
72
  ttlSeconds: 30 * 24 * 60 * 60,
73
73
  builtIn: true,
package/src/validation.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ValidationError } from "./db";
1
+ import { ValidationError } from "./db/connection.ts";
2
2
  import { type TokenConstraints, type WorkspaceAutoMergePolicy, type WorkspaceLandingPolicy, type WorkspaceMetadata, type WorkspaceMode, type WorkspaceProbe, type WorkspaceStatus } from "./types";
3
3
  import { AUTO_MERGE_POLICIES, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
4
4
 
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{$n as t,An as n,G as r,Ht as i,N as a,U as o,V as s,fr as c,g as l,j as u}from"./lucide-react-DmbfpRvJ.js";import{c as d,f,l as p,s as m,t as h}from"./store-DDWg67Pg.js";import{p as g}from"./types-DSwhOFcJ.js";import{K as _,h as v}from"./display-_5kFrHJh.js";import{t as y}from"./badge-kdQDWO6e.js";import{t as b}from"./button-BKFvNXa3.js";import{t as x}from"./input-DfN6sUbX.js";import{V as S,b as C,c as w,l as T}from"./index-DXEYz_JV.js";import{o as E,r as D,s as O,t as k}from"./card-B0y1i_2D.js";var A=e(c(),1),j=t(),M={scheduled:`bg-blue-500/10 text-blue-400 border-blue-500/20`,dispatching:`bg-sky-500/10 text-sky-400 border-sky-500/20`,waiting_agent:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,running:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,succeeded:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,failed:`bg-red-500/10 text-red-400 border-red-500/20`,canceled:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,timed_out:`bg-red-500/10 text-red-400 border-red-500/20`},N=new Set([`scheduled`,`dispatching`,`waiting_agent`,`running`]);function P(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`budget elapsed`:e.status}function F(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`:M[e.status]||``}function I(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?void 0:e.error}function L(e){return N.has(e.status)}function R(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function z(e){return typeof e==`string`&&e.length>0?e:void 0}function B(e){let t=Math.max(0,Math.round(e/1e3)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60;return n?`${n}h ${r}m`:r?`${r}m ${i}s`:`${i}s`}function V(e){return R(e.meta?.runtimeBudgetDeadlineAt)??R(e.result?.deadlineAt)}function H(e,t){if(!e.startedAt)return`-`;let n=e.finishedAt||(L(e)?t:e.updatedAt);return B(Number(n)-Number(e.startedAt))}function U(e,t){let n=V(e);if(!(!n||!L(e)))return B(n-t)}function W(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`runtime budget elapsed`:e.error}function G(e,t,n){let r=e.targetAgentId||e.spawnedAgentId||z(e.meta?.runtimeAgentId),i=r?t.find(e=>e.id===r):void 0,a=r?n.flatMap(e=>e.managedAgents.map(t=>({orchestrator:e,managedAgent:t}))).find(({managedAgent:e})=>e.agentId===r):void 0,o=z(e.meta?.terminalSession)||z(e.meta?.tmuxSession)||z(e.meta?.sessionName)||z(i?.meta?.terminalSession)||z(i?.meta?.tmuxSession)||z(i?.meta?.sessionName)||a?.managedAgent.terminalSession||a?.managedAgent.tmuxSession||a?.managedAgent.sessionName,s=z(e.meta?.orchestratorId)||a?.orchestrator.id||e.orchestratorId;return o?{orchestratorId:s,session:o}:void 0}function K(e=``,t){let n=p(t);return{name:``,description:``,enabled:!0,schedule:`0 8 * * *`,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`,orchestratorId:e,targetMode:`existing_agent`,provider:n,model:d(n),effort:``,profile:`default-relay`,label:``,tags:``,capabilities:``,ifNoMatch:`fail`,cwd:``,approvalMode:`guarded`,workspaceMode:`inherit`,keepAlive:!1,budgetMinutes:``,warningMinutes:``,warningMessage:``,concurrencyPolicy:`skip`,title:``,body:``,severity:`info`,dedupeKey:``,externalUrl:``}}function q(e){let t=e.split(`,`).map(e=>e.trim()).filter(Boolean);return t.length?t:void 0}function J(e){let t=e.targetPolicy;return{...K(e.orchestratorId),id:e.id,name:e.name,description:e.description||``,enabled:e.enabled,schedule:e.schedule,timezone:e.timezone,orchestratorId:e.orchestratorId,targetMode:t.mode,provider:t.mode===`on_demand_agent`?t.provider:t.selector.provider||g()[0]||``,model:t.mode===`on_demand_agent`?t.model||d(t.provider):``,effort:t.mode===`on_demand_agent`&&t.effort||``,profile:t.mode===`on_demand_agent`&&t.profile||`default-relay`,label:t.mode===`existing_agent`&&t.selector.label||``,tags:t.mode===`existing_agent`?(t.selector.tags||[]).join(`, `):``,capabilities:t.mode===`existing_agent`?(t.selector.capabilities||[]).join(`, `):``,ifNoMatch:t.mode===`existing_agent`&&t.ifNoMatch||`fail`,cwd:t.mode===`on_demand_agent`&&t.cwd||``,approvalMode:t.mode===`on_demand_agent`&&t.approvalMode||`guarded`,workspaceMode:t.mode===`on_demand_agent`&&t.workspaceMode||`inherit`,keepAlive:t.mode===`on_demand_agent`?!!t.keepAlive:!1,budgetMinutes:t.mode===`on_demand_agent`&&t.runtimeBudget?String(Math.round(t.runtimeBudget.maxRuntimeMs/6e4)):``,warningMinutes:t.mode===`on_demand_agent`&&t.runtimeBudget?.warnAtMs!==void 0?String(Math.round(t.runtimeBudget.warnAtMs/6e4)):``,warningMessage:t.mode===`on_demand_agent`&&t.runtimeBudget?.warningMessage||``,concurrencyPolicy:e.concurrencyPolicy,title:e.taskTemplate.title,body:e.taskTemplate.body,severity:e.taskTemplate.severity||`info`,dedupeKey:e.taskTemplate.dedupeKey||``,externalUrl:e.taskTemplate.externalUrl||``}}function Y(e){let t=Number(e.budgetMinutes),n=Number(e.warningMinutes),r=e.targetMode===`on_demand_agent`&&e.budgetMinutes.trim()&&Number.isFinite(t)?{maxRuntimeMs:Math.round(t*6e4),warnAtMs:e.warningMinutes.trim()&&Number.isFinite(n)?Math.round(n*6e4):void 0,warningMessage:e.warningMessage.trim()||void 0}:void 0,i=e.targetMode===`existing_agent`?{mode:`existing_agent`,selector:{provider:e.provider||void 0,label:e.label.trim()||void 0,tags:q(e.tags),capabilities:q(e.capabilities)},ifNoMatch:e.ifNoMatch}:{mode:`on_demand_agent`,provider:e.provider,model:e.model||void 0,effort:e.effort?e.effort:void 0,profile:e.profile||void 0,cwd:e.cwd.trim()||void 0,approvalMode:e.approvalMode,workspaceMode:e.workspaceMode,keepAlive:e.keepAlive,runtimeBudget:r};return{kind:`scheduled_task`,name:e.name.trim(),description:e.description.trim()||void 0,enabled:e.enabled,schedule:e.schedule.trim(),timezone:e.timezone.trim()||`UTC`,catchUpPolicy:`skip`,concurrencyPolicy:e.concurrencyPolicy,orchestratorId:e.orchestratorId,targetPolicy:i,taskTemplate:{title:e.title.trim(),body:e.body.trim(),severity:e.severity,dedupeKey:e.dedupeKey.trim()||void 0,externalUrl:e.externalUrl.trim()||void 0}}}function X(){let e=f(),t=h(e=>e.automations),r=h(e=>e.automationRuns),i=h(e=>e.orchestrators),a=h(e=>e.agents),o=h(e=>e.apiCall),c=h(e=>e.showError),l=h(e=>e.openConfirm),u=h(e=>e.fetchAutomations),d=h(e=>e.fetchAutomationRuns),p=h(e=>e.fetchTasks),m=h(e=>e.fetchProviderCatalog),g=h(e=>e.agentProfiles),[_,v]=(0,A.useState)(``),[x,S]=(0,A.useState)(()=>K(i[0]?.id||``,i[0])),[C,w]=(0,A.useState)(!1),[T,E]=(0,A.useState)(null),D=t.find(e=>e.id===_),O=i.filter(e=>e.status===`online`),k=r.filter(L);(0,A.useEffect)(()=>{Promise.all([u(),d(),m()])},[]),(0,A.useEffect)(()=>{!x.orchestratorId&&i[0]&&S(e=>({...e,orchestratorId:i[0].id}))},[i]);let M=(0,A.useMemo)(()=>{let e=new Map;for(let t of r){let n=e.get(t.automationId)||[];n.push(t),e.set(t.automationId,n)}return e},[r]);function N(){v(``),S(K(i[0]?.id||``,i[0]))}function P(e){v(e.id),S(J(e))}async function F(){w(!0);try{let e=Y(x);if(x.id)await o(`PATCH`,`/automations/`+encodeURIComponent(x.id),e);else{let t=await o(`POST`,`/automations`,e);v(t.id),S(J(t))}await Promise.all([u(),d()])}catch(e){c(`Save Failed`,e.message)}finally{w(!1)}}async function I(e,t){try{await o(`PATCH`,`/automations/`+encodeURIComponent(e.id),t),await u()}catch(e){c(`Update Failed`,e.message)}}async function R(e){try{await o(`POST`,`/automations/`+encodeURIComponent(e.id)+`/run`),await Promise.all([d(),p()])}catch(e){c(`Run Failed`,e.message)}}function z(e){l(`Delete Automation`,`Delete automation "${e.name}"?`,async()=>{try{await o(`DELETE`,`/automations/`+encodeURIComponent(e.id)),_===e.id&&N(),await Promise.all([u(),d()])}catch(e){c(`Delete Failed`,e.message)}})}return(0,j.jsxs)(`div`,{className:`space-y-4`,children:[(0,j.jsxs)(`div`,{className:`flex flex-wrap items-center gap-3`,children:[(0,j.jsx)(n,{className:`w-5 h-5 text-muted-foreground`}),(0,j.jsxs)(`div`,{children:[(0,j.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Automation`}),(0,j.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Scheduled tasks`})]}),(0,j.jsxs)(`div`,{className:`ml-auto flex flex-wrap items-center gap-2`,children:[(0,j.jsxs)(y,{variant:`outline`,className:`text-xs`,children:[t.length,` schedules`]}),(0,j.jsxs)(y,{variant:`outline`,className:`text-xs`,children:[k.length,` running`]}),(0,j.jsxs)(b,{size:`sm`,className:`gap-1`,onClick:N,children:[(0,j.jsx)(s,{className:`w-3.5 h-3.5`}),`New`]})]})]}),k.length>0&&(0,j.jsx)(Z,{runs:k,automations:t,agents:a,orchestrators:i,now:e,logRunId:T,onToggleLogs:e=>E(t=>t===e?null:e)}),(0,j.jsxs)(`div`,{className:`grid gap-4 xl:grid-cols-[minmax(0,1fr)_420px]`,children:[(0,j.jsx)(`div`,{className:`space-y-3`,children:t.length===0?(0,j.jsx)(`div`,{className:`py-16 text-center text-sm text-muted-foreground`,children:`No automations`}):t.map(t=>(0,j.jsx)(Q,{automation:t,runs:M.get(t.id)||[],now:e,selected:_===t.id,onEdit:()=>P(t),onRun:()=>R(t),onToggle:()=>I(t,{enabled:!t.enabled}),onDelete:()=>z(t)},t.id))}),(0,j.jsx)(ee,{form:x,agentProfiles:g,selected:D,saving:C,orchestrators:O.length?O:i,onChange:e=>S(t=>({...t,...e})),onSave:F})]}),(0,j.jsx)(te,{runs:r,automations:t,agents:a,orchestrators:i,now:e,logRunId:T,onToggleLogs:e=>E(t=>t===e?null:e)})]})}function Z({runs:e,automations:t,agents:n,orchestrators:r,now:a,logRunId:o,onToggleLogs:s}){let c=new Map(t.map(e=>[e.id,e.name]));return(0,j.jsxs)(`div`,{className:`rounded-lg border border-border bg-card/40`,children:[(0,j.jsx)(`div`,{className:`border-b border-border px-3 py-2 text-sm font-medium`,children:`Active runs`}),(0,j.jsx)(`div`,{className:`divide-y divide-border`,children:e.map(e=>{let t=G(e,n,r),l=U(e,a);return(0,j.jsxs)(`div`,{className:`space-y-3 px-3 py-3`,children:[(0,j.jsxs)(`div`,{className:`grid gap-3 text-xs md:grid-cols-[minmax(12rem,1fr)_7rem_7rem_7rem_8rem_auto] md:items-center`,children:[(0,j.jsxs)(`div`,{className:`min-w-0`,children:[(0,j.jsx)(`div`,{className:`truncate text-sm font-medium`,children:c.get(e.automationId)||e.automationId.slice(0,12)}),(0,j.jsx)(`div`,{className:`truncate font-mono text-muted-foreground`,children:e.targetAgentId||e.spawnedAgentId||z(e.meta?.onDemandLabel)||`-`})]}),(0,j.jsx)(y,{variant:`outline`,className:`border text-[10px] w-fit ${F(e)}`,children:P(e)}),(0,j.jsxs)(`div`,{children:[(0,j.jsx)(`span`,{className:`text-muted-foreground`,children:`elapsed `}),H(e,a)]}),(0,j.jsxs)(`div`,{children:[(0,j.jsx)(`span`,{className:`text-muted-foreground`,children:`left `}),l||`-`]}),(0,j.jsx)(`div`,{className:`font-mono text-muted-foreground`,children:e.taskId?`task #${e.taskId}`:`-`}),t&&(0,j.jsxs)(b,{type:`button`,size:`sm`,variant:`outline`,className:`h-7 gap-1 text-xs`,onClick:()=>s(e.id),children:[(0,j.jsx)(i,{className:`w-3 h-3`}),`Logs`]})]}),o===e.id&&t&&(0,j.jsx)(C,{orchestratorId:t.orchestratorId,session:t.session})]},e.id)})})]})}function Q({automation:e,runs:t,now:n,selected:i,onEdit:a,onRun:s,onToggle:c,onDelete:u}){let d=t[0],f=e.targetPolicy,p=f.mode===`existing_agent`?[f.selector.provider,f.selector.label?`label:${f.selector.label}`:``,...(f.selector.tags||[]).map(e=>`tag:${e}`),...(f.selector.capabilities||[]).map(e=>`cap:${e}`)].filter(Boolean).join(` `):`${f.provider} on demand`;return(0,j.jsxs)(k,{className:`${i?`ring-1 ring-primary/50`:``} ${e.enabled?``:`opacity-60`}`,children:[(0,j.jsx)(E,{className:`px-4 py-3`,children:(0,j.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,j.jsx)(S,{checked:e.enabled,onCheckedChange:c,className:`mt-0.5`}),(0,j.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,j.jsx)(O,{className:`text-sm truncate`,children:e.name}),(0,j.jsxs)(`div`,{className:`mt-1 flex flex-wrap gap-1.5 text-xs text-muted-foreground`,children:[(0,j.jsx)(`span`,{className:`font-mono`,children:e.schedule}),(0,j.jsx)(`span`,{children:e.timezone}),(0,j.jsx)(`span`,{children:e.orchestratorId}),(0,j.jsx)(`span`,{className:`truncate max-w-[28rem]`,children:p})]})]}),(0,j.jsxs)(`div`,{className:`flex gap-1`,children:[(0,j.jsx)(b,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Run now`,onClick:s,children:(0,j.jsx)(o,{className:`w-3.5 h-3.5`})}),(0,j.jsx)(b,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Edit`,onClick:a,children:(0,j.jsx)(r,{className:`w-3.5 h-3.5`})}),(0,j.jsx)(b,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Delete`,onClick:u,children:(0,j.jsx)(l,{className:`w-3.5 h-3.5`})})]})]})}),(0,j.jsx)(D,{className:`px-4 pb-3 pt-0`,children:(0,j.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 text-xs text-muted-foreground`,children:[(0,j.jsxs)(`span`,{title:e.nextRunAt?v(e.nextRunAt):void 0,children:[`next `,e.nextRunAt?_(n,e.nextRunAt):`disabled`]}),d&&(0,j.jsx)(y,{variant:`outline`,className:`border text-[10px] ${F(d)}`,children:P(d)}),d?.taskId&&(0,j.jsxs)(`span`,{children:[`task #`,d.taskId]}),d&&I(d)&&(0,j.jsx)(`span`,{className:`text-red-400 truncate`,children:I(d)})]})})]})}function ee({form:e,selected:t,saving:n,orchestrators:r,agentProfiles:i,onChange:o,onSave:s}){let c=h(e=>e.providerCatalog),l=r.find(t=>t.id===e.orchestratorId)?.providers||[],f=m(e.provider,c),p=f.find(t=>t.alias===e.model)?.efforts||[];function g(t){let n=r.find(e=>e.id===t)?.providers[0]||e.provider;o({orchestratorId:t,provider:n,model:d(n,c),effort:``})}function _(e){o({provider:e,model:d(e,c),effort:``})}return(0,j.jsxs)(`div`,{className:`rounded-lg border border-border bg-card p-4 space-y-4 h-fit xl:sticky xl:top-4`,children:[(0,j.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,j.jsx)(`h3`,{className:`text-sm font-semibold`,children:t?`Edit schedule`:`New schedule`}),(0,j.jsx)(y,{variant:e.enabled?`default`:`secondary`,className:`ml-auto text-[10px]`,children:e.enabled?`enabled`:`disabled`})]}),(0,j.jsxs)(`div`,{className:`grid gap-3`,children:[(0,j.jsx)($,{label:`Name`,children:(0,j.jsx)(x,{value:e.name,onChange:e=>o({name:e.target.value})})}),(0,j.jsx)($,{label:`Description`,children:(0,j.jsx)(x,{value:e.description,onChange:e=>o({description:e.target.value})})}),(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,j.jsx)($,{label:`Cron`,children:(0,j.jsx)(x,{value:e.schedule,onChange:e=>o({schedule:e.target.value}),className:`font-mono`})}),(0,j.jsx)($,{label:`Timezone`,children:(0,j.jsx)(x,{value:e.timezone,onChange:e=>o({timezone:e.target.value})})})]}),(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,j.jsx)($,{label:`Orchestrator`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.orchestratorId,onChange:e=>g(e.target.value),children:[(0,j.jsx)(`option`,{value:``,children:`Select`}),r.map(e=>(0,j.jsxs)(`option`,{value:e.id,children:[e.hostname,` (`,e.id,`)`]},e.id))]})}),(0,j.jsx)($,{label:`Concurrency`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.concurrencyPolicy,onChange:e=>o({concurrencyPolicy:e.target.value}),children:[(0,j.jsx)(`option`,{value:`skip`,children:`skip`}),(0,j.jsx)(`option`,{value:`replace`,children:`replace`}),(0,j.jsx)(`option`,{value:`queue`,children:`queue`})]})})]}),(0,j.jsxs)(`label`,{className:`flex items-center justify-between rounded-md border border-border px-3 py-2`,children:[(0,j.jsx)(`span`,{className:`text-sm`,children:`Enabled`}),(0,j.jsx)(S,{checked:e.enabled,onCheckedChange:e=>o({enabled:e})})]})]}),(0,j.jsxs)(`div`,{className:`space-y-3`,children:[(0,j.jsx)(w,{children:`Target`}),(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-2`,children:[(0,j.jsx)(b,{type:`button`,variant:e.targetMode===`existing_agent`?`default`:`outline`,onClick:()=>o({targetMode:`existing_agent`}),children:`Existing`}),(0,j.jsx)(b,{type:`button`,variant:e.targetMode===`on_demand_agent`?`default`:`outline`,onClick:()=>o({targetMode:`on_demand_agent`}),children:`On demand`})]}),(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,j.jsx)($,{label:`Provider`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.provider,onChange:e=>_(e.target.value),disabled:!l.length,children:[!l.length&&(0,j.jsx)(`option`,{value:e.provider,children:`No providers available`}),l.map(e=>(0,j.jsx)(`option`,{value:e,children:e},e))]})}),e.targetMode===`existing_agent`?(0,j.jsx)($,{label:`If no match`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.ifNoMatch,onChange:e=>o({ifNoMatch:e.target.value}),children:[(0,j.jsx)(`option`,{value:`fail`,children:`fail`}),(0,j.jsx)(`option`,{value:`spawn`,children:`spawn`})]})}):(0,j.jsx)($,{label:`Approval`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.approvalMode,onChange:e=>o({approvalMode:e.target.value}),children:[(0,j.jsx)(`option`,{value:`guarded`,children:`guarded`}),(0,j.jsx)(`option`,{value:`read-only`,children:`read-only`}),(0,j.jsx)(`option`,{value:`open`,children:`open`})]})})]}),e.targetMode===`on_demand_agent`&&(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,j.jsx)($,{label:`Model`,children:(0,j.jsx)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.model,onChange:e=>o({model:e.target.value,effort:``}),children:f.map(e=>(0,j.jsx)(`option`,{value:e.alias,children:e.label},e.alias))})}),(0,j.jsx)($,{label:`Effort`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.effort,onChange:e=>o({effort:e.target.value}),disabled:!p.length,children:[(0,j.jsx)(`option`,{value:``,children:`Default`}),p.map(e=>(0,j.jsx)(`option`,{value:e,children:e},e))]})}),(0,j.jsx)($,{label:`Agent profile`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.profile||`default-relay`,onChange:e=>o({profile:e.target.value}),children:[!i.length&&(0,j.jsx)(`option`,{value:`default-relay`,children:`default-relay`}),i.map(e=>(0,j.jsx)(`option`,{value:e.name,children:e.name},e.name))]})}),(0,j.jsx)($,{label:`Workspace`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.workspaceMode,onChange:e=>o({workspaceMode:e.target.value}),children:[(0,j.jsx)(`option`,{value:`inherit`,children:`inherit (default → isolated worktree)`}),(0,j.jsx)(`option`,{value:`shared`,children:`shared (run in the repo, commit to its branch)`}),(0,j.jsx)(`option`,{value:`isolated`,children:`isolated (fresh git worktree per run)`})]})})]}),e.targetMode===`existing_agent`?(0,j.jsxs)(`div`,{className:`grid gap-3`,children:[(0,j.jsx)($,{label:`Label`,children:(0,j.jsx)(x,{value:e.label,onChange:e=>o({label:e.target.value})})}),(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,j.jsx)($,{label:`Tags`,children:(0,j.jsx)(x,{value:e.tags,onChange:e=>o({tags:e.target.value})})}),(0,j.jsx)($,{label:`Capabilities`,children:(0,j.jsx)(x,{value:e.capabilities,onChange:e=>o({capabilities:e.target.value})})})]})]}):(0,j.jsxs)(`div`,{className:`grid gap-3`,children:[(0,j.jsx)($,{label:`Cwd`,children:(0,j.jsx)(x,{value:e.cwd,onChange:e=>o({cwd:e.target.value})})}),(0,j.jsxs)(`label`,{className:`flex items-center justify-between rounded-md border border-border px-3 py-2`,children:[(0,j.jsx)(`span`,{className:`text-sm`,children:`Keep alive`}),(0,j.jsx)(S,{checked:e.keepAlive,onCheckedChange:e=>o({keepAlive:e})})]}),(0,j.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,j.jsx)($,{label:`Budget minutes`,children:(0,j.jsx)(x,{type:`number`,min:1,step:1,value:e.budgetMinutes,onChange:e=>o({budgetMinutes:e.target.value})})}),(0,j.jsx)($,{label:`Warning minute`,children:(0,j.jsx)(x,{type:`number`,min:0,step:1,value:e.warningMinutes,onChange:e=>o({warningMinutes:e.target.value})})})]}),(0,j.jsx)($,{label:`Warning message`,children:(0,j.jsx)(T,{value:e.warningMessage,onChange:e=>o({warningMessage:e.target.value}),className:`min-h-20`})})]})]}),(0,j.jsxs)(`div`,{className:`space-y-3`,children:[(0,j.jsx)(w,{children:`Task`}),(0,j.jsx)($,{label:`Title`,children:(0,j.jsx)(x,{value:e.title,onChange:e=>o({title:e.target.value})})}),(0,j.jsx)($,{label:`Body`,children:(0,j.jsx)(T,{value:e.body,onChange:e=>o({body:e.target.value}),className:`min-h-28`})}),(0,j.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,j.jsx)($,{label:`Severity`,children:(0,j.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.severity,onChange:e=>o({severity:e.target.value}),children:[(0,j.jsx)(`option`,{value:`info`,children:`info`}),(0,j.jsx)(`option`,{value:`warning`,children:`warning`}),(0,j.jsx)(`option`,{value:`critical`,children:`critical`})]})}),(0,j.jsx)($,{label:`Dedupe key`,children:(0,j.jsx)(x,{value:e.dedupeKey,onChange:e=>o({dedupeKey:e.target.value})})}),(0,j.jsx)($,{label:`External URL`,children:(0,j.jsx)(x,{value:e.externalUrl,onChange:e=>o({externalUrl:e.target.value})})})]})]}),(0,j.jsxs)(b,{className:`w-full gap-2`,onClick:s,disabled:n||!e.name||!e.schedule||!e.orchestratorId||!e.title||!e.body,children:[n?(0,j.jsx)(a,{className:`w-4 h-4 animate-spin`}):(0,j.jsx)(u,{className:`w-4 h-4`}),`Save`]})]})}function $({label:e,children:t}){return(0,j.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,j.jsx)(w,{className:`text-xs text-muted-foreground`,children:e}),t]})}function te({runs:e,automations:t,agents:n,orchestrators:r,now:a,logRunId:o,onToggleLogs:s}){let c=new Map(t.map(e=>[e.id,e.name]));return(0,j.jsxs)(`div`,{className:`space-y-2`,children:[(0,j.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,j.jsx)(`h3`,{className:`text-sm font-semibold`,children:`Runs`}),(0,j.jsx)(y,{variant:`secondary`,className:`text-[10px]`,children:e.length})]}),(0,j.jsxs)(`div`,{className:`rounded-lg border border-border overflow-x-auto`,children:[(0,j.jsxs)(`div`,{className:`grid grid-cols-[minmax(10rem,1fr)_8rem_8rem_7rem_7rem_5rem] gap-3 bg-muted/30 px-3 py-2 text-xs text-muted-foreground`,children:[(0,j.jsx)(`span`,{children:`Automation`}),(0,j.jsx)(`span`,{children:`Status`}),(0,j.jsx)(`span`,{children:`Agent`}),(0,j.jsx)(`span`,{children:`Runtime`}),(0,j.jsx)(`span`,{children:`Updated`}),(0,j.jsx)(`span`,{children:`Logs`})]}),e.length===0?(0,j.jsx)(`div`,{className:`px-3 py-10 text-center text-sm text-muted-foreground`,children:`No runs`}):e.slice(0,80).map(e=>{let t=G(e,n,r),l=U(e,a),u=W(e);return(0,j.jsxs)(`div`,{className:`border-t border-border`,children:[(0,j.jsxs)(`div`,{className:`grid grid-cols-[minmax(10rem,1fr)_8rem_8rem_7rem_7rem_5rem] gap-3 px-3 py-2 text-xs items-center`,children:[(0,j.jsxs)(`div`,{className:`min-w-0`,children:[(0,j.jsx)(`div`,{className:`truncate`,children:c.get(e.automationId)||e.automationId.slice(0,12)}),(0,j.jsxs)(`div`,{className:`truncate text-muted-foreground`,children:[e.taskId?`task #${e.taskId}`:`no task`,l?` · ${l} left`:``,u?` · ${u}`:``]})]}),(0,j.jsx)(y,{variant:`outline`,className:`border text-[10px] w-fit ${F(e)}`,children:P(e)}),(0,j.jsx)(`span`,{className:`truncate font-mono text-muted-foreground`,children:e.targetAgentId||e.spawnedAgentId||`-`}),(0,j.jsx)(`span`,{className:`text-muted-foreground`,children:H(e,a)}),(0,j.jsx)(`span`,{className:`text-muted-foreground`,title:v(e.updatedAt),children:_(a,e.updatedAt)}),t?(0,j.jsx)(b,{type:`button`,size:`sm`,variant:`ghost`,className:`h-7 w-7 p-0`,title:`Open logs`,onClick:()=>s(e.id),children:(0,j.jsx)(i,{className:`w-3.5 h-3.5`})}):(0,j.jsx)(`span`,{className:`text-muted-foreground`,children:`-`})]}),o===e.id&&t&&(0,j.jsx)(`div`,{className:`border-t border-border px-3 py-3`,children:(0,j.jsx)(C,{orchestratorId:t.orchestratorId,session:t.session})})]},e.id)})]})]})}export{X as AutomationView};
2
- //# sourceMappingURL=automation-ShdXg0pX.js.map