@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
@@ -0,0 +1,551 @@
1
+ /**
2
+ * Workspace Swarm Proposal V1 — the governed swarm lane for the helper.
3
+ *
4
+ * AWaC boundary: a swarm run is a governed object — a `sandbox-environment`
5
+ * row carrying an `agent-swarm-v1` orchestration graph. Swarm proposals
6
+ * therefore travel through the EXISTING dataModel patch lane (no new
7
+ * top-level PATCH allowlist field), and execution happens ONLY through
8
+ * POST /api/workspace/sandbox-run after an explicit, human-reviewed apply.
9
+ *
10
+ * The model is never trusted to hand-author the final orchestration graph.
11
+ * The helper proposes INTENT (objective, agent roles, task prompts, budgets);
12
+ * the server-side normalizer in helper/apply reduces that intent into a
13
+ * sandbox row via buildDefaultAgentSwarmGraph — the same builder every other
14
+ * swarm surface in this kit uses.
15
+ *
16
+ * This module is PURE (constants/validate/build/find/summarize). No React,
17
+ * no fetch, no fs, no config writes. The confined mutation lives in the
18
+ * helper apply route, gated by the same persistence rules as every other
19
+ * proposal lane.
20
+ *
21
+ * See docs/SWARM_RUN_CONTRACT_V1.md for the full contract.
22
+ */
23
+
24
+ import {
25
+ buildDefaultAgentSwarmGraph,
26
+ isAgentSwarmGraph,
27
+ parseOrchestrationGraph,
28
+ serializeOrchestrationGraph,
29
+ slugifyName,
30
+ validateAgentSwarmGraph,
31
+ } from "./orchestration-graph.js";
32
+ import { KNOWN_SANDBOX_AGENT_HOSTS } from "./workspace-schema.js";
33
+
34
+ const SWARM_RUN_PROPOSAL_TYPE = "swarm.run.propose";
35
+ const SWARM_WORKFLOW_SAVE_PROPOSAL_TYPE = "swarm.workflow.save";
36
+ const SWARM_RUN_RESUME_PROPOSAL_TYPE = "swarm.run.resume";
37
+
38
+ const SWARM_PROPOSAL_TYPES = [
39
+ SWARM_RUN_PROPOSAL_TYPE,
40
+ SWARM_WORKFLOW_SAVE_PROPOSAL_TYPE,
41
+ SWARM_RUN_RESUME_PROPOSAL_TYPE,
42
+ ];
43
+
44
+ // Swarm rows live in dataModel.objects[] — the existing patch lane. This is
45
+ // deliberately NOT a new top-level PATCH allowlist field.
46
+ const SWARM_AFFECTED_FIELD = "dataModel";
47
+
48
+ // Well-known governed object that hosts helper-proposed swarm workflow rows.
49
+ // Same well-known-id pattern as "helper-threads" / "nav-folders": a normal
50
+ // sandbox-environment object, persisted in growthub.config.json, validated by
51
+ // validateWorkspaceConfig, re-seeded if the user deletes it.
52
+ const SWARM_WORKFLOWS_OBJECT_ID = "swarm-workflows";
53
+ const SWARM_WORKFLOWS_LABEL = "Swarm Workflows";
54
+ const WORKSPACE_HELPER_SANDBOX_OBJECT_ID = "workspace-helper-sandbox";
55
+ const WORKSPACE_HELPER_ROW_NAME = "workspace-helper";
56
+
57
+ const SWARM_ALLOWED_ADAPTERS = new Set(["local-agent-host", "local-intelligence"]);
58
+ const SWARM_MAX_AGENTS = 24;
59
+ const SWARM_DEFAULT_TIMEOUT_MS = 120000;
60
+ const SWARM_EXECUTION_TARGET_FIELDS = [
61
+ "runLocality",
62
+ "schedulerRegistryId",
63
+ "runtime",
64
+ "adapter",
65
+ "agentHost",
66
+ "localModel",
67
+ "localEndpoint",
68
+ "intelligenceAdapterMode",
69
+ "timeoutMs",
70
+ "networkAllow",
71
+ "allowList",
72
+ ];
73
+
74
+ function clean(value) {
75
+ return String(value == null ? "" : value).trim();
76
+ }
77
+
78
+ function clampPositiveInt(value, fallback = 0) {
79
+ const n = Number(value);
80
+ if (!Number.isFinite(n) || n <= 0) return fallback;
81
+ return Math.floor(n);
82
+ }
83
+
84
+ // agentHost must be a registered host slug or empty — model-invented host
85
+ // names are dropped (the row falls back to the adapter default) so the
86
+ // normalized row always passes validateSandboxEnvironmentRow.
87
+ function sanitizeAgentHost(value) {
88
+ const host = clean(value);
89
+ return KNOWN_SANDBOX_AGENT_HOSTS.includes(host) ? host : "";
90
+ }
91
+
92
+ function findWorkspaceHelperSandboxRow(workspaceConfig) {
93
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
94
+ const object = objects.find((item) => item?.id === WORKSPACE_HELPER_SANDBOX_OBJECT_ID);
95
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
96
+ return rows.find((row) => clean(row?.Name) === WORKSPACE_HELPER_ROW_NAME) || rows[0] || null;
97
+ }
98
+
99
+ function deriveHelperWidgetCausationState(workspaceConfig) {
100
+ const row = findWorkspaceHelperSandboxRow(workspaceConfig);
101
+ const lifecycleStatus = clean(row?.lifecycleStatus).toLowerCase() || "draft";
102
+ const runLocality = normalizeRunLocality(row?.runLocality);
103
+ const adapter = sanitizeSwarmAdapter(row?.adapter, "");
104
+ const agentHost = sanitizeAgentHost(row?.agentHost);
105
+ const missing = [];
106
+ if (!row) missing.push("workspace helper sandbox");
107
+ if (row && lifecycleStatus !== "live") missing.push("live helper status");
108
+ if (row && runLocality !== "local") missing.push("local helper runtime");
109
+ if (row && !adapter) missing.push("prompt-capable helper adapter");
110
+ if (adapter === "local-agent-host" && !agentHost) missing.push("helper agent host");
111
+ const ready = missing.length === 0;
112
+ return {
113
+ ready,
114
+ status: ready ? "ready" : "blocked",
115
+ row,
116
+ adapter,
117
+ agentHost,
118
+ runLocality,
119
+ lifecycleStatus,
120
+ missing,
121
+ guidance: ready
122
+ ? `Helper is live on ${adapter}${agentHost ? ` using ${agentHost}` : ""}.`
123
+ : `Set up the live workspace helper first: ${missing.join(", ")}.`,
124
+ };
125
+ }
126
+
127
+ function sanitizeSwarmAdapter(value, fallback = "local-intelligence") {
128
+ const adapter = clean(value);
129
+ return SWARM_ALLOWED_ADAPTERS.has(adapter) ? adapter : fallback;
130
+ }
131
+
132
+ function normalizeRunLocality(value) {
133
+ return clean(value).toLowerCase() === "serverless" ? "serverless" : "local";
134
+ }
135
+
136
+ function resolveSwarmExecutionTarget(workspaceConfig, payload = {}) {
137
+ const helperState = deriveHelperWidgetCausationState(workspaceConfig);
138
+ const helperRow = helperState.ready ? helperState.row : null;
139
+ const helperAdapter = sanitizeSwarmAdapter(helperRow?.adapter, "");
140
+ const payloadAdapter = sanitizeSwarmAdapter(payload?.adapter, "");
141
+ const adapter = helperAdapter || payloadAdapter || "local-intelligence";
142
+ const runLocality = normalizeRunLocality(helperRow?.runLocality || payload?.runLocality);
143
+ const target = {
144
+ runLocality: "local",
145
+ schedulerRegistryId: "",
146
+ runtime: clean(helperRow?.runtime || payload?.runtime || "node") || "node",
147
+ adapter,
148
+ agentHost: "",
149
+ localModel: clean(helperRow?.localModel || payload?.localModel),
150
+ localEndpoint: clean(helperRow?.localEndpoint || payload?.localEndpoint),
151
+ intelligenceAdapterMode: clean(helperRow?.intelligenceAdapterMode || payload?.intelligenceAdapterMode),
152
+ timeoutMs: String(clampPositiveInt(payload?.timeoutMs || helperRow?.timeoutMs, SWARM_DEFAULT_TIMEOUT_MS)),
153
+ networkAllow: clean(payload?.networkAllow || helperRow?.networkAllow),
154
+ allowList: clean(payload?.allowList || helperRow?.allowList),
155
+ inheritedFromObjectId: helperRow ? WORKSPACE_HELPER_SANDBOX_OBJECT_ID : "",
156
+ inheritedFromName: helperRow ? clean(helperRow.Name || WORKSPACE_HELPER_ROW_NAME) : "",
157
+ };
158
+ if (adapter === "local-agent-host") {
159
+ target.agentHost = sanitizeAgentHost(helperRow?.agentHost) || sanitizeAgentHost(payload?.agentHost);
160
+ }
161
+ // Swarm workflows execute through prompt-capable local adapters. Serverless
162
+ // helper setup is not a runnable first-run target for agent-swarm-v1 because
163
+ // the proposal payload does not carry a scheduler registry contract.
164
+ if (runLocality === "serverless") {
165
+ target.runLocality = "local";
166
+ target.schedulerRegistryId = "";
167
+ }
168
+ return target;
169
+ }
170
+
171
+ function applyExecutionTargetToSwarmGraph(graph, executionTarget) {
172
+ const agentHost = sanitizeAgentHost(executionTarget?.agentHost);
173
+ const adapter = sanitizeSwarmAdapter(executionTarget?.adapter, "");
174
+ if ((!agentHost && !adapter) || !graph || typeof graph !== "object" || !Array.isArray(graph.nodes)) return graph;
175
+ return {
176
+ ...graph,
177
+ nodes: graph.nodes.map((node) => {
178
+ if (node?.type !== "ai-agent" && node?.type !== "tool-result") return node;
179
+ const config = node.config && typeof node.config === "object" ? node.config : {};
180
+ return {
181
+ ...node,
182
+ config: {
183
+ ...config,
184
+ adapter: sanitizeSwarmAdapter(config.adapter, "") || adapter,
185
+ agentHost: sanitizeAgentHost(config.agentHost) || agentHost,
186
+ },
187
+ };
188
+ }),
189
+ };
190
+ }
191
+
192
+ function deriveSwarmWorkflowExecutionEligibility(entryOrRow) {
193
+ const row = entryOrRow?.row && typeof entryOrRow.row === "object" ? entryOrRow.row : entryOrRow;
194
+ const adapter = sanitizeSwarmAdapter(row?.adapter, "");
195
+ const runLocality = normalizeRunLocality(row?.runLocality);
196
+ const agentHost = sanitizeAgentHost(row?.agentHost);
197
+ const graph = parseOrchestrationGraph(row?.orchestrationConfig || row?.orchestrationGraph);
198
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
199
+ const runnableNodes = nodes.filter((node) => node?.type === "ai-agent" || node?.type === "tool-result");
200
+ const missing = [];
201
+ if (!graph || !isAgentSwarmGraph(graph)) missing.push("agent-swarm-v1 graph");
202
+ if (runLocality !== "local") missing.push("local run target");
203
+ if (!adapter) missing.push("prompt-capable adapter");
204
+ if (adapter === "local-agent-host" && !agentHost) missing.push("agent host");
205
+ for (const node of runnableNodes) {
206
+ const config = node.config && typeof node.config === "object" ? node.config : {};
207
+ const nodeAdapter = sanitizeSwarmAdapter(config.adapter, "") || adapter;
208
+ const nodeHost = sanitizeAgentHost(config.agentHost) || agentHost;
209
+ if (!nodeAdapter) missing.push(`${clean(node.label || node.id) || "subagent"} adapter`);
210
+ if (nodeAdapter === "local-agent-host" && !nodeHost) missing.push(`${clean(node.label || node.id) || "subagent"} agent host`);
211
+ }
212
+ const uniqueMissing = Array.from(new Set(missing));
213
+ const ready = uniqueMissing.length === 0;
214
+ return {
215
+ ready,
216
+ status: ready ? "ready" : "blocked",
217
+ adapter,
218
+ agentHost,
219
+ runLocality,
220
+ runnableNodeCount: runnableNodes.length,
221
+ missing: uniqueMissing,
222
+ guidance: ready
223
+ ? `Ready to run through ${adapter}${agentHost ? ` using ${agentHost}` : ""}.`
224
+ : `Set ${uniqueMissing.join(", ")} before running this swarm workflow.`,
225
+ };
226
+ }
227
+
228
+ /**
229
+ * Validate a swarm proposal envelope before the apply lane normalizes it.
230
+ * Returns { ok, error } with concrete user-facing messages. Resume proposals
231
+ * only need a target name; propose/save need a full intent payload.
232
+ */
233
+ function validateSwarmRunProposal(proposal) {
234
+ if (!proposal || typeof proposal !== "object") {
235
+ return { ok: false, error: "swarm proposal must be an object" };
236
+ }
237
+ if (!SWARM_PROPOSAL_TYPES.includes(proposal.type)) {
238
+ return { ok: false, error: `not a swarm proposal type: "${proposal.type}"` };
239
+ }
240
+ if (proposal.affectedField && proposal.affectedField !== SWARM_AFFECTED_FIELD) {
241
+ return { ok: false, error: `swarm proposals must target affectedField "${SWARM_AFFECTED_FIELD}"` };
242
+ }
243
+ const payload = proposal.payload;
244
+ if (!payload || typeof payload !== "object") {
245
+ return { ok: false, error: "swarm proposal payload must be an object" };
246
+ }
247
+
248
+ if (proposal.type === SWARM_RUN_RESUME_PROPOSAL_TYPE) {
249
+ if (!clean(payload.name)) {
250
+ return { ok: false, error: "swarm.run.resume requires payload.name (the workflow row to resume)" };
251
+ }
252
+ return { ok: true, error: null };
253
+ }
254
+
255
+ if (!clean(payload.name)) {
256
+ return { ok: false, error: "swarm proposal requires payload.name" };
257
+ }
258
+ if (!clean(payload.objective)) {
259
+ return { ok: false, error: "swarm proposal requires payload.objective" };
260
+ }
261
+ const agents = Array.isArray(payload.agents) ? payload.agents : [];
262
+ if (agents.length === 0) {
263
+ return { ok: false, error: "swarm proposal requires at least one agent" };
264
+ }
265
+ if (agents.length > SWARM_MAX_AGENTS) {
266
+ return { ok: false, error: `swarm proposal exceeds the ${SWARM_MAX_AGENTS}-agent ceiling` };
267
+ }
268
+ for (let i = 0; i < agents.length; i += 1) {
269
+ const agent = agents[i];
270
+ if (!agent || typeof agent !== "object") {
271
+ return { ok: false, error: `agents[${i}] must be an object` };
272
+ }
273
+ if (!clean(agent.role)) {
274
+ return { ok: false, error: `agents[${i}] must declare a role` };
275
+ }
276
+ if (!clean(agent.taskPrompt)) {
277
+ return { ok: false, error: `agents[${i}] ("${clean(agent.role)}") must declare a taskPrompt` };
278
+ }
279
+ }
280
+ const adapter = clean(payload.adapter);
281
+ if (adapter && !SWARM_ALLOWED_ADAPTERS.has(adapter)) {
282
+ return {
283
+ ok: false,
284
+ error: `swarm adapter "${adapter}" cannot execute prompts; use local-agent-host or local-intelligence`,
285
+ };
286
+ }
287
+ const runLocality = clean(payload.runLocality);
288
+ if (runLocality && runLocality !== "local" && runLocality !== "serverless") {
289
+ return { ok: false, error: `runLocality must be "local" or "serverless", got "${runLocality}"` };
290
+ }
291
+ // Secrets never travel in swarm payloads — env-ref slugs only, and even
292
+ // those are not part of the V1 intent shape.
293
+ const flat = JSON.stringify(payload);
294
+ if (/"(apiKey|api_key|secret|password|bearerToken)"\s*:/i.test(flat)) {
295
+ return { ok: false, error: "swarm proposal payload must not carry credential-shaped fields" };
296
+ }
297
+ return { ok: true, error: null };
298
+ }
299
+
300
+ /**
301
+ * Normalize one intent-shaped agent entry into the subagent shape
302
+ * buildDefaultAgentSwarmGraph consumes.
303
+ */
304
+ function normalizeSwarmAgent(agent, index, fallbackAgentHost) {
305
+ const role = clean(agent.role) || `Agent ${index + 1}`;
306
+ const id = slugifyName(clean(agent.id) || role) || `subagent-${index + 1}`;
307
+ return {
308
+ id: id.startsWith("subagent-") || id.startsWith("agent-") ? id : `subagent-${id}`,
309
+ role,
310
+ description: clean(agent.description),
311
+ taskPrompt: clean(agent.taskPrompt),
312
+ // Optional author-named cockpit phase (e.g. "ping"). Slugged; empty
313
+ // falls back to the single Dispatch phase.
314
+ phase: slugifyName(clean(agent.phase || agent.phaseId)) || "",
315
+ tools: Array.isArray(agent.tools) ? agent.tools.map((t) => clean(t)).filter(Boolean) : [],
316
+ required: agent.required !== false,
317
+ agentHost: sanitizeAgentHost(agent.agentHost) || sanitizeAgentHost(fallbackAgentHost),
318
+ adapter: SWARM_ALLOWED_ADAPTERS.has(clean(agent.adapter)) ? clean(agent.adapter) : "",
319
+ networkAccess: agent.networkAccess === true,
320
+ maxTokens: clampPositiveInt(agent.maxTokens, 0),
321
+ timeoutMs: clampPositiveInt(agent.timeoutMs, 0),
322
+ };
323
+ }
324
+
325
+ /**
326
+ * Build an inert swarm.run.propose proposal envelope from intent input.
327
+ * Nothing executes and nothing is written until the user reviews and applies.
328
+ */
329
+ function buildSwarmRunProposal(input = {}) {
330
+ const name = clean(input.name) || "Agent Swarm";
331
+ const agents = (Array.isArray(input.agents) ? input.agents : [])
332
+ .map((agent, index) => normalizeSwarmAgent(agent || {}, index, input.agentHost));
333
+ return {
334
+ type: SWARM_RUN_PROPOSAL_TYPE,
335
+ affectedField: SWARM_AFFECTED_FIELD,
336
+ rationale:
337
+ clean(input.rationale)
338
+ || `Create the governed "${name}" swarm workflow (${agents.length} agent${agents.length === 1 ? "" : "s"}). Execution stays behind sandbox-run after apply.`,
339
+ confidence: typeof input.confidence === "number" ? input.confidence : 0.85,
340
+ payload: {
341
+ name,
342
+ description: clean(input.description),
343
+ objective: clean(input.objective),
344
+ agents,
345
+ maxConcurrency: clampPositiveInt(input.maxConcurrency, Math.max(1, agents.length)),
346
+ outcomeCriteria: clean(input.outcomeCriteria),
347
+ runLocality: clean(input.runLocality) === "serverless" ? "serverless" : "local",
348
+ agentHost: sanitizeAgentHost(input.agentHost),
349
+ adapter: SWARM_ALLOWED_ADAPTERS.has(clean(input.adapter)) ? clean(input.adapter) : "local-intelligence",
350
+ },
351
+ };
352
+ }
353
+
354
+ /**
355
+ * Reduce a validated swarm proposal into a governed sandbox-environment row.
356
+ * The orchestration graph is ALWAYS produced by buildDefaultAgentSwarmGraph —
357
+ * model-authored graph JSON is never trusted verbatim. If the payload carries
358
+ * an `orchestrationGraph`, it is only honored when it parses AND validates as
359
+ * an agent-swarm-v1 graph; otherwise it is discarded and rebuilt from intent.
360
+ */
361
+ function buildSandboxRowFromSwarmProposal(workspaceConfig, proposal) {
362
+ const payload = proposal?.payload || {};
363
+ const name = clean(payload.name) || "Agent Swarm";
364
+ const executionTarget = resolveSwarmExecutionTarget(workspaceConfig, payload);
365
+ const adapter = executionTarget.adapter;
366
+ const agents = (Array.isArray(payload.agents) ? payload.agents : [])
367
+ .map((agent, index) => normalizeSwarmAgent(agent || {}, index, executionTarget.agentHost));
368
+
369
+ let graph = null;
370
+ if (payload.orchestrationGraph) {
371
+ const candidate = parseOrchestrationGraph(payload.orchestrationGraph);
372
+ if (candidate && isAgentSwarmGraph(candidate) && validateAgentSwarmGraph(candidate).ok) {
373
+ graph = candidate;
374
+ }
375
+ }
376
+ if (!graph) {
377
+ graph = buildDefaultAgentSwarmGraph({
378
+ agentHost: executionTarget.agentHost,
379
+ subagents: agents,
380
+ orchestratorPrompt: clean(payload.objective),
381
+ outcomeCriteria: clean(payload.outcomeCriteria),
382
+ maxConcurrency: clampPositiveInt(payload.maxConcurrency, Math.max(1, agents.length)),
383
+ });
384
+ }
385
+ graph = applyExecutionTargetToSwarmGraph(graph, executionTarget);
386
+
387
+ // Swarm phases dispatch through prompt-capable LOCAL adapters only —
388
+ // serverless locality would also require a schedulerRegistryId the swarm
389
+ // intent payload never carries. Pin local.
390
+ const runLocality = executionTarget.runLocality;
391
+
392
+ return {
393
+ Name: name,
394
+ slug: slugifyName(name) || "agent-swarm",
395
+ objectType: "sandbox-environment",
396
+ lifecycleStatus: "draft",
397
+ version: "1",
398
+ runLocality,
399
+ schedulerRegistryId: executionTarget.schedulerRegistryId,
400
+ runtime: executionTarget.runtime,
401
+ adapter,
402
+ agentHost: executionTarget.agentHost,
403
+ localModel: executionTarget.localModel,
404
+ localEndpoint: executionTarget.localEndpoint,
405
+ intelligenceAdapterMode: executionTarget.intelligenceAdapterMode,
406
+ envRefs: "",
407
+ networkAllow: executionTarget.networkAllow,
408
+ allowList: executionTarget.allowList,
409
+ instructions: clean(payload.objective),
410
+ command: "",
411
+ timeoutMs: executionTarget.timeoutMs,
412
+ status: "untested",
413
+ lastTested: "",
414
+ lastRunId: "",
415
+ lastSourceId: "",
416
+ lastResponse: "",
417
+ orchestrationConfig: serializeOrchestrationGraph(graph),
418
+ description: clean(payload.description) || clean(payload.objective),
419
+ };
420
+ }
421
+
422
+ /**
423
+ * Ensure the well-known Swarm Workflows governed object exists. Mirrors
424
+ * ensureHelperThreadsObject — never overwrites an existing object's fields.
425
+ */
426
+ function ensureSwarmWorkflowsObject(config) {
427
+ const dm = config?.dataModel && typeof config.dataModel === "object" ? config.dataModel : {};
428
+ const objects = Array.isArray(dm.objects) ? dm.objects.slice() : [];
429
+ const idx = objects.findIndex((o) => o?.id === SWARM_WORKFLOWS_OBJECT_ID);
430
+ if (idx >= 0) {
431
+ const existing = objects[idx];
432
+ if (!Array.isArray(existing.rows)) {
433
+ objects[idx] = { ...existing, rows: [] };
434
+ }
435
+ return { ...config, dataModel: { ...dm, objects } };
436
+ }
437
+ const seeded = {
438
+ id: SWARM_WORKFLOWS_OBJECT_ID,
439
+ label: SWARM_WORKFLOWS_LABEL,
440
+ source: SWARM_WORKFLOWS_LABEL,
441
+ objectType: "sandbox-environment",
442
+ icon: "Workflow",
443
+ columns: [
444
+ "Name",
445
+ "lifecycleStatus",
446
+ ...SWARM_EXECUTION_TARGET_FIELDS,
447
+ "status",
448
+ "lastTested",
449
+ "description",
450
+ ],
451
+ rows: [],
452
+ binding: { mode: "manual", source: SWARM_WORKFLOWS_LABEL },
453
+ };
454
+ return { ...config, dataModel: { ...dm, objects: [...objects, seeded] } };
455
+ }
456
+
457
+ /**
458
+ * Upsert a swarm sandbox row (by Name) into the Swarm Workflows object.
459
+ * Returns the next config — never mutates. On update, run-history stamps
460
+ * (status, lastRunId, lastSourceId, lastResponse, lastTested) are preserved
461
+ * so re-saving a workflow does not erase its audit trail.
462
+ */
463
+ function upsertSwarmRunRow(config, row) {
464
+ const withObject = ensureSwarmWorkflowsObject(config);
465
+ const dm = withObject.dataModel;
466
+ const objects = dm.objects.slice();
467
+ const idx = objects.findIndex((o) => o?.id === SWARM_WORKFLOWS_OBJECT_ID);
468
+ if (idx === -1) return withObject;
469
+ const obj = objects[idx];
470
+ const rows = Array.isArray(obj.rows) ? obj.rows.slice() : [];
471
+ const rowIdx = rows.findIndex((r) => clean(r?.Name) === clean(row?.Name));
472
+ if (rowIdx >= 0) {
473
+ const prior = rows[rowIdx];
474
+ rows[rowIdx] = {
475
+ ...prior,
476
+ ...row,
477
+ status: prior.status || row.status,
478
+ lastTested: prior.lastTested || "",
479
+ lastRunId: prior.lastRunId || "",
480
+ lastSourceId: prior.lastSourceId || "",
481
+ lastResponse: prior.lastResponse || "",
482
+ };
483
+ } else {
484
+ rows.push(row);
485
+ }
486
+ objects[idx] = { ...obj, rows };
487
+ return { ...withObject, dataModel: { ...dm, objects } };
488
+ }
489
+
490
+ /**
491
+ * Find governed swarm rows across EVERY sandbox-environment object — not just
492
+ * the well-known one — so swarms created through the orchestration editor
493
+ * surface in the cockpit too. Criteria: { name?, objectId? }.
494
+ *
495
+ * Returns [{ objectId, objectLabel, row, graph }].
496
+ */
497
+ function findSwarmRunRows(workspaceConfig, criteria = {}) {
498
+ const wantedName = clean(criteria.name);
499
+ const wantedObjectId = clean(criteria.objectId);
500
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
501
+ const out = [];
502
+ for (const object of objects) {
503
+ if (object?.objectType !== "sandbox-environment") continue;
504
+ if (wantedObjectId && clean(object.id) !== wantedObjectId) continue;
505
+ for (const row of Array.isArray(object.rows) ? object.rows : []) {
506
+ if (wantedName && clean(row?.Name) !== wantedName) continue;
507
+ const graph = parseOrchestrationGraph(row?.orchestrationConfig || row?.orchestrationGraph);
508
+ if (!graph || !isAgentSwarmGraph(graph)) continue;
509
+ out.push({ objectId: clean(object.id), objectLabel: clean(object.label), row, graph });
510
+ }
511
+ }
512
+ return out;
513
+ }
514
+
515
+ /**
516
+ * One-line human summary for proposal review cards and receipts.
517
+ */
518
+ function summarizeSwarmRunProposal(proposal) {
519
+ const payload = proposal?.payload || {};
520
+ if (proposal?.type === SWARM_RUN_RESUME_PROPOSAL_TYPE) {
521
+ return clean(payload.name) ? `resume: ${clean(payload.name)}` : "resume swarm run";
522
+ }
523
+ const agents = Array.isArray(payload.agents) ? payload.agents : [];
524
+ const parts = [
525
+ clean(payload.name) ? `name: ${clean(payload.name)}` : null,
526
+ agents.length ? `${agents.length} agent${agents.length === 1 ? "" : "s"}` : null,
527
+ payload.maxConcurrency ? `concurrency: ${clampPositiveInt(payload.maxConcurrency, 1)}` : null,
528
+ clean(payload.adapter) ? `adapter: ${clean(payload.adapter)}` : null,
529
+ ];
530
+ return parts.filter(Boolean).join(" · ");
531
+ }
532
+
533
+ export {
534
+ SWARM_RUN_PROPOSAL_TYPE,
535
+ SWARM_WORKFLOW_SAVE_PROPOSAL_TYPE,
536
+ SWARM_RUN_RESUME_PROPOSAL_TYPE,
537
+ SWARM_PROPOSAL_TYPES,
538
+ SWARM_AFFECTED_FIELD,
539
+ SWARM_WORKFLOWS_OBJECT_ID,
540
+ SWARM_WORKFLOWS_LABEL,
541
+ SWARM_EXECUTION_TARGET_FIELDS,
542
+ deriveHelperWidgetCausationState,
543
+ validateSwarmRunProposal,
544
+ buildSwarmRunProposal,
545
+ buildSandboxRowFromSwarmProposal,
546
+ deriveSwarmWorkflowExecutionEligibility,
547
+ ensureSwarmWorkflowsObject,
548
+ upsertSwarmRunRow,
549
+ findSwarmRunRows,
550
+ summarizeSwarmRunProposal,
551
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthub/cli",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "CLI control plane for Growthub Local and Agent Workspace as Code: export, fork, inspect, operate, sync, and optionally activate governed AI workspaces.",
5
5
  "type": "module",
6
6
  "bin": {