@growthub/cli 0.14.8 → 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.
@@ -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
 
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Growthub Workspace Blast-Radius V1 — transitive impact deriver.
3
+ *
4
+ * Answers the one question the single-hop primitives cannot: *if I change (or
5
+ * remove) this node, what is the FULL downstream set that goes stale?*
6
+ *
7
+ * The metadata graph (`workspace-metadata-graph.js`) already ships:
8
+ * - `findDependents(graph, nodeId)` — the nodes ONE hop upstream (incoming edges)
9
+ * - `selectImpactedNodes(graph, nodeId)` (selectors) — a thin wrapper over it
10
+ *
11
+ * Both stop at the first hop. Editing a field surfaces the widgets that use it,
12
+ * but NOT the dashboards that contain those widgets, nor the runs that executed
13
+ * a workflow whose node reads the object. This module generalises
14
+ * `findDependents` into its transitive closure: a deterministic breadth-first
15
+ * walk of incoming edges, returning every reachable dependent with its hop
16
+ * distance and the relation it was reached through.
17
+ *
18
+ * It is a PURE module — no React, no fetch, no fs, no writes, no localStorage,
19
+ * no CSS. It introduces NO new graph, NO new mutation path: it reads the
20
+ * read-only graph the Workspace Map already builds, and emits a low-entropy
21
+ * view-model that the Map inspector, `patch/preflight`, the CLI `plan` command,
22
+ * and an MCP `simulate_causal_impact` tool can all consume from one source of
23
+ * truth. It contains no secrets — it carries only the same compact node
24
+ * summaries the graph already exposes.
25
+ *
26
+ * Determinism: results are ordered (distance → type → id) and the BFS visits
27
+ * each node once, so a cycle in the graph terminates and the output diffs
28
+ * cleanly between calls.
29
+ */
30
+
31
+ const BLAST_RADIUS_KIND = "growthub-workspace-blast-radius-v1";
32
+ const BLAST_RADIUS_VERSION = 1;
33
+
34
+ // Bound the walk so a pathological graph can never produce an unbounded
35
+ // payload. Honest truncation (`truncated: true`) beats a silent cap.
36
+ const DEFAULT_MAX_NODES = 500;
37
+
38
+ function safeString(value) {
39
+ if (value == null) return "";
40
+ return typeof value === "string" ? value : String(value);
41
+ }
42
+
43
+ function summarizeOrigin(node) {
44
+ if (!node || typeof node !== "object") return null;
45
+ return {
46
+ id: node.id,
47
+ type: node.type,
48
+ label: node.label,
49
+ metadataId: node.metadataId
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Build a `Map<toId, Array<{ from, relation }>>` of incoming edges once, so the
55
+ * transitive walk is linear in (nodes + edges) instead of calling the O(N)
56
+ * `findDependents` per visited node. This is the transitive form of
57
+ * `findDependents`; the per-hop semantics are identical.
58
+ */
59
+ function buildIncomingIndex(graph) {
60
+ const incoming = new Map();
61
+ const edges = Array.isArray(graph?.edges) ? graph.edges : [];
62
+ for (const edge of edges) {
63
+ if (!edge || edge.from == null || edge.to == null) continue;
64
+ const key = String(edge.to);
65
+ if (!incoming.has(key)) incoming.set(key, []);
66
+ incoming.get(key).push({ from: String(edge.from), relation: edge.relation });
67
+ }
68
+ return incoming;
69
+ }
70
+
71
+ /**
72
+ * Compute the blast radius of a node — every node that transitively depends on
73
+ * it (reverse edge closure).
74
+ *
75
+ * @param {object} graph a `buildWorkspaceMetadataGraph` envelope
76
+ * @param {string} originId the metadataId of the node being changed/removed
77
+ * @param {object} [options]
78
+ * @param {number} [options.maxNodes=500] hard cap on impacted nodes
79
+ * @param {number} [options.maxDistance] optional hop limit (omit = unbounded)
80
+ * @returns {object} `{ kind, version, origin, impacted[], byType, total, maxDistanceReached, truncated, summary, warnings }`
81
+ */
82
+ function deriveBlastRadius(graph, originId, options = {}) {
83
+ const maxNodes = Number.isFinite(options.maxNodes) && options.maxNodes > 0
84
+ ? Math.floor(options.maxNodes)
85
+ : DEFAULT_MAX_NODES;
86
+ const maxDistance = Number.isFinite(options.maxDistance) && options.maxDistance > 0
87
+ ? Math.floor(options.maxDistance)
88
+ : Infinity;
89
+
90
+ const empty = (warning) => ({
91
+ kind: BLAST_RADIUS_KIND,
92
+ version: BLAST_RADIUS_VERSION,
93
+ origin: null,
94
+ impacted: [],
95
+ byType: {},
96
+ total: 0,
97
+ maxDistanceReached: 0,
98
+ truncated: false,
99
+ summary: "No impact computed.",
100
+ warnings: warning ? [warning] : []
101
+ });
102
+
103
+ const id = safeString(originId).trim();
104
+ if (!graph || typeof graph !== "object" || !Array.isArray(graph.nodes)) {
105
+ return empty("graph missing or malformed");
106
+ }
107
+ if (!id) return empty("originId missing");
108
+
109
+ const nodesById = new Map(graph.nodes.map((node) => [node.id, node]));
110
+ const originNode = nodesById.get(id);
111
+ if (!originNode) return empty(`origin "${id}" not found in graph`);
112
+
113
+ const incoming = buildIncomingIndex(graph);
114
+
115
+ const visited = new Set([id]);
116
+ const impacted = [];
117
+ let truncated = false;
118
+ let maxDistanceReached = 0;
119
+
120
+ // FIFO queue → breadth-first, so the first time a node is reached is via its
121
+ // shortest dependency path (the most direct reason it goes stale).
122
+ const queue = [{ id, distance: 0 }];
123
+ while (queue.length) {
124
+ const current = queue.shift();
125
+ if (current.distance >= maxDistance) continue;
126
+ const dependents = incoming.get(current.id) || [];
127
+ for (const { from, relation } of dependents) {
128
+ if (visited.has(from)) continue;
129
+ const node = nodesById.get(from);
130
+ if (!node) continue;
131
+ if (impacted.length >= maxNodes) {
132
+ truncated = true;
133
+ continue;
134
+ }
135
+ visited.add(from);
136
+ const distance = current.distance + 1;
137
+ maxDistanceReached = Math.max(maxDistanceReached, distance);
138
+ impacted.push({
139
+ id: node.id,
140
+ type: node.type,
141
+ label: node.label,
142
+ metadataId: node.metadataId,
143
+ distance,
144
+ viaRelation: relation
145
+ });
146
+ queue.push({ id: from, distance });
147
+ }
148
+ }
149
+
150
+ // Deterministic order: nearest first, then by type, then by id.
151
+ impacted.sort((a, b) =>
152
+ a.distance - b.distance ||
153
+ a.type.localeCompare(b.type) ||
154
+ a.id.localeCompare(b.id)
155
+ );
156
+
157
+ const byType = {};
158
+ for (const entry of impacted) {
159
+ byType[entry.type] = (byType[entry.type] || 0) + 1;
160
+ }
161
+
162
+ return {
163
+ kind: BLAST_RADIUS_KIND,
164
+ version: BLAST_RADIUS_VERSION,
165
+ origin: summarizeOrigin(originNode),
166
+ impacted,
167
+ byType,
168
+ total: impacted.length,
169
+ maxDistanceReached,
170
+ truncated,
171
+ summary: summarizeBlastRadius(originNode, impacted, byType, truncated),
172
+ warnings: []
173
+ };
174
+ }
175
+
176
+ /**
177
+ * One human sentence for the inspector chip / PR comment / CLI line.
178
+ * Pure string assembly — never throws.
179
+ */
180
+ function summarizeBlastRadius(originNode, impacted, byType, truncated) {
181
+ const label = originNode?.label || originNode?.id || "node";
182
+ if (!impacted.length) {
183
+ return `Changing "${label}" has no downstream impact — nothing depends on it.`;
184
+ }
185
+ const parts = Object.keys(byType)
186
+ .sort()
187
+ .map((type) => `${byType[type]} ${type}`);
188
+ const tail = truncated ? " (truncated)" : "";
189
+ return `Changing "${label}" affects ${impacted.length} downstream node(s): ${parts.join(", ")}${tail}.`;
190
+ }
191
+
192
+ export {
193
+ BLAST_RADIUS_KIND,
194
+ BLAST_RADIUS_VERSION,
195
+ DEFAULT_MAX_NODES,
196
+ deriveBlastRadius,
197
+ summarizeBlastRadius
198
+ };
@@ -107,6 +107,7 @@
107
107
  "apps/workspace/lib/workspace-metadata-store.js",
108
108
  "apps/workspace/lib/workspace-metadata-graph.js",
109
109
  "apps/workspace/lib/workspace-metadata-selectors.js",
110
+ "apps/workspace/lib/workspace-metadata-impact.js",
110
111
  "apps/workspace/app/api/workspace/metadata-graph/route.js",
111
112
  "apps/workspace/app/api/workspace/swarm-condition/route.js",
112
113
  "apps/workspace/app/data-model/components/WorkspaceGraphInspectorPanel.jsx",
@@ -161,6 +161,17 @@ Every mutation lane emits the **same canonical receipt** (`@growthub/api-contrac
161
161
 
162
162
  **First-session continuation:** before acting, read the stream. Cite `receiptId`s, continue from `nextActions`, and inspect `rollbackRef` (previous version + delta index for publishes; sourceId for runs) before redoing anyone's work. Rejections come with `repairPlan[]` — follow it instead of retrying variations.
163
163
 
164
+ **Outcome completion:** for regular user work, do not stop at "proposal created" or "run attempted" when the requested business outcome requires deliverables. The completion proof must live in governed state: successful run ids or source ids, connected output rows, durable storage/reference paths where applicable, review status, and a concise documentation/receipt trail. Failed or partial rows stay as evidence, but they do not count as delivered outputs. Human-review states remain explicit; an agent can execute and persist, but it does not silently approve or launch work that requires workspace-admin or super-admin judgment.
165
+
166
+ ## Intelligence layer — graph + blast radius (read-only)
167
+
168
+ After a mutation lands, the platform **understands** it. Two read-only, secret-free surfaces:
169
+
170
+ - **World model** — `GET /api/workspace/metadata-graph` projects the live config + source-record sidecar into a typed node/edge graph (`buildWorkspaceMetadataStore → buildWorkspaceMetadataGraph`, `lib/workspace-metadata-graph.js`); the Workspace Map (`/workspace-map`) renders the same graph. Every governed object becomes nodes; every dependency a deterministic edge (`bindsToObject`, `usesField`, `containsWidget`, `readsObject`/`writesObject`, `materializes`, …). A landed mutation expands this graph — the workspace knows more about itself than before.
171
+ - **Causal impact** — the graph ships single-hop `findDependents(graph, nodeId)`; the transitive closure (the real blast radius) is `deriveBlastRadius(graph, nodeId)` in `lib/workspace-metadata-impact.js` — a deterministic, cycle-safe BFS of incoming edges returning every reachable dependent with hop distance and via-relation. This is the difference between *"what directly uses this field?"* (catalog) and *"if this field changes, which widgets, dashboards, and delivered workspace kits go stale?"* (intelligence). Verified live: editing `customers.mrr` → widget (`usesField`) → dashboard (`containsWidget`) → workerKit (`materializes`). Conceptual map: `docs/OPERATING_THE_GOVERNED_UNIVERSE_V1.md` in the source repo.
172
+
173
+ Both are derived projections — they own no state and mutate nothing. Use them to size a change *before* you PATCH (pair with `patch/preflight`) and to explain what an accepted mutation affects.
174
+
164
175
  ## Applications as governed entities (Control Plane V1)
165
176
 
166
177
  Applications are first-class governed objects, not loose files. The source of truth is the `workspace-app-registry` Data Model object (objectType `"app-surface"`, preset ships in the Data Model) — one row per application, referencing its governed parts by id: `dashboardIds`, `workflowRefs` (`objectId:RowName`), `dataSourceIds`, `registryIds`. Rows mutate through the normal PATCH lane (policy + receipts apply).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthub/cli",
3
- "version": "0.14.8",
3
+ "version": "0.14.9",
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": {