@growthub/cli 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) 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-run/route.js +70 -9
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +18 -4
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +264 -22
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +129 -3
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +48 -9
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +6 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +23 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
  23. 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);
@@ -303,6 +690,7 @@ function normalizeRunConsoleRecord(record) {
303
690
  },
304
691
  lineage,
305
692
  swarm: record.swarm && typeof record.swarm === "object" ? record.swarm : null,
693
+ swarmRun: deriveSwarmRunProjection(record),
306
694
  logTree: buildRunLogTree(record)
307
695
  };
308
696
  }
@@ -387,6 +775,24 @@ function formatRunDuration(ms) {
387
775
  return `${minutes}m ${seconds}s`;
388
776
  }
389
777
 
778
+ /**
779
+ * Compact zero-padded duration for the swarm cockpit tables and cards —
780
+ * Claude Code Background-tasks format: "04s", "15s", "1m 04s". A separate
781
+ * formatter (not a change to formatRunDuration) so existing non-swarm
782
+ * run-console surfaces keep their ms-precision rendering untouched.
783
+ */
784
+ function formatCompactRunDuration(ms) {
785
+ if (ms == null) return "—";
786
+ const n = clampNumber(ms);
787
+ if (n == null) return "—";
788
+ const totalSeconds = Math.max(0, Math.round(n / 1000));
789
+ const minutes = Math.floor(totalSeconds / 60);
790
+ const seconds = totalSeconds % 60;
791
+ const padded = String(seconds).padStart(2, "0");
792
+ if (minutes <= 0) return `${padded}s`;
793
+ return `${minutes}m ${padded}s`;
794
+ }
795
+
390
796
  function downloadRunBundle({ record, runId, sourceId } = {}) {
391
797
  const normalized = normalizeRunConsoleRecord(record || {});
392
798
  return {
@@ -403,6 +809,10 @@ export {
403
809
  DEFAULT_EXPORT_TARGETS,
404
810
  normalizeRunConsoleRecord,
405
811
  deriveRunSummary,
812
+ deriveSwarmRunProjection,
813
+ deriveSwarmGraphProjection,
814
+ deriveSwarmDeltaProjection,
815
+ formatCompactRunDuration,
406
816
  deriveRunLifecycle,
407
817
  buildRunLogTree,
408
818
  buildRunTimeline,
@@ -40,6 +40,12 @@ const WORKSPACE_HELPER_PROPOSAL_TYPES = [
40
40
  // Server-file lane (AWaC: NOT a config PATCH field). Routed in helper/apply to
41
41
  // the confined, gated resolver write — never through writeWorkspaceConfig.
42
42
  "resolver.create",
43
+ // Swarm lane — governed sandbox-environment rows in the EXISTING dataModel
44
+ // patch field. Apply normalizes the intent payload into an agent-swarm-v1
45
+ // graph via buildDefaultAgentSwarmGraph; execution stays behind sandbox-run.
46
+ "swarm.run.propose",
47
+ "swarm.workflow.save",
48
+ "swarm.run.resume",
43
49
  ];
44
50
 
45
51
  const PROPOSAL_TYPE_TO_PATCH_FIELD = {
@@ -56,6 +62,10 @@ const PROPOSAL_TYPE_TO_PATCH_FIELD = {
56
62
  // Sentinel — resolver.create writes a server file, not a config field. It is
57
63
  // explicitly excluded from the PATCH allowlist and handled by its own lane.
58
64
  "resolver.create": "server-file",
65
+ // Swarm rows are dataModel rows. No new PATCH field is introduced.
66
+ "swarm.run.propose": "dataModel",
67
+ "swarm.workflow.save": "dataModel",
68
+ "swarm.run.resume": "dataModel",
59
69
  };
60
70
 
61
71
  const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
@@ -77,6 +87,8 @@ const INTENT_DESCRIPTIONS = {
77
87
  "Inspect the workspace snapshot for broken references, missing bindings, empty objects, or incomplete views. Propose the minimum changes needed to repair each issue.",
78
88
  explain:
79
89
  "Return a clear explanation of what one or more workspace objects, widgets, or configurations do. Use the explain.object proposal type — payload is { explanation: string }.",
90
+ swarm:
91
+ "Propose a governed agent swarm via swarm.run.propose. You are propose-only: do not execute, do not store credentials. Describe the swarm objective, agent roles, task prompts, tools, maxConcurrency, and outcomeCriteria as an intent payload — the server normalizes it into the governed agent-swarm-v1 graph and execution happens only through sandbox-run after the user applies the proposal.",
80
92
  };
81
93
 
82
94
  /**
@@ -91,6 +103,10 @@ const INTENT_DESCRIPTIONS = {
91
103
  * `create_object` when the prompt mentions both "object" and "API".
92
104
  */
93
105
  const INTENT_HEURISTIC_PATTERNS = [
106
+ { intent: "swarm", patterns: [
107
+ /\b(swarm|sub-?agents?|multi-?agent|agent\s+team|orchestrat(e|or|ion))\b/i,
108
+ /\b(run|launch|spawn|dispatch)\b.*\b(agents?|workers?)\b.*\b(parallel|swarm|workflow)\b/i,
109
+ ]},
94
110
  { intent: "register_api", patterns: [
95
111
  /\b(api|endpoint|webhook|integration|connector|oauth|bearer\s+token|auth\s+header)\b/i,
96
112
  /\b(register|connect|wire|hook\s*up)\b.*\b(api|endpoint|webhook|service|integration)\b/i,
@@ -202,6 +218,13 @@ function buildStableSystemPrompt(intent) {
202
218
  "- Reset invalid axis, filter, group, and sort settings when source changes.",
203
219
  "- Mark recomputed values as unsaved unless PATCH succeeds.",
204
220
  "",
221
+ "## When proposing agent swarms (swarm.run.propose)",
222
+ "- You may propose swarm.run.propose. You are propose-only. Do not execute. Do not store credentials.",
223
+ "- Describe the swarm objective, agent roles, tasks, tools, maxConcurrency, and outcomeCriteria.",
224
+ "- The server will normalize this into the governed agent-swarm graph; execution happens only through sandbox-run after apply.",
225
+ "- payload shape: { name, description?, objective, agents: [{ id?, role, description?, taskPrompt, tools?, required?, maxTokens?, timeoutMs? }], maxConcurrency?, outcomeCriteria?, runLocality?, agentHost?, adapter? }",
226
+ "- adapter, when set, must be local-agent-host or local-intelligence.",
227
+ "",
205
228
  "## Valid proposal types and their target patch field",
206
229
  WORKSPACE_HELPER_PROPOSAL_TYPES.map(
207
230
  (t) => ` ${t} → ${PROPOSAL_TYPE_TO_PATCH_FIELD[t]}`
@@ -545,8 +545,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
545
545
  const graphEdges = Array.isArray(graph?.edges) ? graph.edges : [];
546
546
  const workflowMetadataId = stableId("workflow", objectId, rowName);
547
547
  const sandboxMetadataId = stableId("sandbox", objectId, rowName);
548
- const agentHost = safeString(row.agentHost).trim();
549
- const adapter = safeString(row.adapter).trim();
548
+ const rowAgentHost = safeString(row.agentHost).trim();
549
+ const rowAdapter = safeString(row.adapter).trim();
550
550
  const inputSchema = graphNodes.length ? discoverRunInputSchema(graph) : { requiresInput: false, fields: [] };
551
551
  const inputFields = Array.isArray(inputSchema?.fields) ? inputSchema.fields : [];
552
552
 
@@ -560,8 +560,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
560
560
  lifecycleStatus: safeString(row.lifecycleStatus).trim() || "draft",
561
561
  version: safeString(row.version).trim() || "1",
562
562
  sandboxMetadataId,
563
- agentHost,
564
- adapter,
563
+ agentHost: rowAgentHost,
564
+ adapter: rowAdapter,
565
565
  runLocality: safeString(row.runLocality).trim(),
566
566
  nodeCount: graphNodes.length,
567
567
  edgeCount: graphEdges.length,
@@ -578,6 +578,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
578
578
  const sourceType = safeString(config.sourceType).trim();
579
579
  const sourceId = safeString(config.sourceId).trim();
580
580
  const integrationId = safeString(config.integrationId).trim();
581
+ const nodeAgentHost = safeString(config.agentHost || rowAgentHost).trim();
582
+ const nodeAdapter = safeString(config.adapter || rowAdapter).trim();
581
583
  const filterClauses = collectFilterClauses({ op: config.filterMode, clauses: config.filters });
582
584
  const writesObjectId = safeString(config.writeObjectId || config.targetObjectId).trim();
583
585
  const readsObjectId = sourceId || safeString(config.objectId).trim();
@@ -603,8 +605,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
603
605
  inputFieldCount: inputs.length,
604
606
  inputFieldIds: inputs.map((field) => field.id),
605
607
  sandboxMetadataId,
606
- agentHost,
607
- adapter,
608
+ agentHost: nodeAgentHost,
609
+ adapter: nodeAdapter,
608
610
  permissions: nodeType === "api-registry-call" ? ["integration:read"] : []
609
611
  });
610
612
  }