@growthub/cli 0.14.6 → 0.14.9

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 (18) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +21 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +1 -1
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +5 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +5 -4
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +58 -4
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +31 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/StatusPill.jsx +22 -6
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ToggleField.jsx +5 -4
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/WorkspaceDataModelCanvas.jsx +457 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +188 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +67 -3
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-map/page.jsx +14 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +48 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-node-status.js +55 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-impact.js +198 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +11 -0
  18. package/package.json +1 -1
@@ -39,6 +39,27 @@ mcpTools: []
39
39
 
40
40
  The **Workspace** is the top-level Growthub Local product object. This kit is the official starter that bootstraps every governed Workspace; everything else (worker kits, templates, workflows, hosted agents, source imports) is an input to a Workspace.
41
41
 
42
+ ## Your operating role (read first)
43
+
44
+ **If you are an agent reading this, you are the operating layer.** A human gave you an outcome; you operate this governed Workspace on their behalf. When the requester is a super admin or workspace admin, you can act as the super-admin orchestrator inside the governed boundary; when the requester is a regular user, you still do the orchestration work, but approval, review, rollback, and authority stay with the workspace/super-admin surfaces. You are not editing files and hoping tests catch mistakes — you are operating a live domain model where the platform rejects invalid reality before it lands and remembers everything that does. The chat you are in *is* the user's steering surface; the no-code Workspace is the control and audit plane.
45
+
46
+ **The human is usually non-technical, and asks for a business outcome — not workspace mechanics.** They will not mention the Data Model, metadata graph, routes, validators, the PATCH boundary, or the run console — those are the control plane and proof layer that exist so you can operate safely and the super admin can inspect everything. They ask in plain language: *research a customer avatar, run market/competitor research, generate this week's content batch, script and brief the next campaign, pull the latest campaign performance, update the client report, build an MRR-by-plan dashboard, run the revenue workflow, iterate on a feature, find what broke.* Your job is to translate that outcome into governed operations over **what already exists** in this Workspace (prebuilt workflows, objects, and procedures), execute it, and deliver the result — outputs saved, synced, inspectable, and governed. This **private repository is the customer's durable operating environment**: their kit, objects, workflows, procedures, rules, source records, docs, receipts, run history, and governance boundaries all live here; your session is just the operator entering that universe.
47
+
48
+ The completion bar is the user's real-world outcome, not a partial proposal or a hidden local artifact. Continue the governed loop until the workspace has objective proof: the relevant workflow ran or object changed, output rows/ledgers reflect the count and state, durable storage or source records hold the deliverables, receipts/run ids explain what happened, and any human-review state remains explicit. Do not hard-code one client's workflow as the pattern; swap the client, rows, registries, brand constraints, storage prefix, dashboard, and quality criteria while keeping the same governed causation loop.
49
+
50
+ Your loop, every time:
51
+
52
+ 1. **Inherit state** — read the `workspace:agent-outcomes` receipt stream (`GET /api/workspace/agent-outcomes`) and `.growthub-fork/project.md` to see what the last agent did; continue from `nextActions` / `rollbackRef`, don't redo work.
53
+ 2. **Check what exists** — a scheduled job, external API, data view, or multi-agent workflow is almost always already a governed object. Prefer operating an existing object over writing code.
54
+ 3. **Act only through governed routes** — `PATCH /api/workspace` (config) and `POST /api/workspace/sandbox-run` (execution); drafts via `workflow/publish`; proposals via `helper/apply`. There is no third path.
55
+ 4. **Let the validator correct you** — preflight, read the rejection reason, repair, retry. Rejections are navigation, not failure.
56
+ 5. **Persist the outcome** — count only connected, durable outputs; save accepted artifacts to the governed ledger/storage surface; keep generated binaries and secrets out of git.
57
+ 6. **Leave proof** — every governed action emits a secret-redacted receipt. The human does not need the mechanics; the super admin inspects all of it after the fact (Workspace Map, Run Console, outcome cockpit).
58
+
59
+ **Three roles:** the human states outcomes → you (the agent) operate → the workspace admin/super admin governs and audits. The mechanics of the boundary are in [`skills/governed-workspace-mutation/SKILL.md`](./skills/governed-workspace-mutation/SKILL.md) — read it before any mutation.
60
+
61
+ > **For the human operator:** you do not have to operate this Workspace yourself. Tell an agent what you want; it operates the Workspace through governed routes; you (or your admin) inspect every change with full proof and rollback. The no-code Builder is the governed substrate and the audit surface — not a tool you must personally drive.
62
+
42
63
  Every Growthub governed Workspace is materialised from this kit. The kit ships the `.growthub-fork/` contract (identity, policy, trace, optional authority), the `apps/workspace` no-code Workspace Builder, the validated `growthub.config.json` V1 contract, plus the six primitive layers Claude/Cursor/Codex agents operate against:
43
64
 
44
65
  1. **`SKILL.md`** — this file. Discovery entry + routing menu. Always loaded first; the full operator runbook (`skills.md`) is disclosed progressively when work begins.
@@ -18,7 +18,7 @@ It intentionally depends on adapter contracts:
18
18
  - `NANGO_ENVIRONMENT` (default `dev`)
19
19
  - `NANGO_MODE` (`cloud` | `self-hosted`, default `cloud`)
20
20
 
21
- The Growthub local-first operator shell remains at `../../studio`.
21
+ This `apps/workspace` app is the only bundled app surface; the legacy `studio/` Vite shell has been removed. It is the governed control plane and audit surface — non-technical users do not operate it directly; an agent operates the Workspace on their behalf through the governed routes (see the workspace `SKILL.md` operating-role contract), while super admins use this app for inspection, proof, and governance.
22
22
 
23
23
  Settings exposes two universal integration lanes:
24
24
 
@@ -413,6 +413,11 @@ function buildRunResponse({
413
413
  if (result && typeof result === "object" && Array.isArray(result.logTree)) {
414
414
  base.logTree = result.logTree;
415
415
  }
416
+ // Per-node orchestration execution trace (general pipeline, not swarm) so the
417
+ // Workflow Canvas can settle per-node status from the persisted record.
418
+ if (result && typeof result === "object" && Array.isArray(result.nodeTrace)) {
419
+ base.nodeTrace = result.nodeTrace;
420
+ }
416
421
  base.exports = {
417
422
  available: ["download-json", "copy-output", "download-stdout", "download-stderr", "download-log-node"],
418
423
  external: []
@@ -20,7 +20,7 @@
20
20
 
21
21
  import { useEffect, useMemo, useState } from "react";
22
22
  import Link from "next/link";
23
- import { Activity, BarChart3, Check, Copy, Eye, GitBranch, MoreVertical, Search } from "lucide-react";
23
+ import { Activity, BarChart3, Check, Copy, Eye, GitBranch, MoreVertical, Search, Share2 } from "lucide-react";
24
24
  import { deriveWorkspaceState, deriveSwarmConditionPacket, deriveWorkspaceContributions, deriveLensWalkthroughState, LENS_WALKTHROUGH_DISMISS_FLAG } from "@/lib/workspace-activation";
25
25
  import { WorkspaceContributionGraph } from "./WorkspaceContributionGraph.jsx";
26
26
  import { WorkspaceLensWalkthrough } from "./WorkspaceLensWalkthrough.jsx";
@@ -244,9 +244,10 @@ export function WorkspaceLensPanel({ workspaceConfig, workspaceSourceRecords, me
244
244
  <h1 className="workspace-lens-title">Workspace Lens</h1>
245
245
  <p className="workspace-lens-subtitle">Live derived state for this workspace.</p>
246
246
  </div>
247
- <p className="workspace-lens-score" aria-label="Workspace lens summary">
248
- {counts.total} lenses · {counts.ready} ready · {counts.blocked} blocked · {counts.assignable} agent-assignable
249
- </p>
247
+ <Link className="workspace-lens-map-link" href="/workspace-map" aria-label="Open Workspace Map">
248
+ <Share2 size={15} aria-hidden="true" />
249
+ <span>Workspace Map</span>
250
+ </Link>
250
251
  </header>
251
252
 
252
253
  {showWalk ? (
@@ -2390,6 +2390,7 @@ function DataModelTableSurface({
2390
2390
  const [selectMenuOpen, setSelectMenuOpen] = useState(false);
2391
2391
  const [pageSize, setPageSize] = useState(15);
2392
2392
  const [pageIndex, setPageIndex] = useState(0);
2393
+ const [search, setSearch] = useState("");
2393
2394
  const fieldInputRef = useRef(null);
2394
2395
  const selectedOriginalIndex = selectedRecordIndex ?? localSelectedOriginalIndex;
2395
2396
 
@@ -2407,6 +2408,7 @@ function DataModelTableSurface({
2407
2408
  setLastSelectedRowIndex(null);
2408
2409
  setSelectMenuOpen(false);
2409
2410
  setPageIndex(0);
2411
+ setSearch("");
2410
2412
  }, [table.id]);
2411
2413
 
2412
2414
  useEffect(() => {
@@ -2428,7 +2430,14 @@ function DataModelTableSurface({
2428
2430
  const visibleColumns = useMemo(() => orderedColumns.filter((column) => !settings.hidden.includes(column)), [orderedColumns, settings.hidden]);
2429
2431
  const rowEntries = useMemo(() => {
2430
2432
  const indexed = (table.rows || []).map((row, originalIndex) => ({ row, originalIndex }));
2431
- const filtered = indexed.filter((entry) => rowMatchesFilter(entry.row, settings.filter));
2433
+ const needle = search.trim().toLowerCase();
2434
+ const filtered = indexed.filter((entry) => {
2435
+ if (!rowMatchesFilter(entry.row, settings.filter)) return false;
2436
+ if (!needle) return true;
2437
+ // Quick search across the visible columns only — additive to the
2438
+ // column-level filter clauses, never replaces them.
2439
+ return visibleColumns.some((column) => String(entry.row?.[column] ?? "").toLowerCase().includes(needle));
2440
+ });
2432
2441
  if (!settings.sort?.length) return filtered;
2433
2442
  const clauses = settings.sort;
2434
2443
  return [...filtered].sort((left, right) => {
@@ -2439,7 +2448,7 @@ function DataModelTableSurface({
2439
2448
  }
2440
2449
  return 0;
2441
2450
  });
2442
- }, [table.rows, settings]);
2451
+ }, [table.rows, settings, search, visibleColumns]);
2443
2452
  const selectedRowCount = selectedRows.size;
2444
2453
  const pageCount = Math.max(1, Math.ceil(rowEntries.length / pageSize));
2445
2454
  const safePageIndex = Math.min(pageIndex, pageCount - 1);
@@ -2491,7 +2500,7 @@ function DataModelTableSurface({
2491
2500
  setSelectedRow(null);
2492
2501
  setLastSelectedRowIndex(null);
2493
2502
  setSelectMenuOpen(false);
2494
- }, [settings.filter, settings.sort, pageSize]);
2503
+ }, [settings.filter, settings.sort, pageSize, search]);
2495
2504
 
2496
2505
  function commitField() {
2497
2506
  const name = fieldName.trim();
@@ -2690,9 +2699,25 @@ function DataModelTableSurface({
2690
2699
  ))}
2691
2700
  </div>
2692
2701
  <div className="dm-records-actions">
2702
+ <label className="dm-toolbar-search">
2703
+ <Search size={13} aria-hidden="true" />
2704
+ <input
2705
+ value={search}
2706
+ placeholder="Search records"
2707
+ onChange={(event) => setSearch(event.target.value)}
2708
+ aria-label={`Search ${table.label || "records"}`}
2709
+ />
2710
+ </label>
2711
+ <span className="dm-toolbar-count">
2712
+ {(search.trim() || settings.filter?.clauses?.length)
2713
+ ? `${rowEntries.length} of ${pluralize(table.rows?.length || 0, "record")}`
2714
+ : pluralize(table.rows?.length || 0, "record")}
2715
+ </span>
2716
+ <span className="dm-toolbar-divider" aria-hidden="true" />
2693
2717
  <span className="dm-filter-anchor">
2694
- <button type="button" className="dm-btn-ghost" onClick={() => setFilterTarget((current) => current === "toolbar" ? "" : "toolbar")}>
2718
+ <button type="button" className={`dm-btn-ghost${settings.filter?.clauses?.length ? " is-active" : ""}`} onClick={() => setFilterTarget((current) => current === "toolbar" ? "" : "toolbar")}>
2695
2719
  <Filter size={13} />Filter
2720
+ {settings.filter?.clauses?.length > 0 && <span className="dm-filter-chip-count">{settings.filter.clauses.length}</span>}
2696
2721
  </button>
2697
2722
  {filterTarget === "toolbar" && (
2698
2723
  <div className="dm-filter-popover dm-filter-popover-toolbar">
@@ -2843,6 +2868,35 @@ function DataModelTableSurface({
2843
2868
  </tr>
2844
2869
  </thead>
2845
2870
  <tbody>
2871
+ {rowEntries.length === 0 && (
2872
+ <tr className="dm-db-empty-row">
2873
+ <td colSpan={visibleColumns.length + 1 + (table.mutable ? 1 : 0)}>
2874
+ <div className="dm-db-empty-state">
2875
+ {(search.trim() || settings.filter?.clauses?.length) ? (
2876
+ <>
2877
+ <strong>No records match</strong>
2878
+ <span>Try a different search or clear your filters.</span>
2879
+ <div className="dm-db-empty-actions">
2880
+ {search.trim() && <button type="button" className="dm-btn-outline" onClick={() => setSearch("")}>Clear search</button>}
2881
+ {settings.filter?.clauses?.length > 0 && <button type="button" className="dm-btn-outline" onClick={resetView}>Reset view</button>}
2882
+ </div>
2883
+ </>
2884
+ ) : (
2885
+ <>
2886
+ <strong>No records yet</strong>
2887
+ <span>{table.mutable ? "Add your first row or import a CSV to start using this object." : "Records resolve at runtime for this live-backed object."}</span>
2888
+ {table.mutable && (
2889
+ <div className="dm-db-empty-actions">
2890
+ <button type="button" className="dm-btn-primary-sm" disabled={saving} onClick={() => onSave((config) => addTableRow(config, table))}><Plus size={13} />Add record</button>
2891
+ <button type="button" className="dm-btn-outline" onClick={() => setCsvOpen(true)}><Upload size={13} />Import CSV</button>
2892
+ </div>
2893
+ )}
2894
+ </>
2895
+ )}
2896
+ </div>
2897
+ </td>
2898
+ </tr>
2899
+ )}
2846
2900
  {pageEntries.map(({ row, originalIndex }, rowIndex) => {
2847
2901
  const visibleIndex = pageStart + rowIndex;
2848
2902
  const displayIndex = visibleIndex + 1;
@@ -103,6 +103,21 @@ function nodeRecordName(node) {
103
103
  return "";
104
104
  }
105
105
 
106
+ // Per-node run-status pill (Attio-style), docked outside the node top-right.
107
+ // Driven only by the real general-orchestration run signal passed in
108
+ // `nodeStatuses` — the streamed orchestration.node.* deltas and the persisted
109
+ // nodeTrace (see lib/orchestration-node-status.js). Never fabricated here.
110
+ const NODE_STATUS_CHIP = {
111
+ completed: { cls: "is-ok", label: "Completed" },
112
+ ok: { cls: "is-ok", label: "Completed" },
113
+ running: { cls: "is-running", label: "Running" },
114
+ executing: { cls: "is-running", label: "Running" },
115
+ failed: { cls: "is-bad", label: "Failed" },
116
+ skipped: { cls: "is-waiting", label: "Skipped" },
117
+ pending: { cls: "is-waiting", label: "Waiting" },
118
+ queued: { cls: "is-waiting", label: "Waiting" },
119
+ };
120
+
106
121
  export function OrchestrationGraphCanvas({
107
122
  graph,
108
123
  selectedNodeId,
@@ -112,6 +127,8 @@ export function OrchestrationGraphCanvas({
112
127
  onRunTest,
113
128
  runStatus,
114
129
  runMessage,
130
+ nodeStatuses,
131
+ onNodeStatusClick,
115
132
  statusLabel = "Draft",
116
133
  }) {
117
134
  const parsed = useMemo(() => parseOrchestrationGraph(graph) || graph, [graph]);
@@ -222,6 +239,9 @@ export function OrchestrationGraphCanvas({
222
239
  const isSelected = activeId === id;
223
240
  const prevId = index > 0 ? String(nodes[index - 1].id || "") : "";
224
241
  const Icon = NODE_ICONS[node.type] || ArrowDownToLine;
242
+ const nodeStatusChip = nodeStatuses
243
+ ? NODE_STATUS_CHIP[String(nodeStatuses[id] || "").toLowerCase()] || null
244
+ : null;
225
245
 
226
246
  return (
227
247
  <div key={id || index} className="dm-orchestration-canvas__step">
@@ -285,6 +305,17 @@ export function OrchestrationGraphCanvas({
285
305
  onSelectNode?.(node);
286
306
  }}
287
307
  >
308
+ {nodeStatusChip && (
309
+ <button
310
+ type="button"
311
+ className={`dm-status-chip ${nodeStatusChip.cls} dm-orchestration-node__status`}
312
+ title={`${nodeStatusChip.label} — open run trace`}
313
+ onClick={(event) => { event.stopPropagation(); onNodeStatusClick?.(node); }}
314
+ >
315
+ <span className="dm-status-dot" aria-hidden="true" />
316
+ {nodeStatusChip.label}
317
+ </button>
318
+ )}
288
319
  <span className="dm-orchestration-node__icon" aria-hidden="true">
289
320
  <Icon size={14} />
290
321
  </span>
@@ -1,13 +1,29 @@
1
1
  "use client";
2
2
 
3
+ // Maps a free-form status string to one calm, consistent chip state.
4
+ // Backwards-compatible: same { value } prop, same default ("untested").
5
+ const OK = ["connected", "approved", "ok", "success", "succeeded", "complete", "completed", "passed", "live", "ready"];
6
+ const BAD = ["failed", "error", "errored", "disconnected", "rejected"];
7
+ const WARN = ["warning", "warn", "pending", "untrusted", "stale", "degraded"];
8
+ const RUNNING = ["running", "in_progress", "in-progress", "active", "executing", "started"];
9
+ const WAITING = ["waiting", "queued", "idle", "scheduled"];
10
+
11
+ function classifyStatus(status) {
12
+ if (OK.includes(status)) return "is-ok";
13
+ if (BAD.includes(status)) return "is-bad";
14
+ if (RUNNING.includes(status)) return "is-running";
15
+ if (WARN.includes(status)) return "is-warn";
16
+ if (WAITING.includes(status)) return "is-waiting";
17
+ return "";
18
+ }
19
+
3
20
  export function StatusPill({ value }) {
4
- const status = String(value || "untested").toLowerCase();
5
- const ok = ["connected", "approved", "ok", "success"].includes(status);
6
- const bad = ["failed", "error", "disconnected"].includes(status);
21
+ const raw = value || "untested";
22
+ const state = classifyStatus(String(raw).toLowerCase());
7
23
  return (
8
- <span className={`dm-db-status ${ok ? "ok" : bad ? "bad" : ""}`}>
9
- <span />
10
- {value || "untested"}
24
+ <span className={`dm-status-chip ${state}`.trim()}>
25
+ <span className="dm-status-dot" />
26
+ {raw}
11
27
  </span>
12
28
  );
13
29
  }
@@ -2,16 +2,17 @@
2
2
 
3
3
  export function ToggleField({ checked, disabled, label, onChange, description }) {
4
4
  return (
5
- <label className="dm-check-row">
5
+ <label className={`dm-switch-row${disabled ? " is-disabled" : ""}`}>
6
6
  <input
7
7
  type="checkbox"
8
8
  checked={Boolean(checked)}
9
9
  disabled={disabled}
10
10
  onChange={(event) => onChange(event.target.checked)}
11
11
  />
12
- <span>
13
- {label}
14
- {description && <span className="dm-cell-empty" style={{ display: "block", marginTop: 4 }}>{description}</span>}
12
+ <span className="dm-switch-track" aria-hidden="true" />
13
+ <span className="dm-switch-label">
14
+ <span>{label}</span>
15
+ {description && <span className="dm-switch-desc">{description}</span>}
15
16
  </span>
16
17
  </label>
17
18
  );