@growthub/cli 0.14.3 → 0.14.4

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.
@@ -9211,6 +9211,14 @@ body.workspace-rail-collapsed .workspace-builder.workspace-lens-page,
9211
9211
  .dm-swarm-body { display: flex; flex-direction: column; min-height: 0; }
9212
9212
  .dm-swarm-cockpit { flex: 1; min-height: 0; overflow-y: auto; padding: 12px 14px 16px; display: flex; flex-direction: column; gap: 10px; }
9213
9213
  .dm-swarm-cockpit-list { display: flex; flex-direction: column; gap: 8px; }
9214
+ .dm-ceo-tabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; margin: -4px 0 2px; }
9215
+ .dm-ceo-tabs button { min-height: 30px; padding: 5px 10px; border: 1px solid #e5e7eb; border-radius: 5px; background: #fff; color: #374151; font-size: 12px; font-weight: 600; cursor: pointer; }
9216
+ .dm-ceo-tabs button:hover { border-color: #cbd5e1; color: #111827; }
9217
+ .dm-ceo-tabs button.is-active { background: #111827; border-color: #111827; color: #fff; }
9218
+ /* CEO fleet list — layout-only grouping; the parent .dm-swarm-cockpit owns the
9219
+ scroll, so this never introduces a second scrollbar. Visible count is capped
9220
+ in the component (CEO_FLEET_VISIBLE_CAP). */
9221
+ .dm-ceo-report-list { display: grid; gap: 8px; }
9214
9222
  .dm-swarm-section-row { display: flex; align-items: center; justify-content: space-between; margin-top: 4px; }
9215
9223
 
9216
9224
  /* Run card — surface chrome comes from dm-helper-toolcall in the JSX. */
@@ -9218,6 +9226,9 @@ body.workspace-rail-collapsed .workspace-builder.workspace-lens-page,
9218
9226
  .dm-swarm-card-head { display: flex; align-items: center; gap: 8px; }
9219
9227
  .dm-swarm-card-title { flex: 1; }
9220
9228
  .dm-swarm-card-action { height: 24px; padding: 0 7px; }
9229
+ .dm-ceo-card-redirect { opacity: 0; transition: opacity .12s ease, border-color .12s ease, color .12s ease; }
9230
+ .dm-swarm-card:hover .dm-ceo-card-redirect,
9231
+ .dm-swarm-card:focus-within .dm-ceo-card-redirect { opacity: 1; }
9221
9232
  .dm-swarm-card-meta { display: flex; align-items: center; gap: 10px; }
9222
9233
  .dm-swarm-card-meta .dm-run-console__hint { font-size: 12px; }
9223
9234
  .dm-swarm-card-kind { font-weight: 600; }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * CEO Agent Teams — the atomic, reusable team-configuration layer for the /ceo
3
+ * surface (CEO_PRIMITIVE_COCKPIT_ROADMAP_V1).
4
+ *
5
+ * Distinction this module encodes:
6
+ * - FLEET (runtime/oversight) = existing `swarm-workflows` rows, readiness,
7
+ * run state, failures, receipts, Background Tasks. UNCHANGED by this module.
8
+ * - AGENT TEAMS (atomic configuration) = reusable blueprints describing the
9
+ * orchestrator, sub-agent roles, skills, processes, workflow
10
+ * responsibilities, and outcome criteria of a swarm.
11
+ *
12
+ * This is a PURE module — no React, no fetch, no fs, no writes, no localStorage.
13
+ * It introduces NO new runtime, executor, API route, PATCH allowlist field, or
14
+ * object type: Agent Teams live in a governed Data Model object of the EXISTING
15
+ * `custom` objectType, created through the EXISTING `dataModel.object.create`
16
+ * helper/apply lane, and they only ever *inform* a `/swarm` proposal — the
17
+ * server still builds the `agent-swarm-v1` graph, the run still lands in
18
+ * `swarm-workflows`, and execution still happens through `sandbox-run`.
19
+ *
20
+ * An Agent Team record never executes anything.
21
+ */
22
+
23
+ export const AGENT_SWARM_TEAMS_OBJECT_ID = "agent-swarm-teams";
24
+ export const AGENT_SWARM_TEAMS_LABEL = "Agent Swarm Teams";
25
+
26
+ // Visible row columns. "Name" is the capital-N identity column (Data Model grid
27
+ // convention). Array-shaped fields are stored as readable strings so we never
28
+ // fight the row-value contract (no typed arrays in rows).
29
+ export const AGENT_SWARM_TEAMS_COLUMNS = [
30
+ "id",
31
+ "Name",
32
+ "status",
33
+ "teamPurpose",
34
+ "orchestratorRole",
35
+ "orchestratorPrompt",
36
+ "subAgentRoles",
37
+ "skills",
38
+ "processes",
39
+ "workflowResponsibilities",
40
+ "outcomeCriteria",
41
+ "defaultRunLocality",
42
+ "defaultAdapter",
43
+ "linkedSwarmWorkflowName",
44
+ "governanceNotes",
45
+ "createdAt",
46
+ "updatedAt",
47
+ ];
48
+
49
+ function clean(value) {
50
+ return typeof value === "string" ? value.trim() : "";
51
+ }
52
+
53
+ function slugify(value) {
54
+ return clean(value).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
55
+ }
56
+
57
+ // One clearly-labeled blueprint row so a freshly-created table shows the shape
58
+ // (and is honestly an example, not live state).
59
+ function blueprintRow() {
60
+ return {
61
+ id: "team-research-synthesis",
62
+ Name: "Example — Research & Synthesis Team",
63
+ status: "blueprint",
64
+ teamPurpose: "Reusable blueprint: research a topic and synthesize a cited brief.",
65
+ orchestratorRole: "Research Lead",
66
+ orchestratorPrompt: "Decompose the objective into independent research subtasks for the team.",
67
+ subAgentRoles: "Researcher; Analyst; Synthesizer",
68
+ skills: "web-research; summarization; critique",
69
+ processes: "gather → analyze → synthesize",
70
+ workflowResponsibilities: "Researcher gathers facts; Analyst stress-tests; Synthesizer writes the brief",
71
+ outcomeCriteria: "A cited brief with risks and a clear recommendation",
72
+ defaultRunLocality: "local",
73
+ defaultAdapter: "local-intelligence",
74
+ linkedSwarmWorkflowName: "",
75
+ governanceNotes: "Blueprint only — launch through /swarm; the run lands in swarm-workflows and emits receipts.",
76
+ createdAt: "",
77
+ updatedAt: "",
78
+ };
79
+ }
80
+
81
+ /** The governed Agent Teams object definition (existing `custom` objectType). */
82
+ export function buildAgentSwarmTeamsObject({ includeBlueprint = true } = {}) {
83
+ return {
84
+ id: AGENT_SWARM_TEAMS_OBJECT_ID,
85
+ label: AGENT_SWARM_TEAMS_LABEL,
86
+ objectType: "custom",
87
+ columns: AGENT_SWARM_TEAMS_COLUMNS.slice(),
88
+ rows: includeBlueprint ? [blueprintRow()] : [],
89
+ binding: { mode: "manual", source: AGENT_SWARM_TEAMS_LABEL },
90
+ };
91
+ }
92
+
93
+ /**
94
+ * A governed proposal that CREATES the Agent Teams table through the EXISTING
95
+ * `dataModel.object.create` helper/apply lane. No new proposal type, no new
96
+ * objectType — objectType stays `custom`.
97
+ */
98
+ export function buildCreateAgentTeamsProposal() {
99
+ return {
100
+ type: "dataModel.object.create",
101
+ affectedField: "dataModel",
102
+ payload: { object: buildAgentSwarmTeamsObject({ includeBlueprint: true }) },
103
+ rationale: "Create the governed Agent Swarm Teams table — reusable swarm blueprints (config, not runtime).",
104
+ };
105
+ }
106
+
107
+ export function findAgentTeamsObject(workspaceConfig) {
108
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
109
+ return objects.find((o) => o?.id === AGENT_SWARM_TEAMS_OBJECT_ID) || null;
110
+ }
111
+
112
+ export function findAgentTeams(workspaceConfig) {
113
+ const object = findAgentTeamsObject(workspaceConfig);
114
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
115
+ const seen = new Set();
116
+ return rows
117
+ .filter((row) => row && clean(row.Name))
118
+ .map((row, index) => {
119
+ const name = clean(row.Name);
120
+ // Stable atomic identity: explicit id, else a slug of Name, else index —
121
+ // de-duplicated so two same-named blueprints never collide as React keys
122
+ // (the same fix applied to fleet reports).
123
+ let teamId = clean(row.id) || slugify(name) || `team-${index + 1}`;
124
+ while (seen.has(teamId)) teamId = `${teamId}-${index + 1}`;
125
+ seen.add(teamId);
126
+ return {
127
+ teamId,
128
+ name,
129
+ status: clean(row.status) || "blueprint",
130
+ teamPurpose: clean(row.teamPurpose),
131
+ orchestratorRole: clean(row.orchestratorRole),
132
+ orchestratorPrompt: clean(row.orchestratorPrompt),
133
+ subAgentRoles: clean(row.subAgentRoles),
134
+ skills: clean(row.skills),
135
+ processes: clean(row.processes),
136
+ workflowResponsibilities: clean(row.workflowResponsibilities),
137
+ outcomeCriteria: clean(row.outcomeCriteria),
138
+ defaultRunLocality: clean(row.defaultRunLocality),
139
+ defaultAdapter: clean(row.defaultAdapter),
140
+ linkedSwarmWorkflowName: clean(row.linkedSwarmWorkflowName),
141
+ governanceNotes: clean(row.governanceNotes),
142
+ };
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Project the Agent Teams configuration layer for the cockpit. Pure.
148
+ * { present, count, teams[], canCreate }
149
+ */
150
+ export function deriveAgentTeamsState({ workspaceConfig } = {}) {
151
+ const object = findAgentTeamsObject(workspaceConfig);
152
+ const teams = findAgentTeams(workspaceConfig);
153
+ return {
154
+ objectId: AGENT_SWARM_TEAMS_OBJECT_ID,
155
+ present: Boolean(object),
156
+ count: teams.length,
157
+ teams,
158
+ // The table is created through the governed create lane when absent.
159
+ canCreate: !object,
160
+ };
161
+ }
162
+
163
+ function joinAgents(subAgentRoles) {
164
+ return clean(subAgentRoles)
165
+ .split(/[;,\n]/)
166
+ .map((part) => part.trim())
167
+ .filter(Boolean);
168
+ }
169
+
170
+ /**
171
+ * Build the /swarm composer seed text from an Agent Team blueprint. This is
172
+ * PROPOSE-ONLY seed copy: it prefills the helper composer so the user proposes
173
+ * a governed swarm "from this blueprint". The model + server still produce the
174
+ * proposal and build the agent-swarm-v1 graph; nothing here executes.
175
+ */
176
+ export function buildSwarmIntentFromTeam(team) {
177
+ if (!team) return "Propose a governed agent swarm:";
178
+ const agents = joinAgents(team.subAgentRoles);
179
+ const target = [
180
+ team.defaultAdapter ? `adapter ${team.defaultAdapter}` : "",
181
+ team.defaultRunLocality ? `${team.defaultRunLocality} run target` : "",
182
+ ].filter(Boolean).join(", ");
183
+ // Carry the full atomic configuration forward so the blueprint is a true
184
+ // configuration inversion of the swarm — still propose-only seed copy; the
185
+ // server builds the agent-swarm-v1 graph and nothing executes here.
186
+ const parts = [
187
+ `Propose a governed agent swarm from the Agent Team blueprint "${team.name}".`,
188
+ team.teamPurpose ? `Objective: ${team.teamPurpose}` : "",
189
+ team.orchestratorRole || team.orchestratorPrompt
190
+ ? `Orchestrator${team.orchestratorRole ? ` (${team.orchestratorRole})` : ""}: ${team.orchestratorPrompt || "plan the work for the team"}.`
191
+ : "",
192
+ agents.length ? `Sub-agents: ${agents.join(", ")}.` : "",
193
+ team.skills ? `Skills: ${team.skills}.` : "",
194
+ team.processes ? `Process: ${team.processes}.` : "",
195
+ team.workflowResponsibilities ? `Workflow responsibilities: ${team.workflowResponsibilities}.` : "",
196
+ team.outcomeCriteria ? `Outcome criteria: ${team.outcomeCriteria}.` : "",
197
+ target ? `Preferred execution target: ${target}.` : "",
198
+ team.governanceNotes ? `Governance notes: ${team.governanceNotes}.` : "",
199
+ ];
200
+ return parts.filter(Boolean).join(" ");
201
+ }
202
+
203
+ export function summarizeTeam(team) {
204
+ const agents = joinAgents(team?.subAgentRoles).length;
205
+ return [
206
+ team?.orchestratorRole ? `orchestrator: ${team.orchestratorRole}` : null,
207
+ agents ? `${agents} sub-agent${agents === 1 ? "" : "s"}` : null,
208
+ ].filter(Boolean).join(" · ");
209
+ }
210
+
211
+ export default deriveAgentTeamsState;
@@ -0,0 +1,325 @@
1
+ /**
2
+ * CEO bootstrap projection — the first-use closed-loop harness for the CEO
3
+ * cockpit (CEO_PRIMITIVE_COCKPIT_ROADMAP_V1, follow-up to R1/R2).
4
+ *
5
+ * This is a PURE deriver — no React, no fetch, no fs, no config writes, no
6
+ * localStorage. It derives TWO modes from workspace state alone:
7
+ *
8
+ * - "bootstrap" — shown until the workspace has a CEO completion marker.
9
+ * A state-derived smoke-test checklist that proves every
10
+ * CEO primitive end-to-end (orchestrate → create → test →
11
+ * launch → observe → review → govern → complete).
12
+ * - "operational" — the existing fleet cockpit, after completion.
13
+ *
14
+ * The mode comes from a completion marker that lives in workspace CONFIG (a
15
+ * field on the existing well-known workspace-helper sandbox row), NEVER a
16
+ * browser flag. It introduces no new object type, no new API, no new PATCH
17
+ * field, and no new execution path: the checklist is a projection over the
18
+ * existing swarm fleet + receipt stream, and completion is stamped through the
19
+ * existing helper/apply lane via the `ceo.bootstrap.complete` proposal.
20
+ *
21
+ * Completion is gated on config-provable evidence — a governed swarm workflow
22
+ * that is ready AND has a COMPLETED run on its row — so "done" means the loop
23
+ * actually ran, not that a button was clicked.
24
+ */
25
+
26
+ import { deriveCeoCockpit } from "./ceo-cockpit-console.js";
27
+ import { deriveHelperWidgetCausationState } from "./workspace-swarm-proposal.js";
28
+
29
+ // Well-known anchors — the same constants the swarm/helper lanes use. Kept
30
+ // local (they are module-private upstream) so this stays a pure leaf module.
31
+ export const WORKSPACE_HELPER_SANDBOX_OBJECT_ID = "workspace-helper-sandbox";
32
+ export const WORKSPACE_HELPER_ROW_NAME = "workspace-helper";
33
+ export const CEO_BOOTSTRAP_COMPLETE_PROPOSAL_TYPE = "ceo.bootstrap.complete";
34
+ export const CEO_BOOTSTRAP_MARKER_FIELD = "ceoBootstrapCompletedAt";
35
+ export const CEO_BOOTSTRAP_BY_FIELD = "ceoBootstrapCompletedBy";
36
+ export const CEO_BOOTSTRAP_RECEIPT_FIELD = "ceoBootstrapReceiptId";
37
+
38
+ function clean(value) {
39
+ return typeof value === "string" ? value.trim() : "";
40
+ }
41
+
42
+ // Locate the well-known helper row (and its object) for marker read/write.
43
+ function findHelperRowLocation(workspaceConfig) {
44
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
45
+ ? workspaceConfig.dataModel.objects
46
+ : [];
47
+ const objectIndex = objects.findIndex((o) => o?.id === WORKSPACE_HELPER_SANDBOX_OBJECT_ID);
48
+ if (objectIndex < 0) return { objectIndex: -1, rowIndex: -1, object: null, row: null };
49
+ const object = objects[objectIndex];
50
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
51
+ let rowIndex = rows.findIndex((r) => clean(r?.Name) === WORKSPACE_HELPER_ROW_NAME);
52
+ if (rowIndex < 0 && rows.length > 0) rowIndex = 0;
53
+ return { objectIndex, rowIndex, object, row: rowIndex >= 0 ? rows[rowIndex] : null };
54
+ }
55
+
56
+ // Read the completion marker from config (the single source of mode truth).
57
+ function readCompletion(workspaceConfig) {
58
+ const { object, row } = findHelperRowLocation(workspaceConfig);
59
+ const at = clean(row?.[CEO_BOOTSTRAP_MARKER_FIELD]);
60
+ if (!row || !at) return null;
61
+ return {
62
+ objectId: object?.id || WORKSPACE_HELPER_SANDBOX_OBJECT_ID,
63
+ rowName: clean(row?.Name) || WORKSPACE_HELPER_ROW_NAME,
64
+ completedAt: at,
65
+ completedBy: clean(row?.[CEO_BOOTSTRAP_BY_FIELD]) || null,
66
+ receiptId: clean(row?.[CEO_BOOTSTRAP_RECEIPT_FIELD]) || null,
67
+ };
68
+ }
69
+
70
+ // A receipt counts as governance evidence when it is an execution proof or a
71
+ // governed apply that succeeded — the same lane vocabulary the cockpit uses.
72
+ function hasGovernanceEvidence(receipts) {
73
+ return (Array.isArray(receipts) ? receipts : []).some((r) => {
74
+ if (!r || typeof r !== "object") return false;
75
+ if (r.outcomeStatus === "blocked") return false;
76
+ return r.lane === "execution-proof" || r.lane === "governed-proposal";
77
+ });
78
+ }
79
+
80
+ function item(id, label, status, { guidance = "", evidenceRefs = [], nextAction = null } = {}) {
81
+ return { id, label, status, guidance, evidenceRefs, nextAction };
82
+ }
83
+
84
+ // Choose the workflow the checklist follows end-to-end: prefer one that
85
+ // completed, else one that ran, else one that is ready, else the first.
86
+ function pickFocus(reports) {
87
+ return (
88
+ reports.find((r) => r.state === "completed") ||
89
+ reports.find((r) => r.lastRun) ||
90
+ reports.find((r) => r.readiness?.ready) ||
91
+ reports[0] ||
92
+ null
93
+ );
94
+ }
95
+
96
+ function openArtifactAction(report, label) {
97
+ if (!report?.nextAction?.artifact) return null;
98
+ return { kind: "open", label, artifact: report.nextAction.artifact };
99
+ }
100
+
101
+ /**
102
+ * Derive the CEO bootstrap state from workspace config (+ optional receipts).
103
+ * Pure. See module header for the return shape.
104
+ */
105
+ export function deriveCeoBootstrapState({ workspaceConfig, receipts = [] } = {}) {
106
+ const completionRef = readCompletion(workspaceConfig);
107
+ const cockpit = deriveCeoCockpit({ workspaceConfig, receipts });
108
+ const reports = cockpit.reports;
109
+ const helperState = deriveHelperWidgetCausationState(workspaceConfig);
110
+ const helperReady = helperState.ready === true;
111
+ const focus = pickFocus(reports);
112
+ const ran = Boolean(focus?.lastRun);
113
+ const completedRun = focus?.lastRun?.status === "completed";
114
+ const failedRun = focus?.lastRun?.status === "failed";
115
+ const governanceEvidence = hasGovernanceEvidence(receipts) || ran;
116
+
117
+ const checklist = [];
118
+
119
+ // 1 — Mental model (informational primer; satisfied by reading the loop).
120
+ checklist.push(item("mental-model", "Understand the CEO loop", "complete", {
121
+ guidance: "You're the CEO: orchestrate a swarm, validate it, launch it, observe truthful telemetry, then review the outcome.",
122
+ }));
123
+
124
+ // 2 — Governed swarm workflow exists (helper must be live to create one).
125
+ if (reports.length > 0) {
126
+ checklist.push(item("swarm-workflow", "Create your first agent swarm", "complete", {
127
+ guidance: "A governed swarm workflow is present in the fleet.",
128
+ evidenceRefs: focus?.name ? [focus.name] : [],
129
+ }));
130
+ } else if (helperReady) {
131
+ checklist.push(item("swarm-workflow", "Create your first agent swarm", "ready", {
132
+ guidance: "Propose your first agent swarm with /swarm — review and apply it to create the governed workflow.",
133
+ nextAction: { kind: "seed-swarm", label: "Propose a swarm" },
134
+ }));
135
+ } else {
136
+ checklist.push(item("swarm-workflow", "Create your first agent swarm", "blocked", {
137
+ guidance: `Bring the workspace helper online first. ${helperState.guidance}`,
138
+ nextAction: { kind: "setup", label: "Open Setup" },
139
+ }));
140
+ }
141
+
142
+ // 3 — Execution readiness (the existing eligibility gate).
143
+ if (!focus) {
144
+ checklist.push(item("readiness", "Validate execution readiness", "pending", {
145
+ guidance: "Create a swarm workflow first.",
146
+ }));
147
+ } else if (focus.readiness.ready) {
148
+ checklist.push(item("readiness", "Validate execution readiness", "complete", {
149
+ guidance: `Execution target ready (${focus.readiness.adapter}${focus.readiness.agentHost ? ` · ${focus.readiness.agentHost}` : ""}).`,
150
+ evidenceRefs: [focus.readiness.adapter].filter(Boolean),
151
+ }));
152
+ } else {
153
+ checklist.push(item("readiness", "Validate execution readiness", "blocked", {
154
+ guidance: focus.readiness.guidance,
155
+ nextAction: openArtifactAction(focus, "Open to fix"),
156
+ }));
157
+ }
158
+
159
+ // 4 — Launch through the existing Background Tasks (sandbox-run).
160
+ if (!focus) {
161
+ checklist.push(item("launch", "Launch through Background Tasks", "pending", {
162
+ guidance: "Create a swarm workflow first.",
163
+ }));
164
+ } else if (ran) {
165
+ checklist.push(item("launch", "Launch through Background Tasks", "complete", {
166
+ guidance: "Launched at least once via the existing sandbox-run executor.",
167
+ evidenceRefs: focus.lastRun.runId ? [focus.lastRun.runId] : [],
168
+ }));
169
+ } else if (focus.readiness.ready) {
170
+ checklist.push(item("launch", "Launch through Background Tasks", "ready", {
171
+ guidance: "Open the workflow and press Run — execution stays in Background Tasks.",
172
+ nextAction: openArtifactAction(focus, "Open to launch"),
173
+ }));
174
+ } else {
175
+ checklist.push(item("launch", "Launch through Background Tasks", "pending", {
176
+ guidance: "Resolve execution readiness first.",
177
+ }));
178
+ }
179
+
180
+ // 5 — Observe truthful telemetry (a persisted run record exists).
181
+ if (ran) {
182
+ checklist.push(item("observe", "Observe truthful telemetry", "complete", {
183
+ guidance: "A run record is persisted — tokens/tools show truthfully (— when the adapter reported nothing).",
184
+ evidenceRefs: focus.lastRun.runId ? [focus.lastRun.runId] : [],
185
+ }));
186
+ } else {
187
+ checklist.push(item("observe", "Observe truthful telemetry", "pending", {
188
+ guidance: "Launch the swarm to produce a run record.",
189
+ }));
190
+ }
191
+
192
+ // 6 — Review the outcome (completed vs failed — failure is not success).
193
+ if (!ran) {
194
+ checklist.push(item("review", "Review the outcome", "pending", {
195
+ guidance: "Launch the swarm first.",
196
+ }));
197
+ } else if (completedRun) {
198
+ checklist.push(item("review", "Review the outcome", "complete", {
199
+ guidance: "The run completed successfully.",
200
+ }));
201
+ } else if (failedRun) {
202
+ checklist.push(item("review", "Review the outcome", "blocked", {
203
+ guidance: "Last run failed — review the transcript and re-run before completing setup.",
204
+ nextAction: openArtifactAction(focus, "Open to review"),
205
+ }));
206
+ } else {
207
+ checklist.push(item("review", "Review the outcome", "pending", {
208
+ guidance: "The run is still in progress — open it to watch.",
209
+ }));
210
+ }
211
+
212
+ // 7 — Confirm governance receipts exist.
213
+ checklist.push(item("governance", "Confirm governance receipts", governanceEvidence ? "complete" : "pending", {
214
+ guidance: governanceEvidence
215
+ ? "Governed activity is recorded in the agent-outcomes receipt stream."
216
+ : "Receipts appear once you launch a run or apply a governed proposal.",
217
+ }));
218
+
219
+ // 8 — Mark CEO setup complete (the only mutation; goes through helper/apply).
220
+ const prereqIds = ["swarm-workflow", "readiness", "launch", "review"];
221
+ const prereqsMet = prereqIds.every(
222
+ (id) => checklist.find((c) => c.id === id)?.status === "complete"
223
+ );
224
+ if (completionRef) {
225
+ checklist.push(item("complete", "Mark CEO setup complete", "complete", {
226
+ guidance: "CEO setup is complete for this workspace.",
227
+ evidenceRefs: [completionRef.completedAt],
228
+ }));
229
+ } else if (prereqsMet) {
230
+ checklist.push(item("complete", "Mark CEO setup complete", "ready", {
231
+ guidance: "You've proven the full CEO loop — lock it in. This checklist then disappears for this workspace.",
232
+ nextAction: { kind: "mark-complete", label: "Complete" },
233
+ }));
234
+ } else {
235
+ checklist.push(item("complete", "Mark CEO setup complete", "pending", {
236
+ guidance: "Finish the steps above to unlock completion.",
237
+ }));
238
+ }
239
+
240
+ // The single next move — first actionable (ready/blocked) item in order.
241
+ const primaryItem = checklist.find(
242
+ (c) => (c.status === "ready" || c.status === "blocked") && c.nextAction
243
+ );
244
+ const primaryAction = primaryItem
245
+ ? { itemId: primaryItem.id, ...primaryItem.nextAction }
246
+ : null;
247
+
248
+ const completedCount = checklist.filter((c) => c.status === "complete").length;
249
+
250
+ return {
251
+ title: "CEO Cockpit",
252
+ mode: completionRef ? "operational" : "bootstrap",
253
+ completed: Boolean(completionRef),
254
+ completionRef,
255
+ checklist,
256
+ primaryAction,
257
+ progress: { completed: completedCount, total: checklist.length },
258
+ focus: focus ? { objectId: focus.objectId, name: focus.name } : null,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Governed completion builder — stamps the CEO bootstrap marker onto the
264
+ * well-known helper row in `dataModel`. Pure (returns a new config; writes
265
+ * nothing). Used by the helper/apply `ceo.bootstrap.complete` lane.
266
+ *
267
+ * Refuses to stamp unless the bootstrap prerequisites are config-provably met
268
+ * (a ready swarm with a completed run) — so the marker is real evidence, not a
269
+ * client assertion.
270
+ *
271
+ * @returns {{ ok: boolean, config: object, error?: string }}
272
+ */
273
+ export function buildCeoBootstrapCompletion({
274
+ workspaceConfig,
275
+ completedAt,
276
+ completedBy = "user",
277
+ receiptId = null,
278
+ } = {}) {
279
+ const existing = readCompletion(workspaceConfig);
280
+ if (existing) {
281
+ // Idempotent — already complete, nothing to change.
282
+ return { ok: true, config: workspaceConfig };
283
+ }
284
+
285
+ const state = deriveCeoBootstrapState({ workspaceConfig });
286
+ const completeItem = state.checklist.find((c) => c.id === "complete");
287
+ if (!completeItem || completeItem.status !== "ready") {
288
+ const blocking = state.checklist
289
+ .filter((c) => ["swarm-workflow", "readiness", "launch", "review"].includes(c.id))
290
+ .filter((c) => c.status !== "complete")
291
+ .map((c) => c.label);
292
+ return {
293
+ ok: false,
294
+ config: workspaceConfig,
295
+ error: `CEO bootstrap prerequisites not met: ${blocking.join("; ") || "incomplete"}`,
296
+ };
297
+ }
298
+
299
+ const { objectIndex, rowIndex } = findHelperRowLocation(workspaceConfig);
300
+ if (objectIndex < 0 || rowIndex < 0) {
301
+ return { ok: false, config: workspaceConfig, error: "workspace helper sandbox row not found" };
302
+ }
303
+
304
+ const stampedAt = clean(completedAt) || new Date().toISOString();
305
+ const objects = workspaceConfig.dataModel.objects.map((object, oi) => {
306
+ if (oi !== objectIndex) return object;
307
+ const rows = object.rows.map((row, ri) => {
308
+ if (ri !== rowIndex) return row;
309
+ return {
310
+ ...row,
311
+ [CEO_BOOTSTRAP_MARKER_FIELD]: stampedAt,
312
+ [CEO_BOOTSTRAP_BY_FIELD]: clean(completedBy) || "user",
313
+ ...(clean(receiptId) ? { [CEO_BOOTSTRAP_RECEIPT_FIELD]: clean(receiptId) } : {}),
314
+ };
315
+ });
316
+ return { ...object, rows };
317
+ });
318
+
319
+ return {
320
+ ok: true,
321
+ config: { ...workspaceConfig, dataModel: { ...workspaceConfig.dataModel, objects } },
322
+ };
323
+ }
324
+
325
+ export default deriveCeoBootstrapState;