@growthub/cli 0.14.0 → 0.14.2

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 (40) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +99 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +84 -10
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +2 -2
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +107 -34
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +72 -15
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +179 -117
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +136 -3
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +61 -13
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +224 -11
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +254 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +10 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +412 -1
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +1 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +554 -0
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  40. package/package.json +1 -1
@@ -16,7 +16,12 @@
16
16
  * - lib/orchestration-run-trace.js (lower-level record parser)
17
17
  */
18
18
 
19
- import { redactSecretsFromText } from "./orchestration-graph.js";
19
+ import {
20
+ extractSwarmNodes,
21
+ isAgentSwarmGraph,
22
+ parseOrchestrationGraph,
23
+ redactSecretsFromText
24
+ } from "./orchestration-graph.js";
20
25
  import { redactRunInputsEnvelope, summarizeRunInputs } from "./orchestration-run-inputs.js";
21
26
 
22
27
  const RUN_LOG_BUNDLE_KIND = "growthub-sandbox-run-log-v1";
@@ -202,6 +207,388 @@ function buildExportsForRecord(record, stdoutText, stderrText, outputText) {
202
207
  return { available, external: [] };
203
208
  }
204
209
 
210
+ /**
211
+ * Swarm cockpit projection (SWARM_RUN_CONTRACT_V1).
212
+ *
213
+ * Pure transformation of a sandbox run record carrying a `swarm` block
214
+ * (written by the agent-swarm-v1 runtime through sandbox-run) into the
215
+ * phase/agent tree the helper sidecar cockpit renders. Returns `null` for
216
+ * non-swarm records so existing runs are untouched.
217
+ *
218
+ * Telemetry is truthful: tokens/tools are null when the adapter did not
219
+ * report them — the UI renders "—", never an estimate. Totals are null when
220
+ * no agent reported a number.
221
+ *
222
+ * Same module rules as the rest of this file: no React, no fetch, no config
223
+ * writes, no localStorage, no CSS.
224
+ */
225
+ // Truthful counts only: null/undefined (adapter reported nothing) stays
226
+ // null — clampNumber would coerce null to 0, which would be a fake metric.
227
+ function toTruthfulCount(value) {
228
+ if (value == null) return null;
229
+ const n = Number(value);
230
+ return Number.isFinite(n) && n >= 0 ? n : null;
231
+ }
232
+
233
+ function toAdapterReportedCount(entry, field) {
234
+ const meta = entry?.adapterMeta && typeof entry.adapterMeta === "object" ? entry.adapterMeta : null;
235
+ if (meta?.telemetrySource === "unreported" && meta?.[field] == null) return null;
236
+ if (entry?.[field] === 0 && meta && meta[field] == null) return null;
237
+ if (entry?.[field] === 0 && !meta && safeString(entry?.adapter).trim() === "local-agent-host") return null;
238
+ return toTruthfulCount(entry?.[field]);
239
+ }
240
+
241
+ function parsePersistedAgentHostTelemetry(entry) {
242
+ const stderr = safeString(entry?.stderr);
243
+ const total = stderr.match(/\b(?:total\s+tokens|tokens\s+used)\s*(?:[:=]|\r?\n)\s*([0-9][0-9,]*)/i);
244
+ const input = stderr.match(/\b(?:input|prompt)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
245
+ const output = stderr.match(/\b(?:output|completion)\s+tokens\s*[:=]\s*([0-9][0-9,]*)/i);
246
+ const tools = stderr.match(/\b(?:tool\s+calls?|tools\s+used)\s*[:=]\s*([0-9][0-9,]*)/i);
247
+ const toInt = (value) => {
248
+ if (value == null || value === "") return null;
249
+ const n = Number(String(value).replace(/,/g, ""));
250
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
251
+ };
252
+ const inputTokens = toInt(input?.[1]);
253
+ const outputTokens = toInt(output?.[1]);
254
+ const summed = inputTokens != null || outputTokens != null
255
+ ? (inputTokens || 0) + (outputTokens || 0)
256
+ : null;
257
+ const tokens = toInt(total?.[1]) ?? summed;
258
+ return {
259
+ tokens,
260
+ tools: toInt(tools?.[1]) ?? (tokens != null ? 0 : null)
261
+ };
262
+ }
263
+
264
+ function titleizePhaseId(id) {
265
+ const text = safeString(id).trim() || "dispatch";
266
+ return text.charAt(0).toUpperCase() + text.slice(1);
267
+ }
268
+
269
+ function sumReportedOrNull(values) {
270
+ const reported = values.filter((n) => n != null);
271
+ return reported.length > 0 ? reported.reduce((sum, n) => sum + n, 0) : null;
272
+ }
273
+
274
+ function deriveSwarmRunProjection(record) {
275
+ if (!record || typeof record !== "object") return null;
276
+ const swarm = record.swarm;
277
+ if (!swarm || typeof swarm !== "object") return null;
278
+
279
+ const summary = deriveRunSummary(record);
280
+ const tasks = Array.isArray(swarm.tasks) ? swarm.tasks.filter((t) => t && typeof t === "object") : [];
281
+
282
+ // Agents projected from a persisted record are terminal: `pending: false`
283
+ // means a missing count is "ran but never reported" → the UI shows "—".
284
+ // (Pending agents — from the graph skeleton or a live stream — show blank.)
285
+ const toAgent = (entry, fallbackId, fallbackLabel, transcriptParts, logNodeId) => {
286
+ const persistedTelemetry = parsePersistedAgentHostTelemetry(entry);
287
+ const tokens = persistedTelemetry.tokens ?? toAdapterReportedCount(entry, "tokens");
288
+ const tools = persistedTelemetry.tools ?? toAdapterReportedCount(entry, "tools");
289
+ return {
290
+ id: safeString(entry?.taskId || entry?.nodeId || fallbackId).trim() || fallbackId,
291
+ label: safeString(entry?.role || entry?.label || fallbackLabel).trim() || fallbackLabel,
292
+ status: safeString(entry?.status || "unknown").trim() || "unknown",
293
+ pending: false,
294
+ tokens,
295
+ tools,
296
+ durationMs: clampNumber(entry?.durationMs) ?? 0,
297
+ transcript: redactSecretsFromText(
298
+ transcriptParts.map((part) => safeString(part).trim()).filter(Boolean).join("\n\n")
299
+ ),
300
+ logNodeId
301
+ };
302
+ };
303
+
304
+ const phases = [];
305
+ const orchestrator = swarm.orchestrator && typeof swarm.orchestrator === "object" ? swarm.orchestrator : null;
306
+ if (orchestrator) {
307
+ phases.push({
308
+ id: "plan",
309
+ label: "Plan",
310
+ status: safeString(orchestrator.status || "unknown").trim() || "unknown",
311
+ agents: [
312
+ toAgent(
313
+ orchestrator,
314
+ "orchestrator",
315
+ "Orchestrator",
316
+ [orchestrator.error, orchestrator.plan],
317
+ "phase-orchestrator"
318
+ )
319
+ ]
320
+ });
321
+ }
322
+
323
+ // Group dispatch tasks by their declared phase id (author-named phases),
324
+ // falling back to the single "Dispatch" group for legacy records whose
325
+ // tasks carry no phaseId — those project identically to before.
326
+ const dispatchGroups = new Map();
327
+ tasks.forEach((task, index) => {
328
+ const phaseId = safeString(task.phaseId).trim() || "dispatch";
329
+ if (!dispatchGroups.has(phaseId)) dispatchGroups.set(phaseId, []);
330
+ dispatchGroups.get(phaseId).push(
331
+ toAgent(
332
+ task,
333
+ `task-${index + 1}`,
334
+ `Agent ${index + 1}`,
335
+ [task.error, task.stdout, task.stderr],
336
+ safeString(task.taskId || task.nodeId || `task-${index + 1}`).trim()
337
+ )
338
+ );
339
+ });
340
+ if (dispatchGroups.size === 0) dispatchGroups.set("dispatch", []);
341
+ for (const [phaseId, agents] of dispatchGroups) {
342
+ const status = agents.length === 0
343
+ ? "failed"
344
+ : agents.every((a) => a.status === "completed")
345
+ ? "completed"
346
+ : agents.some((a) => a.status === "failed")
347
+ ? "failed"
348
+ : "info";
349
+ phases.push({ id: phaseId, label: titleizePhaseId(phaseId), status, agents });
350
+ }
351
+
352
+ const synthesis = swarm.synthesis && typeof swarm.synthesis === "object" ? swarm.synthesis : null;
353
+ if (synthesis) {
354
+ phases.push({
355
+ id: "synthesize",
356
+ label: "Synthesize",
357
+ status: safeString(synthesis.status || "unknown").trim() || "unknown",
358
+ agents: [
359
+ toAgent(
360
+ synthesis,
361
+ "synthesis",
362
+ synthesis.label || "Synthesizer",
363
+ [synthesis.error, synthesis.answer],
364
+ "phase-synthesis"
365
+ )
366
+ ]
367
+ });
368
+ }
369
+
370
+ const allAgents = phases.flatMap((phase) => phase.agents);
371
+
372
+ return {
373
+ runId: safeString(record.runId).trim(),
374
+ title: safeString(record.name || record.sandboxName).trim() || "agent-swarm",
375
+ status: summary.status,
376
+ elapsedMs: clampNumber(record.durationMs) ?? 0,
377
+ agentCount: allAgents.length,
378
+ totalTokens: sumReportedOrNull(allAgents.map((a) => a.tokens)),
379
+ totalTools: sumReportedOrNull(allAgents.map((a) => a.tools)),
380
+ phases
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Declared-phase skeleton (SWARM_RUN_CONTRACT_V1, parity P1).
386
+ *
387
+ * Projects the SAME projection shape as deriveSwarmRunProjection from the
388
+ * governed row's agent-swarm-v1 graph alone — before any run exists. Every
389
+ * phase and agent renders upfront with `status: "pending"`, `pending: true`
390
+ * (UI shows hollow dots and BLANK cells, reserving "—" for terminal
391
+ * never-reported). Author-named phases come from each subagent node's
392
+ * `config.phase` / `config.phaseId`; graphs that declare none fall back to
393
+ * the Plan / Dispatch / Synthesize derivation, converging exactly with the
394
+ * record projection for the same workflow.
395
+ *
396
+ * Pure: no React, no fetch, no config writes, no localStorage, no CSS.
397
+ */
398
+ function deriveSwarmGraphProjection(graphLike, { title = "", runId = "" } = {}) {
399
+ const graph = parseOrchestrationGraph(graphLike);
400
+ if (!graph || !isAgentSwarmGraph(graph)) return null;
401
+ const extracted = extractSwarmNodes(graph);
402
+ if (!extracted) return null;
403
+ const { orchestrator, subagents, synthesis } = extracted;
404
+
405
+ const pendingAgent = (id, label) => ({
406
+ id: safeString(id).trim() || "agent",
407
+ label: safeString(label).trim() || "Agent",
408
+ status: "pending",
409
+ pending: true,
410
+ tokens: null,
411
+ tools: null,
412
+ durationMs: 0,
413
+ transcript: "",
414
+ logNodeId: ""
415
+ });
416
+
417
+ const phases = [];
418
+ if (orchestrator) {
419
+ phases.push({
420
+ id: "plan",
421
+ label: "Plan",
422
+ status: "pending",
423
+ agents: [pendingAgent(orchestrator.id || "orchestrator", orchestrator.config?.role || "Orchestrator")]
424
+ });
425
+ }
426
+
427
+ const dispatchGroups = new Map();
428
+ subagents.forEach((node, index) => {
429
+ const phaseId = safeString(node?.config?.phase || node?.config?.phaseId).trim().toLowerCase() || "dispatch";
430
+ if (!dispatchGroups.has(phaseId)) dispatchGroups.set(phaseId, []);
431
+ dispatchGroups.get(phaseId).push(
432
+ pendingAgent(node?.id || `task-${index + 1}`, node?.config?.role || node?.label || `Agent ${index + 1}`)
433
+ );
434
+ });
435
+ if (dispatchGroups.size === 0) dispatchGroups.set("dispatch", []);
436
+ for (const [phaseId, agents] of dispatchGroups) {
437
+ phases.push({ id: phaseId, label: titleizePhaseId(phaseId), status: "pending", agents });
438
+ }
439
+
440
+ if (synthesis) {
441
+ phases.push({
442
+ id: "synthesize",
443
+ label: "Synthesize",
444
+ status: "pending",
445
+ agents: [pendingAgent(synthesis.id || "synthesis", synthesis.label || "Synthesizer")]
446
+ });
447
+ }
448
+
449
+ return {
450
+ runId: safeString(runId).trim(),
451
+ title: safeString(title).trim() || "agent-swarm",
452
+ status: "pending",
453
+ elapsedMs: 0,
454
+ agentCount: phases.reduce((sum, phase) => sum + phase.agents.length, 0),
455
+ totalTokens: null,
456
+ totalTools: null,
457
+ phases
458
+ };
459
+ }
460
+
461
+ function cloneSwarmProjection(projection) {
462
+ if (!projection || typeof projection !== "object") return null;
463
+ return {
464
+ ...projection,
465
+ phases: Array.isArray(projection.phases)
466
+ ? projection.phases.map((phase) => ({
467
+ ...phase,
468
+ agents: Array.isArray(phase.agents)
469
+ ? phase.agents.map((agent) => ({ ...agent }))
470
+ : []
471
+ }))
472
+ : []
473
+ };
474
+ }
475
+
476
+ function findOrCreateLivePhase(projection, phaseId, label) {
477
+ const id = safeString(phaseId).trim() || "dispatch";
478
+ let phase = projection.phases.find((entry) => entry.id === id);
479
+ if (!phase) {
480
+ phase = { id, label: safeString(label).trim() || titleizePhaseId(id), status: "pending", agents: [] };
481
+ projection.phases.push(phase);
482
+ }
483
+ return phase;
484
+ }
485
+
486
+ function upsertLiveAgent(phase, incoming) {
487
+ if (!phase) return;
488
+ const fallbackId = safeString(incoming?.taskId || incoming?.nodeId || incoming?.id).trim() || "agent";
489
+ const id = fallbackId;
490
+ const label = safeString(incoming?.role || incoming?.label).trim() || "Agent";
491
+ const index = phase.agents.findIndex((agent) => agent.id === id || agent.logNodeId === id);
492
+ const prior = index >= 0 ? phase.agents[index] : {};
493
+ const status = safeString(incoming?.status || prior.status || "running").trim() || "running";
494
+ const next = {
495
+ id,
496
+ label: label || prior.label || "Agent",
497
+ status,
498
+ pending: status === "pending" || status === "running" || status === "executing",
499
+ tokens: toTruthfulCount(incoming?.tokens ?? prior.tokens),
500
+ tools: toTruthfulCount(incoming?.tools ?? prior.tools),
501
+ durationMs: clampNumber(incoming?.durationMs) ?? prior.durationMs ?? 0,
502
+ transcript: prior.transcript || "",
503
+ logNodeId: safeString(incoming?.nodeId || prior.logNodeId || id).trim() || id
504
+ };
505
+ if (index >= 0) phase.agents[index] = next;
506
+ else phase.agents.push(next);
507
+ }
508
+
509
+ function derivePhaseStatusFromAgents(phase) {
510
+ const agents = Array.isArray(phase?.agents) ? phase.agents : [];
511
+ if (agents.some((agent) => agent.status === "running" || agent.status === "executing")) return "running";
512
+ if (agents.some((agent) => agent.status === "failed")) return "failed";
513
+ if (agents.length > 0 && agents.every((agent) => agent.status === "completed")) return "completed";
514
+ return phase?.status || "pending";
515
+ }
516
+
517
+ /**
518
+ * Live swarm delta projection.
519
+ *
520
+ * This consumes the optional NDJSON events from POST /api/workspace/sandbox-run
521
+ * and returns the exact same shape as persisted-record projection. The final
522
+ * source of truth remains deriveSwarmRunProjection(record); this only hydrates
523
+ * the cosmetic Background Tasks card while the POST is still open.
524
+ */
525
+ function deriveSwarmDeltaProjection(graphLike, events, { title = "", runId = "", elapsedMs = 0 } = {}) {
526
+ const base = cloneSwarmProjection(deriveSwarmGraphProjection(graphLike, { title, runId }));
527
+ if (!base) return null;
528
+ const list = Array.isArray(events) ? events.filter((event) => event && typeof event === "object") : [];
529
+ let status = "running";
530
+ let latestRunId = safeString(runId).trim() || base.runId;
531
+
532
+ for (const event of list) {
533
+ if (event.runId) latestRunId = safeString(event.runId).trim() || latestRunId;
534
+ const type = safeString(event.type).trim();
535
+ if (type === "swarm.run.started") {
536
+ status = "running";
537
+ continue;
538
+ }
539
+ if (type === "swarm.run.completed" || type === "swarm.run.failed") {
540
+ status = safeString(event.status || (type.endsWith("completed") ? "completed" : "failed")).trim() || status;
541
+ continue;
542
+ }
543
+ if (type === "swarm.phase.started") {
544
+ const phase = findOrCreateLivePhase(base, event.phaseId, event.label);
545
+ phase.status = "running";
546
+ continue;
547
+ }
548
+ if (type === "swarm.phase.completed" || type === "swarm.phase.failed") {
549
+ const phase = findOrCreateLivePhase(base, event.phaseId, event.label);
550
+ phase.status = safeString(event.status || (type.endsWith("completed") ? "completed" : "failed")).trim() || phase.status;
551
+ if (event.agent && typeof event.agent === "object") upsertLiveAgent(phase, event.agent);
552
+ continue;
553
+ }
554
+ if (type === "swarm.task.started") {
555
+ const phase = findOrCreateLivePhase(base, event.phaseId, titleizePhaseId(event.phaseId));
556
+ phase.status = "running";
557
+ upsertLiveAgent(phase, {
558
+ taskId: event.taskId,
559
+ nodeId: event.nodeId,
560
+ role: event.role,
561
+ status: "running",
562
+ phaseId: event.phaseId
563
+ });
564
+ continue;
565
+ }
566
+ if (type === "swarm.task.completed" || type === "swarm.task.failed") {
567
+ const task = event.task && typeof event.task === "object" ? event.task : event;
568
+ const phase = findOrCreateLivePhase(base, task.phaseId || event.phaseId, titleizePhaseId(task.phaseId || event.phaseId));
569
+ upsertLiveAgent(phase, task);
570
+ phase.status = derivePhaseStatusFromAgents(phase);
571
+ }
572
+ }
573
+
574
+ for (const phase of base.phases) {
575
+ if (phase.status === "running") continue;
576
+ phase.status = derivePhaseStatusFromAgents(phase);
577
+ }
578
+
579
+ const allAgents = base.phases.flatMap((phase) => phase.agents);
580
+ return {
581
+ ...base,
582
+ runId: latestRunId,
583
+ status,
584
+ elapsedMs: clampNumber(elapsedMs) ?? base.elapsedMs ?? 0,
585
+ agentCount: allAgents.length,
586
+ totalTokens: sumReportedOrNull(allAgents.map((agent) => agent.tokens)),
587
+ totalTools: sumReportedOrNull(allAgents.map((agent) => agent.tools)),
588
+ phases: base.phases
589
+ };
590
+ }
591
+
205
592
  function normalizeRunConsoleRecord(record) {
206
593
  if (!record || typeof record !== "object") return null;
207
594
  const summary = deriveRunSummary(record);
@@ -298,11 +685,13 @@ function normalizeRunConsoleRecord(record) {
298
685
  envRefsMissing: Array.isArray(record.envRefsMissing) ? record.envRefsMissing.slice() : [],
299
686
  networkAllow: Boolean(record.networkAllow),
300
687
  allowList: Array.isArray(record.allowList) ? record.allowList.slice() : [],
688
+ browserAccess: Boolean(record.browserAccess),
301
689
  adapterMeta,
302
690
  templateTrace
303
691
  },
304
692
  lineage,
305
693
  swarm: record.swarm && typeof record.swarm === "object" ? record.swarm : null,
694
+ swarmRun: deriveSwarmRunProjection(record),
306
695
  logTree: buildRunLogTree(record)
307
696
  };
308
697
  }
@@ -387,6 +776,24 @@ function formatRunDuration(ms) {
387
776
  return `${minutes}m ${seconds}s`;
388
777
  }
389
778
 
779
+ /**
780
+ * Compact zero-padded duration for the swarm cockpit tables and cards —
781
+ * Claude Code Background-tasks format: "04s", "15s", "1m 04s". A separate
782
+ * formatter (not a change to formatRunDuration) so existing non-swarm
783
+ * run-console surfaces keep their ms-precision rendering untouched.
784
+ */
785
+ function formatCompactRunDuration(ms) {
786
+ if (ms == null) return "—";
787
+ const n = clampNumber(ms);
788
+ if (n == null) return "—";
789
+ const totalSeconds = Math.max(0, Math.round(n / 1000));
790
+ const minutes = Math.floor(totalSeconds / 60);
791
+ const seconds = totalSeconds % 60;
792
+ const padded = String(seconds).padStart(2, "0");
793
+ if (minutes <= 0) return `${padded}s`;
794
+ return `${minutes}m ${padded}s`;
795
+ }
796
+
390
797
  function downloadRunBundle({ record, runId, sourceId } = {}) {
391
798
  const normalized = normalizeRunConsoleRecord(record || {});
392
799
  return {
@@ -403,6 +810,10 @@ export {
403
810
  DEFAULT_EXPORT_TARGETS,
404
811
  normalizeRunConsoleRecord,
405
812
  deriveRunSummary,
813
+ deriveSwarmRunProjection,
814
+ deriveSwarmGraphProjection,
815
+ deriveSwarmDeltaProjection,
816
+ formatCompactRunDuration,
406
817
  deriveRunLifecycle,
407
818
  buildRunLogTree,
408
819
  buildRunTimeline,
@@ -34,15 +34,14 @@
34
34
  * on-disk auth state; this module only records *readiness*, not secrets.
35
35
  *
36
36
  * The status semantics are deliberately conservative:
37
- * - "active" a real auth probe confirmed authentication (auth-status
38
- * exit 0 with auth-shaped output, or a clean login exit)
39
- * - "reachable" the binary is callable (version probe exit 0) — but
40
- * authentication is NOT yet confirmed
37
+ * - "active" the selected pinned host CLI is callable and ready for the
38
+ * local agent-host bridge, or a clean login exit completed
39
+ * - "reachable" legacy metadata value treated as active by the UI
41
40
  * - "stale" the binary printed auth-shaped failure output
42
41
  * - "missing" binary not found on PATH
43
42
  *
44
- * A `--version` probe NEVER promotes to "active". The next sandbox-run is
45
- * the final source of truth for session readiness.
43
+ * A successful host probe promotes to "active" because the sidecar represents
44
+ * selected local host readiness, not provider-account auth semantics.
46
45
  */
47
46
 
48
47
  import { spawn } from "node:child_process";
@@ -132,6 +131,38 @@ function assertAgentHostEligible(row, { requireLogin = false, requireLogout = fa
132
131
  return { spec, agentHost };
133
132
  }
134
133
 
134
+ function normalizeAgentHostOverride(agentHost) {
135
+ const nextAgentHost = String(agentHost || "").trim();
136
+ if (!nextAgentHost) return "";
137
+ if (!getHostAuthSpec(nextAgentHost)) {
138
+ const error = new Error(
139
+ `Agent auth setup is not registered for agentHost "${nextAgentHost}"`
140
+ );
141
+ error.code = "SANDBOX_AGENT_AUTH_HOST_UNSUPPORTED";
142
+ throw error;
143
+ }
144
+ return nextAgentHost;
145
+ }
146
+
147
+ function applyAgentHostOverride(row, agentHost) {
148
+ if (!agentHost) return row;
149
+ return {
150
+ ...row,
151
+ runLocality: "local",
152
+ adapter: "local-agent-host",
153
+ agentHost
154
+ };
155
+ }
156
+
157
+ function buildAgentHostSelectionPatch(agentHost) {
158
+ if (!agentHost) return {};
159
+ return {
160
+ runLocality: "local",
161
+ adapter: "local-agent-host",
162
+ agentHost
163
+ };
164
+ }
165
+
135
166
  function resolveHostBinary(row, spec) {
136
167
  const candidates = [row?.agentCommand, row?.claudeCommand];
137
168
  for (const candidate of candidates) {
@@ -205,9 +236,24 @@ function hasAny(patterns, text) {
205
236
  return patterns.some((p) => p.test(text));
206
237
  }
207
238
 
239
+ function deriveStatusFromAuthStatusJson(text) {
240
+ if (!text || typeof text !== "string") return null;
241
+ const trimmed = text.trim();
242
+ if (!trimmed.startsWith("{")) return null;
243
+ try {
244
+ const parsed = JSON.parse(trimmed);
245
+ if (parsed && typeof parsed === "object" && typeof parsed.loggedIn === "boolean") {
246
+ return parsed.loggedIn ? "active" : "stale";
247
+ }
248
+ } catch {}
249
+ return null;
250
+ }
251
+
208
252
  function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", spawnError }) {
209
253
  if (spawnError) return spawnError.notFound ? "missing" : null;
210
254
  const combined = `${stdout}\n${stderr}`;
255
+ const jsonStatus = deriveStatusFromAuthStatusJson(stdout) || deriveStatusFromAuthStatusJson(stderr);
256
+ if (jsonStatus) return jsonStatus;
211
257
  if (hasAny(UNKNOWN_SUBCOMMAND_PATTERNS, combined)) return null; // fall back
212
258
  if (hasAny(STALE_AUTH_PATTERNS, combined)) return "stale";
213
259
  if (exitCode === 0) return "active";
@@ -219,7 +265,7 @@ function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", s
219
265
 
220
266
  function deriveStatusFromVersionProbe({ exitCode, stderr, spawnError }) {
221
267
  if (spawnError) return spawnError.notFound ? "missing" : "unknown";
222
- if (typeof exitCode === "number" && exitCode === 0) return "reachable";
268
+ if (typeof exitCode === "number" && exitCode === 0) return "active";
223
269
  const text = String(stderr || "");
224
270
  if (hasAny(STALE_AUTH_PATTERNS, text)) return "stale";
225
271
  return "unknown";
@@ -237,8 +283,8 @@ function deriveLoginStatus({ exitCode, stderr, stdout, timedOut, spawnError }) {
237
283
  function shortMessage({ status, label, exitCode, error, loginUrl }) {
238
284
  const name = label || "Local agent CLI";
239
285
  if (error) return `${name}: ${redactSecrets(String(error))}`;
240
- if (status === "active") return loginUrl ? "Login completed." : "Authenticated.";
241
- if (status === "reachable") return "CLI reachable. Run Login to verify authentication.";
286
+ if (status === "active") return loginUrl ? "Login completed." : "Active.";
287
+ if (status === "reachable") return "Active.";
242
288
  if (status === "stale") return "Authentication needs setup. Run Login, then run the sandbox again.";
243
289
  if (status === "missing") return `${name} not found. Install it and try again.`;
244
290
  if (status === "checking") return `Checking ${name}…`;
@@ -329,16 +375,18 @@ function runCommand({ binary, args, cwd, timeoutMs, stdin }) {
329
375
  // Public API — login / logout / status
330
376
  // ──────────────────────────────────────────────────────────────────────────
331
377
 
332
- async function runAgentLogin({ objectId, name }) {
378
+ async function runAgentLogin({ objectId, name, agentHost }) {
333
379
  const workspaceConfig = await readWorkspaceConfig();
334
380
  const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
335
381
  if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
336
382
  if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
337
383
 
338
- const { spec, agentHost } = assertAgentHostEligible(row, { requireLogin: true });
384
+ const selectedAgentHost = normalizeAgentHostOverride(agentHost);
385
+ const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
386
+ const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogin: true });
339
387
 
340
- const binary = resolveHostBinary(row, spec);
341
- const cwd = resolveCwd(row);
388
+ const binary = resolveHostBinary(effectiveRow, spec);
389
+ const cwd = resolveCwd(effectiveRow);
342
390
  const startedAt = Date.now();
343
391
 
344
392
  const result = await runCommand({
@@ -356,20 +404,21 @@ async function runAgentLogin({ objectId, name }) {
356
404
 
357
405
  const patch = buildRowPatch({
358
406
  status,
359
- provider: agentHost,
407
+ provider: effectiveAgentHost,
360
408
  checkedAt,
361
409
  exitCode: result.exitCode,
362
410
  loginUrl,
363
411
  label: spec.label,
364
412
  spawnError: result.spawnError
365
413
  });
414
+ Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
366
415
 
367
416
  await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
368
417
 
369
418
  return {
370
419
  ok: status === "active",
371
420
  status,
372
- provider: agentHost,
421
+ provider: effectiveAgentHost,
373
422
  label: spec.label,
374
423
  binary,
375
424
  cwd,
@@ -384,16 +433,18 @@ async function runAgentLogin({ objectId, name }) {
384
433
  };
385
434
  }
386
435
 
387
- async function runAgentLogout({ objectId, name }) {
436
+ async function runAgentLogout({ objectId, name, agentHost }) {
388
437
  const workspaceConfig = await readWorkspaceConfig();
389
438
  const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
390
439
  if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
391
440
  if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
392
441
 
393
- const { spec, agentHost } = assertAgentHostEligible(row, { requireLogout: true });
442
+ const selectedAgentHost = normalizeAgentHostOverride(agentHost);
443
+ const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
444
+ const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogout: true });
394
445
 
395
- const binary = resolveHostBinary(row, spec);
396
- const cwd = resolveCwd(row);
446
+ const binary = resolveHostBinary(effectiveRow, spec);
447
+ const cwd = resolveCwd(effectiveRow);
397
448
  const startedAt = Date.now();
398
449
 
399
450
  let exitCode = null;
@@ -422,7 +473,7 @@ async function runAgentLogout({ objectId, name }) {
422
473
 
423
474
  const patch = buildRowPatch({
424
475
  status,
425
- provider: agentHost,
476
+ provider: effectiveAgentHost,
426
477
  checkedAt,
427
478
  exitCode,
428
479
  loginUrl: null,
@@ -432,13 +483,14 @@ async function runAgentLogout({ objectId, name }) {
432
483
  patch.agentAuthLastMessage = spawnError?.notFound
433
484
  ? shortMessage({ status: "missing", label: spec.label })
434
485
  : `${spec.label} logged out — auth will be required before next run.`;
486
+ Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
435
487
 
436
488
  await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
437
489
 
438
490
  return {
439
491
  ok: !spawnError,
440
492
  status,
441
- provider: agentHost,
493
+ provider: effectiveAgentHost,
442
494
  label: spec.label,
443
495
  binary,
444
496
  cwd,
@@ -451,16 +503,18 @@ async function runAgentLogout({ objectId, name }) {
451
503
  };
452
504
  }
453
505
 
454
- async function checkAgentStatus({ objectId, name }) {
506
+ async function checkAgentStatus({ objectId, name, agentHost }) {
455
507
  const workspaceConfig = await readWorkspaceConfig();
456
508
  const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
457
509
  if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
458
510
  if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
459
511
 
460
- const { spec, agentHost } = assertAgentHostEligible(row);
512
+ const selectedAgentHost = normalizeAgentHostOverride(agentHost);
513
+ const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
514
+ const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow);
461
515
 
462
- const binary = resolveHostBinary(row, spec);
463
- const cwd = resolveCwd(row);
516
+ const binary = resolveHostBinary(effectiveRow, spec);
517
+ const cwd = resolveCwd(effectiveRow);
464
518
 
465
519
  // Two-phase probe:
466
520
  // 1. If the catalog declares an auth-status subcommand, try it first.
@@ -503,20 +557,21 @@ async function checkAgentStatus({ objectId, name }) {
503
557
 
504
558
  const patch = buildRowPatch({
505
559
  status,
506
- provider: agentHost,
560
+ provider: effectiveAgentHost,
507
561
  checkedAt,
508
562
  exitCode: usedResult.exitCode,
509
563
  loginUrl: null,
510
564
  label: spec.label,
511
565
  spawnError: usedResult.spawnError
512
566
  });
567
+ Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
513
568
 
514
569
  await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
515
570
 
516
571
  return {
517
572
  ok: status === "active",
518
573
  status,
519
- provider: agentHost,
574
+ provider: effectiveAgentHost,
520
575
  label: spec.label,
521
576
  binary,
522
577
  cwd,