@synap-core/workspace-templates 0.7.2 → 0.7.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 (44) hide show
  1. package/dist/index.d.ts +207 -8
  2. package/dist/index.js +2148 -1480
  3. package/dist/index.js.map +1 -1
  4. package/package.json +1 -1
  5. package/src/agent-fleet.yaml +666 -108
  6. package/src/blockchain-ecosystem.yaml +1 -0
  7. package/src/brand-library.yaml +127 -12
  8. package/src/builder.yaml +116 -12
  9. package/src/business-developer.yaml +102 -18
  10. package/src/catalog.ts +13 -3
  11. package/src/communication.yaml +23 -453
  12. package/src/composition-graph.test.ts +26 -0
  13. package/src/content-os.yaml +103 -10
  14. package/src/content-studio.yaml +1 -0
  15. package/src/crm.yaml +1 -0
  16. package/src/define.ts +8 -8
  17. package/src/dev-dashboard.yaml +43 -9
  18. package/src/domains.test.ts +117 -0
  19. package/src/domains.ts +102 -0
  20. package/src/ecosystem.yaml +21 -4
  21. package/src/enterprise-os.yaml +1 -0
  22. package/src/finance.yaml +46 -5
  23. package/src/foundation.yaml +13 -1
  24. package/src/grants.yaml +1 -0
  25. package/src/hr.yaml +61 -8
  26. package/src/index.ts +29 -0
  27. package/src/internal-runbook.yaml +36 -5
  28. package/src/legal.yaml +97 -12
  29. package/src/life-os.yaml +188 -27
  30. package/src/marketing.yaml +15 -2
  31. package/src/meetings-knowledge.yaml +7 -1
  32. package/src/operations.yaml +347 -0
  33. package/src/outreach-comms.yaml +6 -1
  34. package/src/project-management.yaml +840 -111
  35. package/src/research-base.yaml +516 -0
  36. package/src/research-decision.yaml +158 -0
  37. package/src/shared-role-ssot.test.ts +268 -0
  38. package/src/social.yaml +105 -37
  39. package/src/templates.ts +4548 -4304
  40. package/src/the-arch.yaml +1 -0
  41. package/src/types.ts +9 -0
  42. package/src/validate.test.ts +81 -0
  43. package/src/validate.ts +561 -0
  44. package/src/personal.yaml +0 -177
@@ -8,6 +8,7 @@
8
8
 
9
9
  meta:
10
10
  slug: content-os
11
+ domain: content
11
12
  name: Content OS
12
13
  description: "Your editorial operating system — pillars, posts, platforms, and newsletters. Turn raw thinking into a published content engine, all connected to your campaigns and brand voice."
13
14
  icon: pen-tool
@@ -71,17 +72,51 @@ profiles:
71
72
  label: Status
72
73
  valueType: string
73
74
  inputType: select
74
- enumValues: ["Idea", "Briefed", "In Draft", "Review", "Scheduled", "Published", "Archived"]
75
+ enumValues:
76
+ [
77
+ "Idea",
78
+ "Briefed",
79
+ "In Draft",
80
+ "Review",
81
+ "Scheduled",
82
+ "Published",
83
+ "Archived",
84
+ ]
75
85
  - slug: post-format
76
86
  label: Format
77
87
  valueType: string
78
88
  inputType: select
79
- enumValues: ["Article", "Thread", "Short Video", "Long Video", "Carousel", "Image Post", "Story", "Newsletter Section", "Podcast Episode", "Other"]
89
+ enumValues:
90
+ [
91
+ "Article",
92
+ "Thread",
93
+ "Short Video",
94
+ "Long Video",
95
+ "Carousel",
96
+ "Image Post",
97
+ "Story",
98
+ "Newsletter Section",
99
+ "Podcast Episode",
100
+ "Other",
101
+ ]
80
102
  - slug: post-platform
81
103
  label: Platform
82
104
  valueType: string
83
105
  inputType: select
84
- enumValues: ["LinkedIn", "Instagram", "TikTok", "YouTube", "Twitter / X", "Threads", "Bluesky", "Newsletter", "Podcast", "Blog", "Other"]
106
+ enumValues:
107
+ [
108
+ "LinkedIn",
109
+ "Instagram",
110
+ "TikTok",
111
+ "YouTube",
112
+ "Twitter / X",
113
+ "Threads",
114
+ "Bluesky",
115
+ "Newsletter",
116
+ "Podcast",
117
+ "Blog",
118
+ "Other",
119
+ ]
85
120
  - slug: post-pillar
86
121
  label: Pillar
87
122
  valueType: entity_id
@@ -160,7 +195,16 @@ profiles:
160
195
  label: Primary Formats
161
196
  valueType: string
162
197
  inputType: select
163
- enumValues: ["Articles + Threads", "Short Video", "Long Video", "Newsletter", "Podcast", "Carousel", "Mixed"]
198
+ enumValues:
199
+ [
200
+ "Articles + Threads",
201
+ "Short Video",
202
+ "Long Video",
203
+ "Newsletter",
204
+ "Podcast",
205
+ "Carousel",
206
+ "Mixed",
207
+ ]
164
208
  - slug: post-count-pillar
165
209
  label: Total Posts Published
166
210
  valueType: number
@@ -178,23 +222,61 @@ profiles:
178
222
  label: Target Frequency
179
223
  valueType: string
180
224
  inputType: select
181
- enumValues: ["Daily", "3x / week", "2x / week", "Weekly", "Bi-weekly", "Monthly", "Ad hoc"]
225
+ enumValues:
226
+ [
227
+ "Daily",
228
+ "3x / week",
229
+ "2x / week",
230
+ "Weekly",
231
+ "Bi-weekly",
232
+ "Monthly",
233
+ "Ad hoc",
234
+ ]
182
235
  - slug: pillar-tags
183
236
  label: Tags
184
237
  valueType: array
185
238
 
239
+ # `platform` is the ONE pod-wide publishing-account *vocabulary*. `scope: shared`
240
+ # makes it a single pod-wide profile row that a `require`-consumer (e.g.
241
+ # social.yaml) can adopt by RE-DECLARING this slug — the resolver writes the
242
+ # grant on declare, giving that workspace the same vocabulary instead of forking
243
+ # its own `channel` kind. (This shares the profile DEFINITION, not the account
244
+ # entities — `platform` has no `entityScope: pod`, so each workspace's platform
245
+ # rows stay its own.) This is the deterministic form of the campaign reuse
246
+ # pattern (there, a workspace kind is promoted to shared at apply; here the
247
+ # SSOT is shared from the start). The re-declared body in social must not
248
+ # conflict with this one — guarded locally by the shared-role SSOT check, not a
249
+ # CI gate.
186
250
  - slug: platform
187
251
  displayName: Platform
188
252
  icon: share-2
189
253
  color: "#06B6D4"
190
254
  description: "A social account, newsletter publication, or podcast channel. Each platform has its own audience metrics and publishing rhythm."
191
- scope: WORKSPACE
255
+ scope: shared
192
256
  properties:
193
257
  - slug: platform-type
194
258
  label: Type
195
259
  valueType: string
196
260
  inputType: select
197
- enumValues: ["LinkedIn", "Instagram", "TikTok", "YouTube", "Twitter / X", "Threads", "Bluesky", "Newsletter", "Podcast", "Blog / Website", "Other"]
261
+ enumValues:
262
+ [
263
+ "LinkedIn",
264
+ "Instagram",
265
+ "TikTok",
266
+ "YouTube",
267
+ "Twitter / X",
268
+ "Threads",
269
+ "Bluesky",
270
+ "Newsletter",
271
+ "Podcast",
272
+ "Blog / Website",
273
+ "Other",
274
+ ]
275
+ - slug: handle
276
+ label: Handle
277
+ valueType: string
278
+ inputType: text
279
+ placeholder: "@username"
198
280
  - slug: platform-status
199
281
  label: Status
200
282
  valueType: string
@@ -222,7 +304,16 @@ profiles:
222
304
  label: Posting Frequency
223
305
  valueType: string
224
306
  inputType: select
225
- enumValues: ["Daily", "3x / week", "2x / week", "Weekly", "Bi-weekly", "Monthly", "As Needed"]
307
+ enumValues:
308
+ [
309
+ "Daily",
310
+ "3x / week",
311
+ "2x / week",
312
+ "Weekly",
313
+ "Bi-weekly",
314
+ "Monthly",
315
+ "As Needed",
316
+ ]
226
317
  - slug: last-post-date
227
318
  label: Last Post Date
228
319
  valueType: date
@@ -246,7 +337,8 @@ profiles:
246
337
  label: Status
247
338
  valueType: string
248
339
  inputType: select
249
- enumValues: ["Idea", "Drafting", "Editing", "Scheduled", "Sent", "Archived"]
340
+ enumValues:
341
+ ["Idea", "Drafting", "Editing", "Scheduled", "Sent", "Archived"]
250
342
  - slug: newsletter-series
251
343
  label: Series
252
344
  valueType: string
@@ -327,7 +419,8 @@ profiles:
327
419
  label: Channel
328
420
  valueType: string
329
421
  inputType: select
330
- enumValues: [email, social, event, content, partnership, paid, organic, other]
422
+ enumValues:
423
+ [email, social, event, content, partnership, paid, organic, other]
331
424
  - slug: campaign-goal
332
425
  label: Goal
333
426
  valueType: string
@@ -3,6 +3,7 @@
3
3
 
4
4
  meta:
5
5
  slug: content-studio
6
+ domain: content
6
7
  name: Content Studio
7
8
  description: HyperFrames + Remotion video pipeline — script, design, compose, render, publish.
8
9
  icon: clapperboard
package/src/crm.yaml CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  meta:
8
8
  slug: crm
9
+ domain: revenue
9
10
  name: CRM
10
11
  description: Sales pipeline and contact management from first contact to handoff.
11
12
  icon: handshake
package/src/define.ts CHANGED
@@ -404,6 +404,7 @@ export interface PackageDefinitionOutput {
404
404
  slug: string;
405
405
  icon?: string;
406
406
  color?: string;
407
+ domain?: string;
407
408
  tags?: string[];
408
409
  isPublic?: boolean;
409
410
  requiredTier?: string | null;
@@ -442,13 +443,11 @@ export interface PackageDefinitionOutput {
442
443
  automations?: PackageAutomationWire[];
443
444
  /**
444
445
  * Entity-detail action placements — carried verbatim (authored form IS the
445
- * wire form). NOTE: the Hub `PackageApplySchema` (synap-backend
446
- * `hub-protocol/rest/packages.ts`) is a plain `z.object` and does not yet
447
- * declare `actionPlacements`, so on the CLI/Hub install path this field is
448
- * STRIPPED before `applyPackagePostWorkspace` sees it. The tRPC
449
- * `createFromDefinition` door DOES carry it. Named follow-up: add an
450
- * `ActionPlacementSchema` field to `PackageApplySchema` to close the Hub half
451
- * (out of the T2 fence — flagged in the handoff).
446
+ * wire form). The Hub `PackageApplySchema` (synap-backend
447
+ * `hub-protocol/rest/packages.ts`) now declares `actionPlacements`
448
+ * (`z.array(ActionPlacementSchema).optional()`), so BOTH the tRPC
449
+ * `createFromDefinition` door and the CLI/Hub install path carry it through to
450
+ * `applyPackagePostWorkspace` — the parity is closed.
452
451
  */
453
452
  actionPlacements?: WorkspaceYaml['actionPlacements'];
454
453
  onboarding?: WorkspaceYaml['onboarding'];
@@ -481,6 +480,7 @@ export function toPackageDefinition(
481
480
  slug: tpl.meta.slug,
482
481
  icon: tpl.meta.icon,
483
482
  color: tpl.meta.color,
483
+ domain: tpl.meta.domain,
484
484
  tags: tpl.meta.tags,
485
485
  isPublic: tpl.meta.isPublic ?? true,
486
486
  },
@@ -535,7 +535,7 @@ type PackageDefinitionForwarded =
535
535
  | 'integrations' // → capabilities
536
536
  | 'playbooks'
537
537
  | 'automations' // → mapped to the apply door's flowDefinition wire shape
538
- | 'actionPlacements' // → carried verbatim (Hub inbound schema decl is a follow-up)
538
+ | 'actionPlacements' // → carried verbatim (Hub inbound schema declares it — parity closed)
539
539
  | 'onboarding'
540
540
  | 'dependencies';
541
541
 
@@ -7,6 +7,7 @@
7
7
 
8
8
  meta:
9
9
  slug: dev-dashboard
10
+ domain: build
10
11
  name: "Synap Dev — Infrastructure Command Center"
11
12
  description: "Internal DevOps dashboard for monitoring Synap infrastructure. Tracks services, vendors, and environments with health status, architecture flow map, and quick access to admin panels."
12
13
  icon: server
@@ -123,7 +124,8 @@ profiles:
123
124
  label: Vendor Type
124
125
  valueType: string
125
126
  inputType: select
126
- enumValues: [Hosting, AI, Auth, Database, Monitoring, CI/CD, DNS, Email, Other]
127
+ enumValues:
128
+ [Hosting, AI, Auth, Database, Monitoring, CI/CD, DNS, Email, Other]
127
129
  - slug: vendor-url
128
130
  label: Vendor URL
129
131
  valueType: string
@@ -271,7 +273,8 @@ profiles:
271
273
  label: Type
272
274
  valueType: string
273
275
  inputType: select
274
- enumValues: [api_key, ssh_key, password, token, certificate, env_variable, other]
276
+ enumValues:
277
+ [api_key, ssh_key, password, token, certificate, env_variable, other]
275
278
  placeholder: "Select credential type"
276
279
  - slug: service-name
277
280
  label: Service
@@ -418,7 +421,14 @@ views:
418
421
  type: table
419
422
  scopeProfileSlug: service
420
423
  config:
421
- visibleColumns: [service-type, environment, deployment-branch, last-deploy, health-status]
424
+ visibleColumns:
425
+ [
426
+ service-type,
427
+ environment,
428
+ deployment-branch,
429
+ last-deploy,
430
+ health-status,
431
+ ]
422
432
  sortBy: last-deploy
423
433
  sortDirection: desc
424
434
 
@@ -854,12 +864,36 @@ bento:
854
864
  config:
855
865
  title: Quick Links
856
866
  links:
857
- - { label: GitHub, url: "https://github.com/synapsh", color: "#24292E" }
858
- - { label: Hetzner Console, url: "https://console.hetzner.cloud", color: "#D50C2D" }
859
- - { label: Vercel Dashboard, url: "https://vercel.com/dashboard", color: "#000000" }
860
- - { label: Ory Console, url: "https://console.ory.sh", color: "#5528FF" }
861
- - { label: Cloudflare, url: "https://dash.cloudflare.com", color: "#F38020" }
862
- - { label: OpenRouter, url: "https://openrouter.ai/dashboard", color: "#6366F1" }
867
+ - {
868
+ label: GitHub,
869
+ url: "https://github.com/synapsh",
870
+ color: "#24292E",
871
+ }
872
+ - {
873
+ label: Hetzner Console,
874
+ url: "https://console.hetzner.cloud",
875
+ color: "#D50C2D",
876
+ }
877
+ - {
878
+ label: Vercel Dashboard,
879
+ url: "https://vercel.com/dashboard",
880
+ color: "#000000",
881
+ }
882
+ - {
883
+ label: Ory Console,
884
+ url: "https://console.ory.sh",
885
+ color: "#5528FF",
886
+ }
887
+ - {
888
+ label: Cloudflare,
889
+ url: "https://dash.cloudflare.com",
890
+ color: "#F38020",
891
+ }
892
+ - {
893
+ label: OpenRouter,
894
+ url: "https://openrouter.ai/dashboard",
895
+ color: "#6366F1",
896
+ }
863
897
  - kind: widget
864
898
  widgetType: entity-list
865
899
  pos: { x: 0, y: 7, w: 6, h: 5 }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Unit tests for the domain taxonomy — the "works for any template" contract.
3
+ * Run: `npx vitest run packages/workspace-templates` (from the repo root).
4
+ */
5
+
6
+ import { describe, expect, it } from "vitest";
7
+ import {
8
+ groupByDomain,
9
+ domainLabel,
10
+ TEMPLATE_DOMAINS,
11
+ OTHER_DOMAIN_ID,
12
+ isKnownDomain,
13
+ } from "./domains.js";
14
+
15
+ type Row = { slug: string; domain?: string };
16
+ const domainOf = (r: Row) => r.domain;
17
+
18
+ describe("domainLabel", () => {
19
+ it("returns the registry label for a known domain", () => {
20
+ expect(domainLabel("revenue")).toBe("Sales & Marketing");
21
+ });
22
+ it("returns 'More' for a missing domain", () => {
23
+ expect(domainLabel(undefined)).toBe("More");
24
+ expect(domainLabel(null)).toBe("More");
25
+ expect(domainLabel("")).toBe("More");
26
+ });
27
+ it("title-cases an unknown-but-declared domain so it still reads", () => {
28
+ expect(domainLabel("supply-chain")).toBe("Supply Chain");
29
+ expect(domainLabel("bizops")).toBe("Bizops");
30
+ });
31
+ });
32
+
33
+ describe("groupByDomain — works for ANY template", () => {
34
+ it("orders known domains by the registry, never alphabetically", () => {
35
+ const rows: Row[] = [
36
+ { slug: "a", domain: "knowledge" }, // registry order 5
37
+ { slug: "b", domain: "strategy" }, // registry order 0
38
+ { slug: "c", domain: "content" }, // registry order 2
39
+ ];
40
+ const groups = groupByDomain(rows, domainOf);
41
+ expect(groups.map((g) => g.id)).toEqual(["strategy", "content", "knowledge"]);
42
+ });
43
+
44
+ it("preserves input order WITHIN a section", () => {
45
+ const rows: Row[] = [
46
+ { slug: "z", domain: "content" },
47
+ { slug: "a", domain: "content" },
48
+ ];
49
+ const [content] = groupByDomain(rows, domainOf);
50
+ expect(content.items.map((r) => r.slug)).toEqual(["z", "a"]);
51
+ });
52
+
53
+ it("places an unknown-but-declared domain AFTER all known ones", () => {
54
+ const rows: Row[] = [
55
+ { slug: "x", domain: "supply-chain" }, // unknown
56
+ { slug: "y", domain: "build" }, // known
57
+ ];
58
+ const groups = groupByDomain(rows, domainOf);
59
+ expect(groups.map((g) => g.id)).toEqual(["build", "supply-chain"]);
60
+ expect(groups[1].label).toBe("Supply Chain");
61
+ });
62
+
63
+ it("sorts multiple unknown domains alphabetically, before 'other'", () => {
64
+ const rows: Row[] = [
65
+ { slug: "1", domain: "zeta" },
66
+ { slug: "2" }, // missing → other
67
+ { slug: "3", domain: "alpha" },
68
+ { slug: "4", domain: "revenue" }, // known
69
+ ];
70
+ const groups = groupByDomain(rows, domainOf);
71
+ expect(groups.map((g) => g.id)).toEqual([
72
+ "revenue",
73
+ "alpha",
74
+ "zeta",
75
+ OTHER_DOMAIN_ID,
76
+ ]);
77
+ });
78
+
79
+ it("routes a MISSING domain into the 'More' bucket, always LAST", () => {
80
+ const rows: Row[] = [
81
+ { slug: "1" },
82
+ { slug: "2", domain: "strategy" },
83
+ ];
84
+ const groups = groupByDomain(rows, domainOf);
85
+ expect(groups[groups.length - 1].id).toBe(OTHER_DOMAIN_ID);
86
+ expect(groups[groups.length - 1].label).toBe("More");
87
+ });
88
+
89
+ it("NEVER drops an item — count in equals count out", () => {
90
+ const rows: Row[] = [
91
+ { slug: "1", domain: "build" },
92
+ { slug: "2", domain: "made-up" },
93
+ { slug: "3" },
94
+ { slug: "4", domain: "build" },
95
+ ];
96
+ const total = groupByDomain(rows, domainOf).reduce(
97
+ (n, g) => n + g.items.length,
98
+ 0
99
+ );
100
+ expect(total).toBe(rows.length);
101
+ });
102
+
103
+ it("omits empty sections", () => {
104
+ const groups = groupByDomain([{ slug: "1", domain: "build" }], domainOf);
105
+ expect(groups).toHaveLength(1);
106
+ expect(groups[0].id).toBe("build");
107
+ });
108
+ });
109
+
110
+ describe("isKnownDomain", () => {
111
+ it("recognises every registry id and rejects the rest", () => {
112
+ for (const d of TEMPLATE_DOMAINS) expect(isKnownDomain(d.id)).toBe(true);
113
+ expect(isKnownDomain("made-up")).toBe(false);
114
+ expect(isKnownDomain(undefined)).toBe(false);
115
+ expect(isKnownDomain(OTHER_DOMAIN_ID)).toBe(false);
116
+ });
117
+ });
package/src/domains.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Template DOMAIN taxonomy — the authored, cross-surface grouping that sections
3
+ * the marketplace (CLI `market list`, browser New Space + catalog, marketplace).
4
+ *
5
+ * WHY A FIELD, NOT DERIVED FROM TAGS. Grouping is a property of the template, so
6
+ * it lives ON the template (`meta.domain`) — authored once, read by every
7
+ * surface. The alternative was a hardcoded slug→group map (or a tag heuristic)
8
+ * inside each consumer; that map drifts the instant a template is added, and no
9
+ * one authoring a template can see it. Same lesson as the vocabulary work: a
10
+ * declared relationship should live where it can be verified, not re-guessed.
11
+ *
12
+ * NOT `category`. The catalog already uses `category` for the PACKAGE TYPE
13
+ * (`workspace`/`capability`/`skill`/…). `domain` is the human subject area.
14
+ *
15
+ * WORKS FOR ANY TEMPLATE. A template may declare a domain absent from this
16
+ * registry, or none at all — `groupByDomain` never drops one. Known domains
17
+ * render first in registry order; an unknown-but-declared domain renders after
18
+ * them (its id title-cased); a missing domain falls into the `other` bucket,
19
+ * always last. So a NEW template appears correctly without touching this file —
20
+ * adding it here only controls a domain's ORDER and LABEL.
21
+ */
22
+
23
+ export interface TemplateDomain {
24
+ id: string;
25
+ label: string;
26
+ }
27
+
28
+ export const OTHER_DOMAIN_ID = "other";
29
+ export const OTHER_DOMAIN_LABEL = "More";
30
+
31
+ /** Known domains, in display order (the order sections appear in a listing). */
32
+ export const TEMPLATE_DOMAINS: readonly TemplateDomain[] = [
33
+ { id: "strategy", label: "Strategy & Market" },
34
+ { id: "revenue", label: "Sales & Marketing" },
35
+ { id: "content", label: "Content & Social" },
36
+ { id: "company", label: "Company & Operations" },
37
+ { id: "build", label: "Build & Operate" },
38
+ { id: "knowledge", label: "Knowledge & Research" },
39
+ ];
40
+
41
+ const KNOWN = new Map(
42
+ TEMPLATE_DOMAINS.map((d, i) => [d.id, { label: d.label, order: i }] as const)
43
+ );
44
+
45
+ /** Every valid domain id the registry knows about (excludes `other`). */
46
+ export function isKnownDomain(id: string | undefined | null): boolean {
47
+ return id != null && KNOWN.has(id);
48
+ }
49
+
50
+ /** Display label for a domain id. Missing → "More"; unknown-declared → title-cased id. */
51
+ export function domainLabel(id: string | undefined | null): string {
52
+ if (!id) return OTHER_DOMAIN_LABEL;
53
+ const known = KNOWN.get(id);
54
+ if (known) return known.label;
55
+ return id
56
+ .replace(/[-_]/g, " ")
57
+ .replace(/\b\w/g, (c) => c.toUpperCase());
58
+ }
59
+
60
+ export interface DomainGroup<T> {
61
+ id: string;
62
+ label: string;
63
+ items: T[];
64
+ }
65
+
66
+ /**
67
+ * Partition `items` into ordered domain sections: known domains first (registry
68
+ * order), then unknown-but-declared domains (alphabetical), then the missing
69
+ * "More" bucket, always last. Empty sections are omitted; nothing is dropped.
70
+ * Item order WITHIN a section is preserved from the input.
71
+ */
72
+ export function groupByDomain<T>(
73
+ items: readonly T[],
74
+ domainOf: (item: T) => string | undefined | null
75
+ ): DomainGroup<T>[] {
76
+ const buckets = new Map<string, T[]>();
77
+ for (const item of items) {
78
+ const id = domainOf(item) || OTHER_DOMAIN_ID;
79
+ const bucket = buckets.get(id);
80
+ if (bucket) bucket.push(item);
81
+ else buckets.set(id, [item]);
82
+ }
83
+
84
+ const rank = (id: string): number => {
85
+ if (id === OTHER_DOMAIN_ID) return Number.MAX_SAFE_INTEGER;
86
+ const known = KNOWN.get(id);
87
+ return known ? known.order : 1_000_000; // unknown-declared: after known, before "other"
88
+ };
89
+
90
+ return [...buckets.entries()]
91
+ .sort(([a], [b]) => {
92
+ const ra = rank(a);
93
+ const rb = rank(b);
94
+ if (ra !== rb) return ra - rb;
95
+ return a < b ? -1 : a > b ? 1 : 0;
96
+ })
97
+ .map(([id, groupItems]) => ({
98
+ id,
99
+ label: id === OTHER_DOMAIN_ID ? OTHER_DOMAIN_LABEL : domainLabel(id),
100
+ items: groupItems,
101
+ }));
102
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  meta:
10
10
  slug: ecosystem
11
+ domain: strategy
11
12
  name: Ecosystem
12
13
  description: Map the market you operate in — the partners, competitors, and players, the segments and trends, and the relationships between them.
13
14
  icon: radar
@@ -170,7 +171,8 @@ profiles:
170
171
  label: Type
171
172
  valueType: string
172
173
  inputType: select
173
- enumValues: [raise, partnership, launch, acquisition, listing, conference, other]
174
+ enumValues:
175
+ [raise, partnership, launch, acquisition, listing, conference, other]
174
176
  - slug: event-date
175
177
  label: Date
176
178
  valueType: date
@@ -390,9 +392,19 @@ playbooks:
390
392
  profileSlug: competitor
391
393
  params:
392
394
  - { name: competitor, type: entity, label: Competitor, required: true }
393
- - { name: focus, type: text, label: Focus area, defaultValue: "product, pricing, and positioning" }
395
+ - {
396
+ name: focus,
397
+ type: text,
398
+ label: Focus area,
399
+ defaultValue: "product, pricing, and positioning",
400
+ }
394
401
  - { name: companyContext, type: text, label: Our company context }
395
- - { name: outputTypes, type: text, label: Outputs, defaultValue: "a head-to-head comparison summary and an updated competitor profile" }
402
+ - {
403
+ name: outputTypes,
404
+ type: text,
405
+ label: Outputs,
406
+ defaultValue: "a head-to-head comparison summary and an updated competitor profile",
407
+ }
396
408
  executor: is-agent
397
409
  # A radar MONITOR: bound to the competitor KIND + a schedule → the backing
398
410
  # cron automation fans out (query→loop→playbook_run) and runs this once per
@@ -423,7 +435,12 @@ playbooks:
423
435
  angles you covered, and why each new one matters.
424
436
  schedule: { cron: "0 9 * * 1", enabled: false }
425
437
  params:
426
- - { name: ourSpace, type: text, label: Our market / category, required: true }
438
+ - {
439
+ name: ourSpace,
440
+ type: text,
441
+ label: Our market / category,
442
+ required: true,
443
+ }
427
444
  executor: is-agent
428
445
  grants:
429
446
  - exa_search
@@ -20,6 +20,7 @@
20
20
 
21
21
  meta:
22
22
  slug: enterprise-os
23
+ domain: company
23
24
  name: Enterprise OS
24
25
  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
26
  icon: layout-dashboard