@growthub/cli 0.13.4 → 0.13.5

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/metadata-graph/route.js +184 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +25 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +326 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +6 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +88 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +41 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx +226 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +16 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +49 -4
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +14 -1
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +923 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +14 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +216 -5
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +28 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-inputs.js +43 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +3 -1
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +36 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-chart-values.js +53 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-graph.js +646 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-selectors.js +249 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +1186 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  23. package/package.json +1 -1
@@ -8,12 +8,14 @@ import {
8
8
  extractApiRegistryCallNode,
9
9
  extractInputNode,
10
10
  extractTransformConfig,
11
+ isAgentSwarmGraph,
11
12
  normalizeJsonAtPath,
12
13
  parseOrchestrationGraph,
13
14
  redactSecretsFromText,
14
15
  substituteVariables
15
16
  } from "./orchestration-graph.js";
16
17
  import { buildInputPayloadForRunner } from "./orchestration-run-inputs.js";
18
+ import { runAgentSwarmGraphIfPresent } from "./orchestration-agent-swarm.js";
17
19
 
18
20
  function normalizeMethod(value) {
19
21
  const method = String(value || "GET").trim().toUpperCase();
@@ -262,10 +264,21 @@ async function executeApiRegistryCall(workspaceConfig, nodeConfig, inputPayload,
262
264
  * for `human-input` / form workflows. Secret values (those stored as
263
265
  * `{ secretRef }`) are never expanded into the runner payload.
264
266
  */
265
- async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs, runInputs }) {
267
+ async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs, runInputs, executionContext }) {
266
268
  const graph = parseOrchestrationGraph(row?.orchestrationGraph || row?.orchestrationConfig);
267
269
  if (!graph || String(graph.provider || "").trim() !== "growthub-native") return null;
268
270
 
271
+ if (isAgentSwarmGraph(graph)) {
272
+ return await runAgentSwarmGraphIfPresent({
273
+ workspaceConfig,
274
+ row,
275
+ graph,
276
+ timeoutMs,
277
+ runInputs,
278
+ executionContext
279
+ });
280
+ }
281
+
269
282
  const apiNode = extractApiRegistryCallNode(graph);
270
283
  if (!apiNode?.config) {
271
284
  return {
@@ -151,11 +151,16 @@ function validateOrchestrationGraph(graph) {
151
151
  errors.push(`${prefix}.type "${type}" is not a known node type`);
152
152
  }
153
153
  });
154
- const hasThinAdapter = graph.nodes.some((n) => n?.type === "thinAdapter");
155
- const hasApi = graph.nodes.some((n) => n?.type === "api-registry-call");
156
- const hasResult = graph.nodes.some((n) => n?.type === "tool-result");
157
- if (!hasThinAdapter && !hasApi) errors.push("orchestrationGraph requires an api-registry-call node");
158
- if (!hasThinAdapter && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
154
+ if (isAgentSwarmGraph(graph)) {
155
+ const swarmCheck = validateAgentSwarmGraph(graph);
156
+ if (!swarmCheck.ok) errors.push(...swarmCheck.errors);
157
+ } else {
158
+ const hasThinAdapter = graph.nodes.some((n) => n?.type === "thinAdapter");
159
+ const hasApi = graph.nodes.some((n) => n?.type === "api-registry-call");
160
+ const hasResult = graph.nodes.some((n) => n?.type === "tool-result");
161
+ if (!hasThinAdapter && !hasApi) errors.push("orchestrationGraph requires an api-registry-call node");
162
+ if (!hasThinAdapter && !hasResult) errors.push("orchestrationGraph requires a tool-result node");
163
+ }
159
164
  }
160
165
  if (!Array.isArray(graph.edges)) {
161
166
  errors.push("orchestrationGraph.edges must be an array");
@@ -680,6 +685,207 @@ function addCanonicalNodeToGraph(graph, nodeId, registryRow, options = {}) {
680
685
  return { ...parsed, nodes, edges };
681
686
  }
682
687
 
688
+ const AGENT_SWARM_EXECUTION_MODE = "agent-swarm-v1";
689
+
690
+ /**
691
+ * Detect whether a graph is an agent-swarm-v1 control plane. Swarm graphs are
692
+ * encoded as growthub-native graphs whose root carries
693
+ * `executionMode: "agent-swarm-v1"` and that contain at least one orchestrator
694
+ * (`thinAdapter`) or subagent (`ai-agent`) node.
695
+ */
696
+ function isAgentSwarmGraph(graph) {
697
+ const parsed = parseOrchestrationGraph(graph) || graph;
698
+ if (!parsed || typeof parsed !== "object") return false;
699
+ if (String(parsed.provider || "").trim() !== "growthub-native") return false;
700
+ if (String(parsed.executionMode || "").trim() !== AGENT_SWARM_EXECUTION_MODE) return false;
701
+ const nodes = Array.isArray(parsed.nodes) ? parsed.nodes : [];
702
+ if (!nodes.length) return false;
703
+ return nodes.some((n) => n?.type === "thinAdapter" || n?.type === "ai-agent");
704
+ }
705
+
706
+ /**
707
+ * Split a swarm graph into its semantic parts so the runtime and UI can reason
708
+ * about it without re-walking the node list. Returns `null` when the graph is
709
+ * not a recognized swarm.
710
+ */
711
+ function extractSwarmNodes(graph) {
712
+ const parsed = parseOrchestrationGraph(graph) || graph;
713
+ if (!isAgentSwarmGraph(parsed)) return null;
714
+ const nodes = Array.isArray(parsed.nodes) ? parsed.nodes : [];
715
+ const orchestrator = nodes.find((n) => n?.type === "thinAdapter") || null;
716
+ const subagents = nodes.filter((n) => n?.type === "ai-agent");
717
+ const synthesis = nodes.find((n) => n?.type === "tool-result") || null;
718
+ const humanInputs = nodes.filter((n) => n?.type === "human-input");
719
+ const flowControls = nodes.filter((n) => n?.type === "flow-control");
720
+ const swarmConfig = (parsed.swarm && typeof parsed.swarm === "object" && !Array.isArray(parsed.swarm))
721
+ ? parsed.swarm
722
+ : {};
723
+ return {
724
+ graph: parsed,
725
+ orchestrator,
726
+ subagents,
727
+ synthesis,
728
+ humanInputs,
729
+ flowControls,
730
+ swarmConfig
731
+ };
732
+ }
733
+
734
+ /**
735
+ * Build the default scaffold for a new agent-swarm-v1 graph. The shape mirrors
736
+ * the Kimi screenshots — an orchestrator, two specialized subagents, a
737
+ * synthesis tool-result — but reuses existing node types so no schema change
738
+ * is required.
739
+ */
740
+ function buildDefaultAgentSwarmGraph(options = {}) {
741
+ const agentHost = String(options.agentHost || "").trim();
742
+ const subagents = Array.isArray(options.subagents) && options.subagents.length > 0
743
+ ? options.subagents
744
+ : [
745
+ {
746
+ id: "subagent-researcher",
747
+ role: "Researcher",
748
+ description: "Gathers facts from the run input and the orchestrator's plan.",
749
+ taskPrompt: "Investigate the orchestrator's plan and gather the relevant facts.",
750
+ tools: ["read", "summarize"],
751
+ required: true
752
+ },
753
+ {
754
+ id: "subagent-analyst",
755
+ role: "Analyst",
756
+ description: "Stress-tests assumptions and surfaces risks.",
757
+ taskPrompt: "Identify risks, assumptions, and dependencies in the orchestrator's plan.",
758
+ tools: ["read", "critique"],
759
+ required: true
760
+ }
761
+ ];
762
+
763
+ const nodes = [
764
+ {
765
+ id: "orchestrator",
766
+ type: "thinAdapter",
767
+ label: "Orchestrator",
768
+ subtitle: "Plans subagent dispatch",
769
+ sandbox: "orchestrator",
770
+ config: {
771
+ executionPolicy: "parallel",
772
+ prompt: String(options.orchestratorPrompt || "Decompose the task into independent subtasks for the listed subagents.").trim(),
773
+ inputBinding: "{{input.payload}}",
774
+ outputKey: "plan"
775
+ }
776
+ },
777
+ ...subagents.map((agent) => ({
778
+ id: String(agent.id || agent.role || "subagent").replace(/[^a-zA-Z0-9_-]+/g, "-"),
779
+ type: "ai-agent",
780
+ label: String(agent.role || agent.id || "Subagent"),
781
+ subtitle: "Swarm subagent",
782
+ config: {
783
+ role: String(agent.role || agent.id || "Subagent"),
784
+ description: String(agent.description || "").trim(),
785
+ taskPrompt: String(agent.taskPrompt || "").trim(),
786
+ tools: Array.isArray(agent.tools) ? agent.tools.map((t) => String(t || "").trim()).filter(Boolean) : [],
787
+ agentHost: String(agent.agentHost || agentHost || "").trim(),
788
+ adapter: String(agent.adapter || "").trim(),
789
+ required: agent.required !== false,
790
+ canReadWorkspace: true,
791
+ canWriteDraft: false,
792
+ networkAccess: agent.networkAccess === true,
793
+ maxTokens: Number.isFinite(Number(agent.maxTokens)) && Number(agent.maxTokens) > 0
794
+ ? Math.floor(Number(agent.maxTokens))
795
+ : 0,
796
+ timeoutMs: Number.isFinite(Number(agent.timeoutMs)) && Number(agent.timeoutMs) > 0
797
+ ? Math.floor(Number(agent.timeoutMs))
798
+ : 0
799
+ }
800
+ })),
801
+ {
802
+ id: "synthesis",
803
+ type: "tool-result",
804
+ label: "Final synthesis",
805
+ subtitle: "Aggregate subagent results",
806
+ config: {
807
+ successStatusCodes: [200],
808
+ writeLastResponse: true,
809
+ writeSourceRecord: true,
810
+ outputMode: "swarm-summary",
811
+ statusField: "status",
812
+ lastTestedField: "lastTested",
813
+ outcomePrompt: String(options.outcomePrompt || "Confirm every required subagent completed and write the final answer.").trim()
814
+ }
815
+ }
816
+ ];
817
+
818
+ const edges = [
819
+ ...subagents.map((agent) => ({
820
+ from: "orchestrator",
821
+ to: String(agent.id || agent.role || "subagent").replace(/[^a-zA-Z0-9_-]+/g, "-"),
822
+ passes: "subtask-assignment"
823
+ })),
824
+ ...subagents.map((agent) => ({
825
+ from: String(agent.id || agent.role || "subagent").replace(/[^a-zA-Z0-9_-]+/g, "-"),
826
+ to: "synthesis",
827
+ passes: "subtask-result"
828
+ }))
829
+ ];
830
+
831
+ const maxConcurrency = Math.max(1, Number(options.maxConcurrency) || subagents.length);
832
+ const rewardWeights = options.rewardWeights && typeof options.rewardWeights === "object"
833
+ ? options.rewardWeights
834
+ : { parallel: 0.25, finish: 0.35, outcome: 0.4 };
835
+
836
+ return {
837
+ version: 1,
838
+ provider: "growthub-native",
839
+ executionMode: AGENT_SWARM_EXECUTION_MODE,
840
+ swarm: {
841
+ maxConcurrency,
842
+ rewardWeights: {
843
+ parallel: Number(rewardWeights.parallel) || 0,
844
+ finish: Number(rewardWeights.finish) || 0,
845
+ outcome: Number(rewardWeights.outcome) || 0
846
+ },
847
+ outcomeCriteria: String(options.outcomeCriteria || "All required subagents complete and synthesis runs without error.").trim()
848
+ },
849
+ nodes,
850
+ edges
851
+ };
852
+ }
853
+
854
+ /**
855
+ * Schema-aware validation for agent-swarm-v1 graphs. Returns `{ ok, errors }`
856
+ * with concrete user-facing messages. Used both by validateOrchestrationGraph
857
+ * and the WorkflowSurface Test gate so the user never gets a runtime "swarm
858
+ * subagent has no prompt-capable adapter" error at Test time when the static
859
+ * config already revealed the issue.
860
+ */
861
+ function validateAgentSwarmGraph(graph) {
862
+ const errors = [];
863
+ if (!graph || typeof graph !== "object") {
864
+ return { ok: false, errors: ["agent-swarm graph must be an object"] };
865
+ }
866
+ const extracted = extractSwarmNodes(graph);
867
+ if (!extracted) return { ok: false, errors: ["graph is not an agent-swarm-v1 graph"] };
868
+ const { orchestrator, subagents, synthesis } = extracted;
869
+ if (!orchestrator) errors.push("missing orchestrator (thinAdapter) node");
870
+ if (subagents.length === 0) errors.push("agent-swarm requires at least one ai-agent subagent");
871
+ subagents.forEach((node, index) => {
872
+ const cfg = node?.config || {};
873
+ const role = String(cfg.role || node?.label || "").trim();
874
+ if (!role) errors.push(`subagent[${index}] (${node?.id || "?"}) must declare a role`);
875
+ if (!String(cfg.taskPrompt || cfg.prompt || "").trim()) {
876
+ errors.push(`subagent "${role || node?.id}" must declare a task prompt`);
877
+ }
878
+ const adapter = String(cfg.adapter || "").trim();
879
+ if (adapter && !["local-agent-host", "local-intelligence"].includes(adapter)) {
880
+ errors.push(`subagent "${role || node?.id}" sets adapter="${adapter}" which cannot execute prompts; use local-agent-host or local-intelligence`);
881
+ }
882
+ });
883
+ if (!synthesis) {
884
+ errors.push("agent-swarm graph should include a tool-result synthesis node to evaluate outcome");
885
+ }
886
+ return { ok: errors.length === 0, errors };
887
+ }
888
+
683
889
  function redactSecretsFromText(text) {
684
890
  let out = String(text || "");
685
891
  for (const pattern of [
@@ -699,9 +905,14 @@ export {
699
905
  FILTER_OPERATORS,
700
906
  FILTER_CONJUNCTIONS,
701
907
  CANONICAL_NODE_ORDER,
908
+ AGENT_SWARM_EXECUTION_MODE,
702
909
  buildBlankOrchestrationGraphShell,
703
910
  buildDefaultOrchestrationGraphFromRegistry,
911
+ buildDefaultAgentSwarmGraph,
704
912
  buildCanonicalNode,
913
+ isAgentSwarmGraph,
914
+ extractSwarmNodes,
915
+ validateAgentSwarmGraph,
705
916
  isOrchestrationGraphEmpty,
706
917
  getOrchestrationGraphUiState,
707
918
  getNextCanonicalNodeId,
@@ -160,6 +160,9 @@ function buildLogChildren(record, summary) {
160
160
 
161
161
  function buildRunLogTree(record) {
162
162
  if (!record || typeof record !== "object") return [];
163
+ if (Array.isArray(record.logTree) && record.logTree.length > 0) {
164
+ return record.logTree;
165
+ }
163
166
  const summary = deriveRunSummary(record);
164
167
  const durationMs = clampNumber(record?.durationMs) || 0;
165
168
  const attemptChildren = buildLogChildren(record, summary);
@@ -224,6 +227,29 @@ function normalizeRunConsoleRecord(record) {
224
227
  const safeInput = rawInput ? redactRunInputsEnvelope(rawInput) : null;
225
228
  const inputSummary = safeInput ? summarizeRunInputs(safeInput) : null;
226
229
  const exports = buildExportsForRecord(record, stdoutText, stderrText, outputText);
230
+ // Workspace Metadata Graph V1 — safe lineage projection. Names only,
231
+ // no secrets. Lets the Live Runs Console UI render "this run came from
232
+ // sandbox X / workflow Y / adapter Z / agent host A" without re-deriving
233
+ // the relationships from raw fields.
234
+ const lineage = {
235
+ runId: safeString(record.runId).trim(),
236
+ objectId: safeString(record.objectId).trim(),
237
+ sandboxName: safeString(record.name || record.sandboxName).trim(),
238
+ workflowRowId: safeString(record.name || record.sandboxName).trim(),
239
+ workflowMetadataId: safeString(record.objectId).trim() && safeString(record.name || record.sandboxName).trim()
240
+ ? `workflow:${safeString(record.objectId).trim()}:${safeString(record.name || record.sandboxName).trim()}`
241
+ : "",
242
+ sandboxMetadataId: safeString(record.objectId).trim() && safeString(record.name || record.sandboxName).trim()
243
+ ? `sandbox:${safeString(record.objectId).trim()}:${safeString(record.name || record.sandboxName).trim()}`
244
+ : "",
245
+ adapter: safeString(record.adapter).trim(),
246
+ agentHost: safeString(record.agentHost).trim(),
247
+ runtime: safeString(record.runtime).trim(),
248
+ runLocality: safeString(record.runLocality).trim(),
249
+ inputFieldCount: inputSummary ? inputSummary.fieldCount : 0,
250
+ inputSource: inputSummary ? inputSummary.source : "",
251
+ hasOutput: Boolean(outputText)
252
+ };
227
253
 
228
254
  return {
229
255
  runId: safeString(record.runId).trim(),
@@ -275,6 +301,8 @@ function normalizeRunConsoleRecord(record) {
275
301
  adapterMeta,
276
302
  templateTrace
277
303
  },
304
+ lineage,
305
+ swarm: record.swarm && typeof record.swarm === "object" ? record.swarm : null,
278
306
  logTree: buildRunLogTree(record)
279
307
  };
280
308
  }
@@ -307,6 +307,48 @@ function buildInputPayloadForRunner(envelope) {
307
307
  return out;
308
308
  }
309
309
 
310
+ /**
311
+ * Workspace Metadata Graph V1 — typed run-input descriptors.
312
+ *
313
+ * Returns a metadata-compatible list:
314
+ * { id, label, type, required, secretRefOnly, sourceNodeId, workflowId }
315
+ *
316
+ * Used by the metadata store + workflow sidecar so the manual run input
317
+ * schema is a single typed contract — never re-derived inside UI code.
318
+ *
319
+ * Existing redaction + size limits remain enforced by
320
+ * `validateRunInputsEnvelope` / `normalizeRunInputsEnvelope`. This helper
321
+ * is descriptor-only and does not mutate or echo any field value.
322
+ */
323
+ function describeRunInputMetadataItems({ workflowId, graph, objectId, rowId } = {}) {
324
+ const schema = discoverRunInputSchema(graph);
325
+ const fields = Array.isArray(schema?.fields) ? schema.fields : [];
326
+ const workflowKey = String(workflowId || (objectId && rowId ? `${objectId}::${rowId}` : "")).trim();
327
+ const sourceNodeId = findFirstHumanInputNodeId(graph);
328
+ return fields.map((field) => ({
329
+ kind: "workspaceRunInput",
330
+ id: field.id,
331
+ label: field.label,
332
+ type: field.type,
333
+ required: Boolean(field.required),
334
+ isSecret: Boolean(field.isSecret),
335
+ secretRefOnly: Boolean(field.isSecret) || field.type === "secretRef",
336
+ sourceNodeId,
337
+ workflowId: workflowKey
338
+ }));
339
+ }
340
+
341
+ function findFirstHumanInputNodeId(graphValue) {
342
+ const graph = parseOrchestrationGraph(graphValue) || graphValue || null;
343
+ const nodes = Array.isArray(graph?.nodes) ? graph.nodes : [];
344
+ for (const node of nodes) {
345
+ const type = String(node?.type || "").trim();
346
+ const action = String(node?.config?.action || "").trim();
347
+ if (type === "human-input" || action === "form") return String(node?.id || "").trim() || "human-input";
348
+ }
349
+ return "";
350
+ }
351
+
310
352
  export {
311
353
  RUN_INPUTS_KIND,
312
354
  MAX_RUN_INPUT_VALUES,
@@ -319,5 +361,6 @@ export {
319
361
  summarizeRunInputs,
320
362
  validateRunInputsEnvelope,
321
363
  buildInputPayloadForRunner,
364
+ describeRunInputMetadataItems,
322
365
  parseFieldDescriptor
323
366
  };
@@ -60,7 +60,9 @@ function parseSandboxRunTrace(lastResponse) {
60
60
  envRefsMissing: Array.isArray(parsed.envRefsMissing) ? parsed.envRefsMissing : [],
61
61
  input: safeInput,
62
62
  inputSummary: safeInput ? summarizeRunInputs(safeInput) : null,
63
- exports: parsed.exports && typeof parsed.exports === "object" ? parsed.exports : null
63
+ exports: parsed.exports && typeof parsed.exports === "object" ? parsed.exports : null,
64
+ swarm: parsed.swarm && typeof parsed.swarm === "object" ? parsed.swarm : null,
65
+ logTree: Array.isArray(parsed.logTree) ? parsed.logTree : null
64
66
  };
65
67
  }
66
68
 
@@ -585,6 +585,41 @@ function notFoundError(message) {
585
585
  return error;
586
586
  }
587
587
 
588
+ // ──────────────────────────────────────────────────────────────────────────
589
+ // Workspace Metadata Graph V1 — safe agent host readiness metadata.
590
+ //
591
+ // Pure helper that distills a sandbox row into the safe metadata the
592
+ // workspace metadata graph + UI inspector can show. Reads ONLY the safe,
593
+ // allowlisted readiness fields the auth helper persists on the row. Never
594
+ // echoes raw tokens, login URLs, or stdout/stderr — those live inside the
595
+ // helper's response object, not the row patch.
596
+ // ──────────────────────────────────────────────────────────────────────────
597
+
598
+ function describeAgentHostReadinessMetadata(row) {
599
+ if (!row || typeof row !== "object") return null;
600
+ const adapter = String(row?.adapter || "").trim();
601
+ const agentHost = String(row?.agentHost || "").trim();
602
+ const runLocality = String(row?.runLocality || "").trim();
603
+ const safe = {};
604
+ for (const key of SAFE_ROW_PATCH_FIELDS) {
605
+ if (Object.prototype.hasOwnProperty.call(row, key)) {
606
+ safe[key] = row[key];
607
+ }
608
+ }
609
+ return {
610
+ kind: "workspaceAgentHostReadiness",
611
+ adapter,
612
+ agentHost,
613
+ runLocality,
614
+ status: KNOWN_AGENT_AUTH_STATUSES.includes(safe.agentAuthStatus) ? safe.agentAuthStatus : "unknown",
615
+ provider: String(safe.agentAuthProvider || agentHost || "unknown").trim(),
616
+ lastChecked: String(safe.agentAuthLastChecked || "").trim(),
617
+ lastExitCode: typeof safe.agentAuthLastExitCode === "number" ? safe.agentAuthLastExitCode : null,
618
+ lastMessage: String(safe.agentAuthLastMessage || "").trim(),
619
+ lastLoginUrl: String(safe.agentAuthLastLoginUrl || "").trim()
620
+ };
621
+ }
622
+
588
623
  // ──────────────────────────────────────────────────────────────────────────
589
624
  // Backwards-compatible Claude aliases (legacy)
590
625
  // ──────────────────────────────────────────────────────────────────────────
@@ -616,6 +651,7 @@ export {
616
651
  buildRowPatch,
617
652
  checkAgentStatus,
618
653
  checkClaudeStatus,
654
+ describeAgentHostReadinessMetadata,
619
655
  findSandboxRow,
620
656
  getAgentHostCapabilities,
621
657
  redactSecrets,
@@ -529,6 +529,58 @@ function deriveChartHydrationState({
529
529
  return "computed";
530
530
  }
531
531
 
532
+ /**
533
+ * Workspace Metadata Graph V1 — typed widget dependency contract.
534
+ *
535
+ * Returns the same dependency shape Twenty-style sidecars consume: required
536
+ * axis fields, filter fields, sort fields, aggregation fields, output shape,
537
+ * and per-widget warnings. The metadata store calls a richer derivation that
538
+ * carries the bound object id; this helper accepts a single widget config
539
+ * and returns the dependency contract without needing the full store.
540
+ *
541
+ * Pure. No fetch. No mutation. Never throws on partial widget shapes.
542
+ */
543
+ function deriveWidgetDependencyContract(widget) {
544
+ const config = isPlainObject(widget?.config) ? widget.config : {};
545
+ const binding = isPlainObject(config.binding) ? config.binding : null;
546
+ const xAxis = isPlainObject(config.xAxis) ? config.xAxis : null;
547
+ const yAxis = isPlainObject(config.yAxis) ? config.yAxis : null;
548
+ const filter = isPlainObject(config.filter) ? config.filter : null;
549
+ const xField = typeof xAxis?.field === "string" ? xAxis.field.trim() : "";
550
+ const yField = typeof yAxis?.field === "string" ? yAxis.field.trim() : "";
551
+ const groupField = typeof yAxis?.groupBy === "string" ? yAxis.groupBy.trim() : "";
552
+ const operation = typeof yAxis?.operation === "string"
553
+ ? yAxis.operation.trim()
554
+ : (typeof yAxis?.aggregation === "string" ? yAxis.aggregation.trim() : "sum");
555
+ const clauses = Array.isArray(filter?.clauses) ? filter.clauses : [];
556
+ const filterFields = [];
557
+ for (const clause of clauses) {
558
+ if (!isPlainObject(clause)) continue;
559
+ const field = typeof clause.fieldId === "string" ? clause.fieldId.trim() : "";
560
+ if (field && !filterFields.includes(field)) filterFields.push(field);
561
+ }
562
+ const sortFields = xField ? [xField] : [];
563
+ const aggregationFields = yField ? [yField] : [];
564
+ const required = Array.from(new Set([xField, yField, groupField].filter(Boolean)));
565
+ const widgetKind = typeof widget?.kind === "string" ? widget.kind.trim() : "chart";
566
+ const warnings = [];
567
+ if (widgetKind === "chart" && required.length === 0 && operation !== "count" && operation !== "countAll") {
568
+ warnings.push("Chart widget is missing both X and Y axis fields.");
569
+ }
570
+ return {
571
+ objectId: typeof binding?.objectId === "string" ? binding.objectId.trim() : "",
572
+ sourceType: typeof binding?.sourceType === "string" ? binding.sourceType.trim() : "",
573
+ sourceAuthority: "workspace-config",
574
+ required,
575
+ filter: filterFields,
576
+ sort: sortFields,
577
+ aggregation: aggregationFields,
578
+ operation: operation || "sum",
579
+ outputShape: widgetKind === "chart" ? "number[]" : "row[]",
580
+ warnings
581
+ };
582
+ }
583
+
532
584
  export {
533
585
  KNOWN_AGGREGATIONS,
534
586
  applyFilter,
@@ -538,5 +590,6 @@ export {
538
590
  computeChartProjectionDebug,
539
591
  computeChartValuesFromRows,
540
592
  deriveChartHydrationState,
593
+ deriveWidgetDependencyContract,
541
594
  groupRows
542
595
  };