@growthub/cli 0.14.1 → 0.14.3

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 (49) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +4 -2
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/agent-outcomes/route.js +85 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/apps/route.js +187 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +36 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/patch/preflight/route.js +152 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +21 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +88 -1
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/login/route.js +3 -2
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/logout/route.js +3 -2
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-agent-auth/status/route.js +3 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +86 -2
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +2 -2
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +21 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/workflow/publish/route.js +338 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +1 -1
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +49 -2
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +54 -11
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +113 -36
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxAgentAuthPanel.jsx +34 -14
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +7 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +35 -169
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +26 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/local-intelligence-browser-access.js +516 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +85 -7
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +3 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +1 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +5 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +8 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +3 -0
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +4 -2
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-publish.js +179 -0
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +1 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-agent-auth.js +82 -27
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +4 -2
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +89 -5
  38. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-app-registry.js +539 -0
  39. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +11 -2
  40. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +24 -0
  41. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-outcome-receipts.js +157 -0
  42. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +400 -0
  43. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +6 -0
  44. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +3 -0
  45. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +364 -0
  46. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  47. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +10 -0
  48. package/assets/worker-kits/growthub-custom-workspace-starter-v1/skills/governed-workspace-mutation/SKILL.md +203 -0
  49. package/package.json +2 -2
@@ -34,15 +34,14 @@
34
34
  * on-disk auth state; this module only records *readiness*, not secrets.
35
35
  *
36
36
  * The status semantics are deliberately conservative:
37
- * - "active" a real auth probe confirmed authentication (auth-status
38
- * exit 0 with auth-shaped output, or a clean login exit)
39
- * - "reachable" the binary is callable (version probe exit 0) — but
40
- * authentication is NOT yet confirmed
37
+ * - "active" the selected pinned host CLI is callable and ready for the
38
+ * local agent-host bridge, or a clean login exit completed
39
+ * - "reachable" legacy metadata value treated as active by the UI
41
40
  * - "stale" the binary printed auth-shaped failure output
42
41
  * - "missing" binary not found on PATH
43
42
  *
44
- * A `--version` probe NEVER promotes to "active". The next sandbox-run is
45
- * the final source of truth for session readiness.
43
+ * A successful host probe promotes to "active" because the sidecar represents
44
+ * selected local host readiness, not provider-account auth semantics.
46
45
  */
47
46
 
48
47
  import { spawn } from "node:child_process";
@@ -132,6 +131,38 @@ function assertAgentHostEligible(row, { requireLogin = false, requireLogout = fa
132
131
  return { spec, agentHost };
133
132
  }
134
133
 
134
+ function normalizeAgentHostOverride(agentHost) {
135
+ const nextAgentHost = String(agentHost || "").trim();
136
+ if (!nextAgentHost) return "";
137
+ if (!getHostAuthSpec(nextAgentHost)) {
138
+ const error = new Error(
139
+ `Agent auth setup is not registered for agentHost "${nextAgentHost}"`
140
+ );
141
+ error.code = "SANDBOX_AGENT_AUTH_HOST_UNSUPPORTED";
142
+ throw error;
143
+ }
144
+ return nextAgentHost;
145
+ }
146
+
147
+ function applyAgentHostOverride(row, agentHost) {
148
+ if (!agentHost) return row;
149
+ return {
150
+ ...row,
151
+ runLocality: "local",
152
+ adapter: "local-agent-host",
153
+ agentHost
154
+ };
155
+ }
156
+
157
+ function buildAgentHostSelectionPatch(agentHost) {
158
+ if (!agentHost) return {};
159
+ return {
160
+ runLocality: "local",
161
+ adapter: "local-agent-host",
162
+ agentHost
163
+ };
164
+ }
165
+
135
166
  function resolveHostBinary(row, spec) {
136
167
  const candidates = [row?.agentCommand, row?.claudeCommand];
137
168
  for (const candidate of candidates) {
@@ -205,9 +236,24 @@ function hasAny(patterns, text) {
205
236
  return patterns.some((p) => p.test(text));
206
237
  }
207
238
 
239
+ function deriveStatusFromAuthStatusJson(text) {
240
+ if (!text || typeof text !== "string") return null;
241
+ const trimmed = text.trim();
242
+ if (!trimmed.startsWith("{")) return null;
243
+ try {
244
+ const parsed = JSON.parse(trimmed);
245
+ if (parsed && typeof parsed === "object" && typeof parsed.loggedIn === "boolean") {
246
+ return parsed.loggedIn ? "active" : "stale";
247
+ }
248
+ } catch {}
249
+ return null;
250
+ }
251
+
208
252
  function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", spawnError }) {
209
253
  if (spawnError) return spawnError.notFound ? "missing" : null;
210
254
  const combined = `${stdout}\n${stderr}`;
255
+ const jsonStatus = deriveStatusFromAuthStatusJson(stdout) || deriveStatusFromAuthStatusJson(stderr);
256
+ if (jsonStatus) return jsonStatus;
211
257
  if (hasAny(UNKNOWN_SUBCOMMAND_PATTERNS, combined)) return null; // fall back
212
258
  if (hasAny(STALE_AUTH_PATTERNS, combined)) return "stale";
213
259
  if (exitCode === 0) return "active";
@@ -219,7 +265,7 @@ function deriveStatusFromAuthStatusProbe({ exitCode, stdout = "", stderr = "", s
219
265
 
220
266
  function deriveStatusFromVersionProbe({ exitCode, stderr, spawnError }) {
221
267
  if (spawnError) return spawnError.notFound ? "missing" : "unknown";
222
- if (typeof exitCode === "number" && exitCode === 0) return "reachable";
268
+ if (typeof exitCode === "number" && exitCode === 0) return "active";
223
269
  const text = String(stderr || "");
224
270
  if (hasAny(STALE_AUTH_PATTERNS, text)) return "stale";
225
271
  return "unknown";
@@ -237,8 +283,8 @@ function deriveLoginStatus({ exitCode, stderr, stdout, timedOut, spawnError }) {
237
283
  function shortMessage({ status, label, exitCode, error, loginUrl }) {
238
284
  const name = label || "Local agent CLI";
239
285
  if (error) return `${name}: ${redactSecrets(String(error))}`;
240
- if (status === "active") return loginUrl ? "Login completed." : "Authenticated.";
241
- if (status === "reachable") return "CLI reachable. Run Login to verify authentication.";
286
+ if (status === "active") return loginUrl ? "Login completed." : "Active.";
287
+ if (status === "reachable") return "Active.";
242
288
  if (status === "stale") return "Authentication needs setup. Run Login, then run the sandbox again.";
243
289
  if (status === "missing") return `${name} not found. Install it and try again.`;
244
290
  if (status === "checking") return `Checking ${name}…`;
@@ -329,16 +375,18 @@ function runCommand({ binary, args, cwd, timeoutMs, stdin }) {
329
375
  // Public API — login / logout / status
330
376
  // ──────────────────────────────────────────────────────────────────────────
331
377
 
332
- async function runAgentLogin({ objectId, name }) {
378
+ async function runAgentLogin({ objectId, name, agentHost }) {
333
379
  const workspaceConfig = await readWorkspaceConfig();
334
380
  const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
335
381
  if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
336
382
  if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
337
383
 
338
- const { spec, agentHost } = assertAgentHostEligible(row, { requireLogin: true });
384
+ const selectedAgentHost = normalizeAgentHostOverride(agentHost);
385
+ const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
386
+ const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogin: true });
339
387
 
340
- const binary = resolveHostBinary(row, spec);
341
- const cwd = resolveCwd(row);
388
+ const binary = resolveHostBinary(effectiveRow, spec);
389
+ const cwd = resolveCwd(effectiveRow);
342
390
  const startedAt = Date.now();
343
391
 
344
392
  const result = await runCommand({
@@ -356,20 +404,21 @@ async function runAgentLogin({ objectId, name }) {
356
404
 
357
405
  const patch = buildRowPatch({
358
406
  status,
359
- provider: agentHost,
407
+ provider: effectiveAgentHost,
360
408
  checkedAt,
361
409
  exitCode: result.exitCode,
362
410
  loginUrl,
363
411
  label: spec.label,
364
412
  spawnError: result.spawnError
365
413
  });
414
+ Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
366
415
 
367
416
  await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
368
417
 
369
418
  return {
370
419
  ok: status === "active",
371
420
  status,
372
- provider: agentHost,
421
+ provider: effectiveAgentHost,
373
422
  label: spec.label,
374
423
  binary,
375
424
  cwd,
@@ -384,16 +433,18 @@ async function runAgentLogin({ objectId, name }) {
384
433
  };
385
434
  }
386
435
 
387
- async function runAgentLogout({ objectId, name }) {
436
+ async function runAgentLogout({ objectId, name, agentHost }) {
388
437
  const workspaceConfig = await readWorkspaceConfig();
389
438
  const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
390
439
  if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
391
440
  if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
392
441
 
393
- const { spec, agentHost } = assertAgentHostEligible(row, { requireLogout: true });
442
+ const selectedAgentHost = normalizeAgentHostOverride(agentHost);
443
+ const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
444
+ const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow, { requireLogout: true });
394
445
 
395
- const binary = resolveHostBinary(row, spec);
396
- const cwd = resolveCwd(row);
446
+ const binary = resolveHostBinary(effectiveRow, spec);
447
+ const cwd = resolveCwd(effectiveRow);
397
448
  const startedAt = Date.now();
398
449
 
399
450
  let exitCode = null;
@@ -422,7 +473,7 @@ async function runAgentLogout({ objectId, name }) {
422
473
 
423
474
  const patch = buildRowPatch({
424
475
  status,
425
- provider: agentHost,
476
+ provider: effectiveAgentHost,
426
477
  checkedAt,
427
478
  exitCode,
428
479
  loginUrl: null,
@@ -432,13 +483,14 @@ async function runAgentLogout({ objectId, name }) {
432
483
  patch.agentAuthLastMessage = spawnError?.notFound
433
484
  ? shortMessage({ status: "missing", label: spec.label })
434
485
  : `${spec.label} logged out — auth will be required before next run.`;
486
+ Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
435
487
 
436
488
  await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
437
489
 
438
490
  return {
439
491
  ok: !spawnError,
440
492
  status,
441
- provider: agentHost,
493
+ provider: effectiveAgentHost,
442
494
  label: spec.label,
443
495
  binary,
444
496
  cwd,
@@ -451,16 +503,18 @@ async function runAgentLogout({ objectId, name }) {
451
503
  };
452
504
  }
453
505
 
454
- async function checkAgentStatus({ objectId, name }) {
506
+ async function checkAgentStatus({ objectId, name, agentHost }) {
455
507
  const workspaceConfig = await readWorkspaceConfig();
456
508
  const { object, row, rowIndex } = findSandboxRow(workspaceConfig, objectId, name);
457
509
  if (!object) throw notFoundError(`no sandbox-environment object with id ${objectId}`);
458
510
  if (!row) throw notFoundError(`no sandbox row named ${name} in object ${objectId}`);
459
511
 
460
- const { spec, agentHost } = assertAgentHostEligible(row);
512
+ const selectedAgentHost = normalizeAgentHostOverride(agentHost);
513
+ const effectiveRow = applyAgentHostOverride(row, selectedAgentHost);
514
+ const { spec, agentHost: effectiveAgentHost } = assertAgentHostEligible(effectiveRow);
461
515
 
462
- const binary = resolveHostBinary(row, spec);
463
- const cwd = resolveCwd(row);
516
+ const binary = resolveHostBinary(effectiveRow, spec);
517
+ const cwd = resolveCwd(effectiveRow);
464
518
 
465
519
  // Two-phase probe:
466
520
  // 1. If the catalog declares an auth-status subcommand, try it first.
@@ -503,20 +557,21 @@ async function checkAgentStatus({ objectId, name }) {
503
557
 
504
558
  const patch = buildRowPatch({
505
559
  status,
506
- provider: agentHost,
560
+ provider: effectiveAgentHost,
507
561
  checkedAt,
508
562
  exitCode: usedResult.exitCode,
509
563
  loginUrl: null,
510
564
  label: spec.label,
511
565
  spawnError: usedResult.spawnError
512
566
  });
567
+ Object.assign(patch, buildAgentHostSelectionPatch(selectedAgentHost));
513
568
 
514
569
  await applyRowPatch({ workspaceConfig, object, rowIndex, patch });
515
570
 
516
571
  return {
517
572
  ok: status === "active",
518
573
  status,
519
- provider: agentHost,
574
+ provider: effectiveAgentHost,
520
575
  label: spec.label,
521
576
  binary,
522
577
  cwd,
@@ -70,6 +70,7 @@ function deriveSandboxServerlessState(input = {}) {
70
70
 
71
71
  const locality = clean(row.runLocality).toLowerCase() === "serverless" ? "serverless" : "local";
72
72
  const isServerless = locality === "serverless";
73
+ const browserAccess = ["true", "1", "on", "yes"].includes(clean(row.browserAccess).toLowerCase());
73
74
  const adapterId = clean(row.adapter);
74
75
  const adapterChosen = Boolean(adapterId);
75
76
 
@@ -162,8 +163,8 @@ function deriveSandboxServerlessState(input = {}) {
162
163
  label: isServerless ? "Run on the scheduler" : "Run locally",
163
164
  status: "optional",
164
165
  description: isServerless
165
- ? "Once the scheduler, auth, and store are ready, run delegates to the serverless scheduler."
166
- : "Run this workflow in-process.",
166
+ ? `Once the scheduler, auth, and store are ready, run delegates to the serverless scheduler.${browserAccess ? " Browser access travels with the run in the growthub-sandbox-run-v1 envelope (sandbox.browserAccess), so the remote handler grants the same capability as a local run." : ""}`
167
+ : `Run this workflow in-process.${browserAccess ? (adapterId === "local-intelligence" ? " Browser access is executed by the local-intelligence browser bridge." : " Browser access is engaged through the selected agent host's first-party browser integration.") : ""}`,
167
168
  action: inline({ id: "run-sandbox", label: "Run" }),
168
169
  });
169
170
 
@@ -192,6 +193,7 @@ function deriveSandboxServerlessState(input = {}) {
192
193
  version: 1,
193
194
  locality,
194
195
  isServerless,
196
+ browserAccess,
195
197
  adapterChosen,
196
198
  schedulerLinked,
197
199
  schedulerHealthy,
@@ -27,6 +27,10 @@
27
27
  * - Backwards-compatible: a workspace with no `provenance` block still
28
28
  * produces a valid (generic) activation state.
29
29
  *
30
+ * Sole import: lib/workspace-app-registry.js — the equally pure,
31
+ * dependency-free App Registry derivation module the Fleet lens reads
32
+ * (roadmap Item 4's runtime surface-metadata source).
33
+ *
30
34
  * The output shape:
31
35
  *
32
36
  * {
@@ -54,6 +58,13 @@
54
58
  * }
55
59
  */
56
60
 
61
+ import {
62
+ APP_REGISTRY_OBJECT_ID,
63
+ deriveAppHealth,
64
+ deriveAppNextAction,
65
+ listAppSurfaceRows
66
+ } from "./workspace-app-registry.js";
67
+
57
68
  const ACTIVATION_KIND = "growthub-workspace-activation-state-v1";
58
69
  const ACTIVATION_VERSION = 1;
59
70
 
@@ -1217,16 +1228,85 @@ function deriveAppBuildLensState(input = {}) {
1217
1228
  };
1218
1229
  }
1219
1230
 
1231
+ /**
1232
+ * Fleet lens (roadmap Item 4 — now un-staged). The precondition named in
1233
+ * docs/ROADMAP_IMPACT_ITEMS_V1.md is satisfied: the runtime surface-metadata
1234
+ * source is the governed `workspace-app-registry` Data Model object
1235
+ * (lib/workspace-app-registry.js). Each registered application derives one
1236
+ * step: status from its health rollup (linked workflows/APIs/data sources +
1237
+ * persistence/deploy flags), description from its computed next action, href
1238
+ * into the real surface that unblocks it. Pure, no secrets, never throws.
1239
+ */
1240
+ function deriveFleetLensState(input = {}) {
1241
+ const cfg = isPlainObject(input?.workspaceConfig) ? input.workspaceConfig : {};
1242
+ const records = isPlainObject(input?.workspaceSourceRecords) ? input.workspaceSourceRecords : {};
1243
+ const dur = deriveRuntimeDurability(input?.metadataGraph);
1244
+ const runtimeFlags = {
1245
+ durable: dur.durable,
1246
+ readOnly: dur.readOnly,
1247
+ deployReady: deriveDeployLensState(input).complete
1248
+ };
1249
+
1250
+ const rows = listAppSurfaceRows(cfg);
1251
+ const steps = [];
1252
+ if (rows.length === 0) {
1253
+ steps.push({
1254
+ id: "register-first-app",
1255
+ label: "Register your first application surface",
1256
+ description: `Create the ${APP_REGISTRY_OBJECT_ID} object (objectType "app-surface") and add one row per app — name, surfacePath, and refs to its dashboards/workflows/data sources/APIs.`,
1257
+ status: "pending",
1258
+ href: "/data-model",
1259
+ cta: "Open Data Model",
1260
+ });
1261
+ }
1262
+ for (const row of rows) {
1263
+ const health = deriveAppHealth(cfg, records, row, runtimeFlags);
1264
+ const next = deriveAppNextAction(row, health);
1265
+ const appId = safeString(row.appId).trim() || safeString(row.Name).trim();
1266
+ steps.push({
1267
+ id: `app-${appId}`,
1268
+ label: safeString(row.Name).trim(),
1269
+ description: health.status === "ready"
1270
+ ? `Healthy — ${health.linkedCount} linked object(s).`
1271
+ : next.label,
1272
+ status: health.status === "ready" ? "complete" : health.status === "blocked" ? "blocked" : "pending",
1273
+ href: next.href,
1274
+ hint: health.status === "blocked" ? health.blockers[0] : "",
1275
+ cta: health.status === "ready" ? "Open app" : "Unblock app",
1276
+ });
1277
+ }
1278
+ for (const step of steps) {
1279
+ if (!step.hint) delete step.hint;
1280
+ }
1281
+
1282
+ const { totalCount, completedCount, complete, nextStepId } = scoreLensSteps(steps);
1283
+ const headline = rows.length === 0
1284
+ ? "Register the applications this workspace operates."
1285
+ : complete
1286
+ ? `All ${rows.length} application(s) are healthy.`
1287
+ : `Operating ${rows.length} application(s) — ${completedCount} healthy.`;
1288
+ const nextStep = steps.find((s) => s.id === nextStepId);
1289
+ return {
1290
+ kind: LENS_STATE_KIND,
1291
+ lensId: "fleet",
1292
+ title: "Application fleet",
1293
+ headline,
1294
+ subheadline: complete
1295
+ ? "Assign the next capability to an agent, or register another app."
1296
+ : (nextStep ? `Next: ${nextStep.label}.` : "Resolve each app's blockers."),
1297
+ complete,
1298
+ completedCount,
1299
+ totalCount,
1300
+ nextStepId,
1301
+ steps,
1302
+ };
1303
+ }
1304
+
1220
1305
  /**
1221
1306
  * The lens registry. The activation deriver is the `primary` lens (it keeps
1222
1307
  * its own v1 state kind for backwards compatibility); every other entry is a
1223
1308
  * secondary lens that plugs into the same panel and the same swarm packet.
1224
1309
  * Adding a roadmap item is "register a deriver" — no new surface.
1225
- *
1226
- * NB: a Fleet / multi-app lens (roadmap Item 4) is intentionally NOT registered
1227
- * — the exported workspace runtime exposes no in-artifact multi-app surface
1228
- * registry to derive from. See docs/ROADMAP_IMPACT_ITEMS_V1.md (it stays staged
1229
- * until a runtime surface-metadata source exists).
1230
1310
  */
1231
1311
  const WORKSPACE_LENS_REGISTRY = [
1232
1312
  { id: "activation", title: "Activation", primary: true, derive: deriveWorkspaceActivationState },
@@ -1235,6 +1315,7 @@ const WORKSPACE_LENS_REGISTRY = [
1235
1315
  { id: "deploy", title: "Deploy readiness", primary: false, derive: deriveDeployLensState },
1236
1316
  { id: "tasks", title: "Task management", primary: false, derive: deriveTaskLensState },
1237
1317
  { id: "app-build", title: "Application buildout", primary: false, derive: deriveAppBuildLensState },
1318
+ { id: "fleet", title: "Application fleet", primary: false, derive: deriveFleetLensState },
1238
1319
  ];
1239
1320
 
1240
1321
  function getLensEntry(lensId) {
@@ -1559,6 +1640,9 @@ export {
1559
1640
  deriveDeployLensState,
1560
1641
  deriveTaskLensState,
1561
1642
  deriveAppBuildLensState,
1643
+ // Fleet / multi-app lens (roadmap Item 4 — derives from the governed
1644
+ // workspace-app-registry object, lib/workspace-app-registry.js)
1645
+ deriveFleetLensState,
1562
1646
  // Swarm-assignable condition packet (roadmap Item 8)
1563
1647
  deriveSwarmConditionPacket,
1564
1648
  // Workspace contribution graph (daily-ritual visualization)