@synap-core/workspace-templates 0.6.0 → 0.6.2

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.
package/src/define.ts CHANGED
@@ -10,9 +10,107 @@
10
10
  * shape never drifts per call-site.
11
11
  */
12
12
 
13
- import type { WorkspaceYaml } from "./types.js";
13
+ import type { TemplateBentoBlock, WorkspaceYaml } from "./types.js";
14
14
  import { getWorkspaceTemplate } from "./templates.js";
15
15
 
16
+ /** A widget block as the wire expects it: `bentoLayout[]`, keyed by `widgetType`. */
17
+ export interface BentoWidgetBlockWire {
18
+ widgetType: string;
19
+ pos: TemplateBentoBlock['pos'];
20
+ config?: Record<string, unknown>;
21
+ }
22
+
23
+ /** A view block as the wire expects it: `bentoViewBlocks[]`, keyed by `viewName`. */
24
+ export interface BentoViewBlockWire {
25
+ kind: 'view';
26
+ viewName: string;
27
+ pos: TemplateBentoBlock['pos'];
28
+ overrides?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface SplitBentoBlocks {
32
+ bentoLayout: BentoWidgetBlockWire[];
33
+ bentoViewBlocks: BentoViewBlockWire[];
34
+ }
35
+
36
+ /**
37
+ * Split the authored `bento.blocks` DISCRIMINATED UNION into the TWO wire
38
+ * fields the pod expects. This is the whole point: `TemplateBentoBlock` is one
39
+ * array discriminated by `kind`, but the pod does NOT read `kind` — it splits by
40
+ * FIELD (`create-workspace-from-definition.ts`):
41
+ *
42
+ * - `bentoLayout` → mapped as widgets, reads `widget.widgetType`
43
+ * - `bentoViewBlocks` → filtered against `viewMap[vb.viewSlug ?? vb.viewName]`,
44
+ * unknown entries DROPPED with a logger.warn
45
+ *
46
+ * Dumping the whole union into either single field silently mis-routes the other
47
+ * half — which is exactly what both converters used to do:
48
+ * - `toWorkspaceDefinition` sent all blocks as `bentoViewBlocks`, so widgets
49
+ * (no `viewName`) hit `viewMap[undefined]` and were filtered out.
50
+ * - `toPackageDefinition` sent all blocks as `bentoLayout`, so view blocks
51
+ * (no `widgetType`) became widgets with `widgetType: undefined`.
52
+ * Net: nobody had ever seen an authored bento render as authored.
53
+ *
54
+ * Blocks are emitted in the EXACT shape each sink declares (the tRPC
55
+ * `createFromDefinition` zod is strict: `bentoLayout` requires `widgetType`,
56
+ * `bentoViewBlocks` requires `kind: 'view'` + `viewName`), so a widget's
57
+ * `overrides` and a view's `config` are not smuggled into the wrong sink.
58
+ * Authored order is preserved WITHIN each kind; the pod concatenates
59
+ * `[...widgetBlocks, ...viewBlocks]`, so cross-kind order is the pod's to define.
60
+ *
61
+ * `kind` is INFERRED from field presence when absent. `TemplateBentoBlock.kind`
62
+ * is a required field in TypeScript, but the generated `templates.ts` casts the
63
+ * parsed YAML `as unknown as Record<string, WorkspaceYaml>` — so the compiler
64
+ * never checks the authored data against the type, and `agent-fleet.yaml`
65
+ * authored 8 widget blocks with no `kind:` at all (bare `widgetType`). Routing
66
+ * strictly on `kind` would silently DROP those — the exact failure mode this
67
+ * function exists to end. Inferring by field mirrors what the pod itself does
68
+ * (it splits by field and never reads `kind`), so the seam is total: a block
69
+ * that CAN be materialized always is. `scripts/check-converters.mjs` is the
70
+ * strict half — it fails the build if an authored block omits `kind` — so this
71
+ * leniency is a safety net, not a place for drift to hide.
72
+ */
73
+ function resolveBlockKind(block: TemplateBentoBlock): 'widget' | 'view' | null {
74
+ if (block.kind === 'widget' || block.kind === 'view') return block.kind;
75
+ if (block.widgetType) return 'widget';
76
+ if (block.viewName) return 'view';
77
+ return null;
78
+ }
79
+
80
+ export function splitBentoBlocks(
81
+ blocks: TemplateBentoBlock[] | undefined
82
+ ): SplitBentoBlocks {
83
+ const bentoLayout: BentoWidgetBlockWire[] = [];
84
+ const bentoViewBlocks: BentoViewBlockWire[] = [];
85
+
86
+ for (const block of blocks ?? []) {
87
+ const kind = resolveBlockKind(block);
88
+ if (kind === 'widget') {
89
+ // A widget block without `widgetType` cannot be rendered — the pod would
90
+ // create a `widgetType: undefined` block. Drop it loudly rather than ship
91
+ // a broken block; the build-time guard asserts this never happens in the
92
+ // authored templates.
93
+ if (!block.widgetType) continue;
94
+ bentoLayout.push({
95
+ widgetType: block.widgetType,
96
+ pos: block.pos,
97
+ ...(block.config ? { config: block.config } : {}),
98
+ });
99
+ } else if (kind === 'view') {
100
+ // Same reasoning: the pod filters unknown/absent view names anyway.
101
+ if (!block.viewName) continue;
102
+ bentoViewBlocks.push({
103
+ kind: 'view',
104
+ viewName: block.viewName,
105
+ pos: block.pos,
106
+ ...(block.overrides ? { overrides: block.overrides } : {}),
107
+ });
108
+ }
109
+ }
110
+
111
+ return { bentoLayout, bentoViewBlocks };
112
+ }
113
+
16
114
  /** The flat definition object accepted by createFromDefinition. */
17
115
  export interface WorkspaceDefinitionInput {
18
116
  workspaceName: string;
@@ -26,7 +124,16 @@ export interface WorkspaceDefinitionInput {
26
124
  displayTemplates?: WorkspaceYaml['displayTemplates'];
27
125
  layoutConfig: WorkspaceYaml['layout'] | undefined;
28
126
  bentoViewName?: string;
29
- bentoViewBlocks?: NonNullable<WorkspaceYaml['bento']>['blocks'];
127
+ /**
128
+ * WIDGET half of the authored bento union. The door declares this field
129
+ * (`workspaces.createFromDefinition` zod) and the pod maps it as widgets via
130
+ * `widget.widgetType`. It was missing from this interface entirely, which is
131
+ * why `toWorkspaceDefinition` had nowhere to put widgets and dumped the whole
132
+ * union into `bentoViewBlocks` — where every widget was silently filtered out.
133
+ */
134
+ bentoLayout?: BentoWidgetBlockWire[];
135
+ /** VIEW half of the authored bento union — resolved against the viewMap. */
136
+ bentoViewBlocks?: BentoViewBlockWire[];
30
137
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
31
138
  // Workspace-level metadata (flattened from the `workspace` block)
32
139
  workspaceSubtype?: string;
@@ -77,6 +184,10 @@ export function toWorkspaceDefinition(
77
184
  const name = nameOverride ?? ws.name;
78
185
  const proposalId = ws.proposalId ?? `${tpl.meta.slug}-v1`;
79
186
 
187
+ // The authored `bento.blocks` union must be ROUTED, not forwarded: the pod
188
+ // splits by field, not by `kind`. See `splitBentoBlocks`.
189
+ const bento = splitBentoBlocks(tpl.bento?.blocks);
190
+
80
191
  const definition: WorkspaceDefinitionInput = {
81
192
  workspaceName: name,
82
193
  description: ws.description,
@@ -89,7 +200,8 @@ export function toWorkspaceDefinition(
89
200
  displayTemplates: tpl.displayTemplates,
90
201
  layoutConfig: tpl.layout,
91
202
  bentoViewName: tpl.bento?.viewName,
92
- bentoViewBlocks: tpl.bento?.blocks,
203
+ bentoLayout: bento.bentoLayout,
204
+ bentoViewBlocks: bento.bentoViewBlocks,
93
205
  profileEntityBentoTemplates: tpl.profileEntityBentoTemplates,
94
206
  workspaceSubtype: ws.subtype,
95
207
  workspaceType: ws.workspaceType,
@@ -114,6 +226,90 @@ export function toWorkspaceDefinition(
114
226
  return { definition, proposalId, workspaceName: name, templateId: tpl.meta.slug };
115
227
  }
116
228
 
229
+ // ─── Exhaustiveness ledger: toWorkspaceDefinition ────────────────────────────
230
+ // `WorkspaceYaml` is read by two converters and nothing forced either to be
231
+ // total — which is how `dependencies` was silently dropped here for months.
232
+ // Every key of `WorkspaceYaml` must be classified below as either FORWARDED
233
+ // onto the wire or explicitly DROPPED with a reason. Adding a field to
234
+ // `WorkspaceYaml` FAILS THE BUILD until it is classified, forcing the author
235
+ // to make the forward-or-drop decision instead of defaulting to silent loss.
236
+ //
237
+ // ⚠️ KNOW WHAT THIS LEDGER DOES NOT CATCH. It is FIELD-PRESENCE-exhaustive, not
238
+ // SEMANTICS-exhaustive. It asks "is every key classified?" — not "does the value
239
+ // reach the right sink?". `bento` was classified as forwarded, and it WAS
240
+ // forwarded: the whole discriminated union was dumped into ONE field on each
241
+ // door (`bentoViewBlocks` here, `bentoLayout` in `toPackageDefinition`). The
242
+ // ledger passed while widgets were filtered out on one door and views became
243
+ // `widgetType: undefined` widgets on the other. A ledger entry is a CLAIM about
244
+ // routing; only a runtime assertion can check the claim.
245
+ // → The counterpart guard for that class is `scripts/check-converters.mjs`,
246
+ // which RUNS both converters over all templates and asserts the per-kind
247
+ // block counts survive the split. Any future union-shaped field (one authored
248
+ // array fanning out to N wire fields) needs an assertion there too — do not
249
+ // let a ledger comment stand in for one.
250
+
251
+ /** Keys `toWorkspaceDefinition` reads and carries onto the wire. */
252
+ type WorkspaceDefinitionForwarded =
253
+ | 'meta' // → proposalId (meta.slug) + templateId; not a wire field itself
254
+ | 'workspace' // flattened into workspaceName/description/workspaceType/…
255
+ | 'profiles'
256
+ | 'views'
257
+ | 'entityLinks'
258
+ | 'seedEntities'
259
+ | 'suggestedRelations'
260
+ | 'displayTemplates'
261
+ | 'layout' // → layoutConfig
262
+ | 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
263
+ | 'profileEntityBentoTemplates'
264
+ | 'onboarding'
265
+ | 'dependencies';
266
+
267
+ /**
268
+ * Keys deliberately NOT forwarded by `toWorkspaceDefinition`. Each needs a
269
+ * one-line WHY. "The door has no such field" = forwarding is a no-op; anything
270
+ * else is a real gap and should say so.
271
+ */
272
+ type WorkspaceDefinitionDropped =
273
+ // UI-only: `workspaces.createFromDefinition` has no `app` field. Consumers
274
+ // read `template.app` straight off the YAML to pick the first screen, so this
275
+ // never belonged on the wire.
276
+ | 'app'
277
+ // The door DOES accept `extends`, but no template declares it today (0/21).
278
+ // Forwarding is a no-op until one does — wire it then.
279
+ | 'extends'
280
+ // The door DOES accept `automations`, but no template declares it today
281
+ // (0/21), and its zod shape ({trigger,action:{playbook_run}}) is not
282
+ // `TemplateAutomation`. Needs a mapper if a template ever uses it.
283
+ // The browser install path installs automations via its own createAutomation door.
284
+ | 'automations'
285
+ // The door has no `integrations` field. Its `tools` field is a DIFFERENT
286
+ // shape ({name,kind,inputSchema} vs {templateKey,params,mode}). Capability
287
+ // templates are seeded via the packages/apply path, not createFromDefinition
288
+ // — see the note on `TemplateIntegration` in types.ts. Forwarding = no-op.
289
+ | 'integrations'
290
+ // REAL GAP (agent-fleet, project-management declare playbooks). The door
291
+ // accepts `playbooks`, but the shapes disagree: template param `type` allows
292
+ // 'text'|'entity'|'choice', the tRPC enum accepts only string|number|boolean
293
+ // (and defaultValue↔default, grants: string[]↔{kind,ref}[]). Raw forwarding =
294
+ // zod BAD_REQUEST. Needs a param mapper — separate wave, not a silent drop.
295
+ | 'playbooks';
296
+
297
+ type UnclassifiedWorkspaceDefinitionKey = Exclude<
298
+ keyof WorkspaceYaml,
299
+ WorkspaceDefinitionForwarded | WorkspaceDefinitionDropped
300
+ >;
301
+
302
+ // If this errors, a new `WorkspaceYaml` field is unclassified. Forward it in
303
+ // `toWorkspaceDefinition` and add it to `WorkspaceDefinitionForwarded`, or add
304
+ // it to `WorkspaceDefinitionDropped` with a one-line WHY.
305
+ const _workspaceDefinitionLedger: [UnclassifiedWorkspaceDefinitionKey] extends [never]
306
+ ? true
307
+ : [
308
+ 'UNCLASSIFIED YAML FIELD — forward it or add it to WorkspaceDefinitionDropped:',
309
+ UnclassifiedWorkspaceDefinitionKey,
310
+ ] = true;
311
+ void _workspaceDefinitionLedger;
312
+
117
313
  /**
118
314
  * The unified `PackageDefinition` shape consumed by `POST /api/hub/packages/apply`
119
315
  * (CLI `synap launch`) and the CP package registry seeder. It composes the
@@ -145,7 +341,17 @@ export interface PackageDefinitionOutput {
145
341
  suggestedRelations?: WorkspaceYaml['suggestedRelations'];
146
342
  displayTemplates?: WorkspaceYaml['displayTemplates'];
147
343
  entityLinks?: WorkspaceYaml['entityLinks'];
148
- bentoLayout?: NonNullable<WorkspaceYaml['bento']>['blocks'];
344
+ /** WIDGET half of the authored bento union — mapped by the pod via `widgetType`. */
345
+ bentoLayout?: BentoWidgetBlockWire[];
346
+ /**
347
+ * VIEW half of the authored bento union. Widgets go to `bentoLayout`, views
348
+ * here — the pod splits by field (create-workspace-from-definition.ts:1373
349
+ * reads `bentoViewBlocks`), so both wire fields must be carried. The apply
350
+ * door's `PackageApplySchema` now declares this field (it is a plain
351
+ * `z.object`, so an undeclared field would be silently stripped) — the inbound
352
+ * seam is closed to match this outbound emit.
353
+ */
354
+ bentoViewBlocks?: BentoViewBlockWire[];
149
355
  bentoViewName?: string;
150
356
  profileEntityBentoTemplates?: WorkspaceYaml['profileEntityBentoTemplates'];
151
357
  layoutConfig?: WorkspaceYaml['layout'];
@@ -173,6 +379,9 @@ export function toPackageDefinition(
173
379
  }
174
380
 
175
381
  const ws = tpl.workspace;
382
+ // Same routing as the tRPC door — the union is split by `kind`, never dumped
383
+ // whole into one field. See `splitBentoBlocks`.
384
+ const bento = splitBentoBlocks(tpl.bento?.blocks);
176
385
  return {
177
386
  _meta: {
178
387
  slug: tpl.meta.slug,
@@ -195,7 +404,8 @@ export function toPackageDefinition(
195
404
  suggestedRelations: tpl.suggestedRelations,
196
405
  displayTemplates: tpl.displayTemplates,
197
406
  entityLinks: tpl.entityLinks,
198
- bentoLayout: tpl.bento?.blocks,
407
+ bentoLayout: bento.bentoLayout,
408
+ bentoViewBlocks: bento.bentoViewBlocks,
199
409
  bentoViewName: tpl.bento?.viewName,
200
410
  profileEntityBentoTemplates: tpl.profileEntityBentoTemplates,
201
411
  layoutConfig: tpl.layout,
@@ -205,3 +415,56 @@ export function toPackageDefinition(
205
415
  dependencies: tpl.dependencies,
206
416
  };
207
417
  }
418
+
419
+ // ─── Exhaustiveness ledger: toPackageDefinition ──────────────────────────────
420
+ // Same forcing function as above, for the OTHER converter. These two are NOT
421
+ // merged on purpose: they target different endpoints with different contracts
422
+ // (`PackageDefinition` is a superset with renames — seedEntities→suggestedEntities,
423
+ // bento.blocks→bentoLayout, integrations→capabilities, plus `_meta`). Keeping
424
+ // two converters is fine; keeping either one lossy-by-accident is not.
425
+
426
+ /** Keys `toPackageDefinition` reads and carries onto the wire. */
427
+ type PackageDefinitionForwarded =
428
+ | 'meta' // → _meta
429
+ | 'workspace' // flattened into workspaceName/description/workspaceType/…
430
+ | 'profiles'
431
+ | 'views'
432
+ | 'seedEntities' // → suggestedEntities
433
+ | 'suggestedRelations'
434
+ | 'displayTemplates'
435
+ | 'entityLinks'
436
+ | 'bento' // → bentoViewName + bentoLayout (widgets) + bentoViewBlocks (views)
437
+ | 'profileEntityBentoTemplates'
438
+ | 'layout' // → layoutConfig
439
+ | 'integrations' // → capabilities
440
+ | 'playbooks'
441
+ | 'onboarding'
442
+ | 'dependencies';
443
+
444
+ /** Keys deliberately NOT forwarded by `toPackageDefinition`. */
445
+ type PackageDefinitionDropped =
446
+ // UI-only, same as the definition door: `PackageDefinition` has no `app`.
447
+ | 'app'
448
+ // REAL GAP: backend `PackageDefinition` DOES declare `extends`
449
+ // (package-definition.ts:262) and `automations` (:268), but
450
+ // `PackageDefinitionOutput` omits both. No template declares either today
451
+ // (0/21), so this is latent, not live — wire them when a template first uses
452
+ // one.
453
+ | 'extends'
454
+ | 'automations';
455
+
456
+ type UnclassifiedPackageDefinitionKey = Exclude<
457
+ keyof WorkspaceYaml,
458
+ PackageDefinitionForwarded | PackageDefinitionDropped
459
+ >;
460
+
461
+ // If this errors, a new `WorkspaceYaml` field is unclassified. Forward it in
462
+ // `toPackageDefinition` and add it to `PackageDefinitionForwarded`, or add it
463
+ // to `PackageDefinitionDropped` with a one-line WHY.
464
+ const _packageDefinitionLedger: [UnclassifiedPackageDefinitionKey] extends [never]
465
+ ? true
466
+ : [
467
+ 'UNCLASSIFIED YAML FIELD — forward it or add it to PackageDefinitionDropped:',
468
+ UnclassifiedPackageDefinitionKey,
469
+ ] = true;
470
+ void _packageDefinitionLedger;
@@ -368,6 +368,35 @@ entityLinks:
368
368
  type: involves
369
369
  label: Involves
370
370
 
371
+ # Playbooks — session templates seeded with the workspace.
372
+ # Ported from the orphaned `deep-research.playbook.json` (CP templates/packs):
373
+ # same goalTemplate + companyContext param + is-agent executor + Web-Search /
374
+ # Entity-Creator grants + typed outputs (encoded via the {outputTypes} param).
375
+ # `subjectProfile` binds it to the shared `competitor` role so the Capture →
376
+ # Session matcher can offer it when a competitor is captured; the captured
377
+ # competitor is passed as the `competitor` entity param.
378
+ playbooks:
379
+ - name: Research Competitor
380
+ description: Deep-research a competitor and position it against us — strengths, weaknesses, pricing, and the gaps we can exploit.
381
+ goalTemplate: >-
382
+ Research this competitor ({competitor}) and compare them to us. Investigate
383
+ their {focus}: positioning, strengths, weaknesses, pricing, and recent
384
+ moves. Use the provided company context where relevant: {companyContext}.
385
+ Produce: {outputTypes}. Update the competitor entity with what you find and
386
+ make clear where we win and where we are exposed.
387
+ subjectProfile:
388
+ profileSlug: competitor
389
+ params:
390
+ - { name: competitor, type: entity, label: Competitor, required: true }
391
+ - { name: focus, type: text, label: Focus area, defaultValue: "product, pricing, and positioning" }
392
+ - { name: companyContext, type: text, label: Our company context }
393
+ - { name: outputTypes, type: text, label: Outputs, defaultValue: "a head-to-head comparison summary and an updated competitor profile" }
394
+ executor: is-agent
395
+ grants:
396
+ - Web Search
397
+ - Entity Creator
398
+ status: active
399
+
371
400
  onboarding:
372
401
  goal: >
373
402
  Map the real ecosystem — the key players (companies and people), who partners
@@ -0,0 +1,188 @@
1
+ # Enterprise OS — the public operating system suite
2
+ # ==================================================
3
+ # A SUITE template: it stands up a whole company's public operating core in one
4
+ # install by REQUIRING its constituent workspaces (deps-first), then adds a thin
5
+ # "command tower" home of its own — a place to track the company's objectives
6
+ # across the lenses it just installed.
7
+ #
8
+ # What it composes (all public, all bundled):
9
+ # • foundation — strategic DNA (mission, audience, positioning, principles)
10
+ # • ecosystem — the market graph (partners, competitors, segments, trends)
11
+ # • operations — how the company runs (the operational lens)
12
+ # • crm — the pipeline (contacts, leads, clients, deals)
13
+ # • content-os — the editorial engine (pillars, posts, platforms, newsletters)
14
+ #
15
+ # It uses `require` (not `compose`): each constituent is a REAL workspace, not an
16
+ # overlay. Deps-first resolution installs foundation first (the SSOT role
17
+ # vocabulary provider), so every downstream lens reuses foundation's pod-wide
18
+ # roles rather than forking twins. Installing Enterprise OS a second time is
19
+ # idempotent — the resolver reuses existing constituent workspaces by slug.
20
+
21
+ meta:
22
+ slug: enterprise-os
23
+ name: Enterprise OS
24
+ description: Stand up a company's public operating core in one install — strategy, market, operations, and pipeline — plus a command tower to track objectives across them.
25
+ icon: layout-dashboard
26
+ color: "#4F46E5"
27
+ tags: [suite, enterprise, os, foundation, crm, operations, ecosystem]
28
+ isPublic: true
29
+
30
+ # Deps-first: foundation is the pod-wide role-vocabulary provider, so it must
31
+ # resolve before ecosystem/operations/crm reuse its shared roles. `require` (not
32
+ # `compose`): each is a standalone workspace, not an additive overlay.
33
+ dependencies:
34
+ - slug: foundation
35
+ kind: workspace
36
+ relation: require
37
+ reason: "Strategic DNA + the pod-wide role vocabulary (partner/client/lead) every other lens reuses. Must resolve first."
38
+ - slug: ecosystem
39
+ kind: workspace
40
+ relation: require
41
+ reason: "The market graph — the players, segments, and trends the company operates around."
42
+ - slug: operations
43
+ kind: workspace
44
+ relation: require
45
+ reason: "How the company runs — the operational lens."
46
+ - slug: crm
47
+ kind: workspace
48
+ relation: require
49
+ reason: "The pipeline — contacts, leads, clients, and deals."
50
+ - slug: content-os
51
+ kind: workspace
52
+ relation: require
53
+ reason: "The editorial engine — pillars, posts, platforms, and newsletters — so the company's content operation is part of the operating core."
54
+
55
+ workspace:
56
+ name: Enterprise OS
57
+ description: The command tower for the company operating system — track strategic objectives across the strategy, market, operations, and pipeline lenses this suite installs.
58
+ subtype: enterprise-os
59
+ visibility: pod_visible
60
+ sourceRoles:
61
+ strategy: consumer
62
+
63
+ profiles:
64
+ # The one native profile the command tower owns: a strategic objective the
65
+ # company is driving across the lenses this suite installs. Deliberately light
66
+ # — the depth lives in the constituent workspaces; this is the cross-cutting
67
+ # scoreboard that ties them together.
68
+ - slug: objective
69
+ displayName: Objective
70
+ icon: target
71
+ color: "#4F46E5"
72
+ description: A company objective the operating system is driving toward — the cross-cutting outcome the lenses serve.
73
+ properties:
74
+ - slug: outcome
75
+ label: Outcome
76
+ valueType: string
77
+ inputType: textarea
78
+ - slug: status
79
+ label: Status
80
+ valueType: string
81
+ inputType: select
82
+ enumValues: [on-track, at-risk, off-track, achieved]
83
+ constraints: { defaultValue: on-track }
84
+ - slug: horizon
85
+ label: Horizon
86
+ valueType: string
87
+ inputType: select
88
+ enumValues: [now, next, later]
89
+ constraints: { defaultValue: now }
90
+ - slug: owner
91
+ label: Owner
92
+ valueType: string
93
+ inputType: text
94
+ - slug: targetDate
95
+ label: Target Date
96
+ valueType: date
97
+ inputType: date
98
+
99
+ views:
100
+ - name: Objectives
101
+ slug: objectives
102
+ type: table
103
+ scopeProfileSlug: objective
104
+ description: The company's strategic objectives across every lens, by horizon and status.
105
+ defaultView: true
106
+ config:
107
+ columns: [title, status, horizon, owner, targetDate]
108
+ - name: Objective Board
109
+ slug: objective-board
110
+ type: kanban
111
+ scopeProfileSlug: objective
112
+ description: Objectives grouped by status — a running scoreboard for the operating system.
113
+ config:
114
+ groupBy: status
115
+
116
+ onboarding:
117
+ goal: >
118
+ Stand up the company's operating core and name the handful of strategic
119
+ objectives it is driving — so strategy, market, operations, and pipeline are
120
+ all in place and pointed at the same outcomes from day one.
121
+ framing: |
122
+ THE CHIEF OF STAFF: Act as a sharp chief of staff standing up the company OS.
123
+ The four lenses (foundation, ecosystem, operations, CRM) are already
124
+ installed — your job is to name the 3-5 objectives that cut across them, so
125
+ the operating system has a scoreboard, not just structure.
126
+ expertise:
127
+ starters:
128
+ - >
129
+ Name few, real objectives. An operating system with 20 objectives has
130
+ none; force the 3-5 that actually decide the year, each with an owner and
131
+ a horizon (now / next / later).
132
+ - >
133
+ Tie each objective to a lens. A good objective names where it lives —
134
+ pipeline growth (CRM), a market move (ecosystem), an operational fix
135
+ (operations), or a positioning shift (foundation) — so the scoreboard
136
+ maps to the structure underneath it.
137
+ blindSpots:
138
+ - >
139
+ Objectives with no owner and no date are wishes. Every one needs a single
140
+ owner and a target date, or it will not move.
141
+ - >
142
+ "Grow the business" is not an objective — it is a category. Push for the
143
+ specific outcome that would make this quarter a win.
144
+ bar: >
145
+ A short, honest scoreboard: 3-5 objectives, each owned, dated, placed on a
146
+ horizon, and tied to the lens that serves it — sitting on top of a fully
147
+ installed operating core.
148
+ collect:
149
+ - profileSlug: objective
150
+ what: The 3-5 strategic objectives the company is driving this horizon.
151
+ cardinality: few
152
+ keyFields: [status, horizon, owner]
153
+ openingQuestions:
154
+ - "What are the 3-5 objectives that will decide this year for the company?"
155
+ - "Who owns each one, and is it a now, next, or later horizon?"
156
+ doneWhen: A few objectives captured, each with an owner and a horizon.
157
+
158
+ app:
159
+ kind: stored
160
+ cellKey: entity-detail
161
+
162
+ layout:
163
+ defaultView: Objectives
164
+ defaultApp: null
165
+ pinnedApps: [dashboard, data]
166
+ sidebarItems:
167
+ - kind: view
168
+ viewName: Objectives
169
+ label: Objectives
170
+ icon: target
171
+ - kind: view
172
+ viewName: Objective Board
173
+ label: Board
174
+ icon: kanban-square
175
+ - kind: app
176
+ appId: data
177
+ label: Data
178
+ icon: database
179
+
180
+ bento:
181
+ viewName: Objectives
182
+ blocks:
183
+ - kind: view
184
+ viewName: Objectives
185
+ pos: { x: 0, y: 0, w: 8, h: 4 }
186
+ - kind: view
187
+ viewName: Objective Board
188
+ pos: { x: 8, y: 0, w: 4, h: 4 }
package/src/index.ts CHANGED
@@ -37,6 +37,7 @@ export type {
37
37
  TemplateAutomation,
38
38
  TemplateExtendsRef,
39
39
  TemplateDependency,
40
+ PackageDependencyKind,
40
41
  TemplateProfileEntityBentoTemplates,
41
42
  TemplateDefaultSource,
42
43
  TemplateSourceRole,
@@ -46,6 +47,12 @@ export type {
46
47
  TemplatePlaybookParam,
47
48
  } from "./types.js";
48
49
 
50
+ // The dependency-kind vocabulary as a VALUE — so consumers (marketplace,
51
+ // onboarding, backend, hub zod, browser, CP zod) can `z.enum(PACKAGE_DEPENDENCY_KINDS)`
52
+ // / derive their type from it instead of hand-copying the list. Six mirrors
53
+ // exist today; this is the door that lets them stop being mirrors.
54
+ export { PACKAGE_DEPENDENCY_KINDS } from "./types.js";
55
+
49
56
  export {
50
57
  WORKSPACE_TEMPLATES,
51
58
  getWorkspaceTemplate,
@@ -55,9 +62,28 @@ export {
55
62
 
56
63
  export type { WorkspaceTemplateSlug } from "./templates.js";
57
64
 
58
- export { toWorkspaceDefinition, toPackageDefinition } from "./define.js";
65
+ export { mergeCatalog } from "./catalog.js";
66
+ export type {
67
+ MergeCatalogInput,
68
+ MergeCatalogResult,
69
+ BundledEntry,
70
+ RemoteEntry,
71
+ RemoteSourceStatus,
72
+ CatalogEntry,
73
+ BundledCatalogEntry,
74
+ RemoteCatalogEntry,
75
+ } from "./catalog.js";
76
+
77
+ export {
78
+ toWorkspaceDefinition,
79
+ toPackageDefinition,
80
+ splitBentoBlocks,
81
+ } from "./define.js";
59
82
  export type {
60
83
  WorkspaceDefinitionInput,
61
84
  ToDefinitionResult,
62
85
  PackageDefinitionOutput,
86
+ SplitBentoBlocks,
87
+ BentoWidgetBlockWire,
88
+ BentoViewBlockWire,
63
89
  } from "./define.js";
@@ -146,16 +146,25 @@ profiles:
146
146
  # foundation already owns (e.g. `leadStage`) with a divergent enum/default —
147
147
  # that is base drift, and the SSOT tripwire fails on it.
148
148
  # (Standards reference inherited base props like `leadStage` directly.)
149
+ #
150
+ # The same reasoning applies to the ROW-LEVEL fields, and more strictly: the
151
+ # row has no overlay mechanism, so this template must not restate what
152
+ # foundation owns. `description`, `semanticSlug` and `applicableKinds` are
153
+ # therefore OMITTED here — they are written only on the create path, which
154
+ # foundation always takes, so declaring them here asserted a value that never
155
+ # applied (this template claimed `applicableKinds: [person]` while the live row
156
+ # says [company, person]).
157
+ # `icon`/`color` are the exception that must be RESTATED rather than dropped:
158
+ # they are also read into `profileHintsMap` to build THIS workspace's auto-bento
159
+ # for `lead`, so omitting them regresses the widgets to a default indigo
160
+ # database icon. They must MATCH foundation's row (`user-plus`/`yellow`) — the
161
+ # old `green` rendered green widgets around a yellow row.
149
162
  - slug: lead
150
163
  displayName: Lead
151
164
  icon: user-plus
152
- color: green
153
- description: "A potential customer or partner contact in your pipeline"
154
- semanticSlug: marketing.lead
165
+ color: yellow
155
166
  scope: shared
156
167
  profileKind: role
157
- applicableKinds:
158
- - person
159
168
  properties:
160
169
  - slug: lead-source
161
170
  label: Source