@synap-core/workspace-templates 0.7.1 → 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 (50) hide show
  1. package/dist/index.d.ts +266 -8
  2. package/dist/index.js +2314 -1440
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  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 +110 -18
  10. package/src/catalog.test.ts +2 -2
  11. package/src/catalog.ts +13 -3
  12. package/src/communication.yaml +23 -453
  13. package/src/composition-graph.test.ts +28 -2
  14. package/src/concept-guard.test.ts +176 -0
  15. package/src/concept-guard.ts +219 -0
  16. package/src/content-os.yaml +103 -10
  17. package/src/content-studio.yaml +1 -0
  18. package/src/crm.yaml +1 -0
  19. package/src/define.ts +8 -8
  20. package/src/dev-dashboard.yaml +43 -9
  21. package/src/domains.test.ts +117 -0
  22. package/src/domains.ts +102 -0
  23. package/src/ecosystem.yaml +25 -5
  24. package/src/enterprise-os.yaml +1 -0
  25. package/src/finance.yaml +46 -5
  26. package/src/foundation.yaml +13 -1
  27. package/src/generate.d.mts +15 -0
  28. package/src/generate.mjs +86 -0
  29. package/src/grants.yaml +1 -0
  30. package/src/hr.yaml +61 -8
  31. package/src/index.ts +40 -0
  32. package/src/internal-runbook.yaml +36 -5
  33. package/src/legal.yaml +97 -12
  34. package/src/life-os.yaml +188 -27
  35. package/src/marketing.yaml +15 -2
  36. package/src/meetings-knowledge.yaml +143 -23
  37. package/src/operations.yaml +347 -0
  38. package/src/outreach-comms.yaml +11 -3
  39. package/src/project-management.yaml +840 -111
  40. package/src/research-base.yaml +516 -0
  41. package/src/research-decision.yaml +158 -0
  42. package/src/shared-role-ssot.test.ts +268 -0
  43. package/src/social.yaml +105 -37
  44. package/src/templates-freshness.test.ts +55 -0
  45. package/src/templates.ts +4620 -4278
  46. package/src/the-arch.yaml +1 -0
  47. package/src/types.ts +9 -0
  48. package/src/validate.test.ts +81 -0
  49. package/src/validate.ts +561 -0
  50. package/src/personal.yaml +0 -177
@@ -5,6 +5,7 @@
5
5
 
6
6
  meta:
7
7
  slug: operations
8
+ domain: company
8
9
  name: Operations
9
10
  description: "Deliver client work end to end — the engagements you're running, the deliverables due inside them, and the contracts that govern them. Won deals from CRM hand off into engagements here."
10
11
  icon: briefcase
@@ -25,6 +26,19 @@ workspace:
25
26
  crm: consumer
26
27
  operations: provider
27
28
 
29
+ # The daily client-briefing automation below enumerates clients with
30
+ # `entity.query { profileSlug: "client" }`, and that verb FAILS CLOSED on an
31
+ # unknown slug (`assertKnownProfileSlug`) — an Operations workspace installed
32
+ # without the pod-wide `client` role would kill the run on its first node. The
33
+ # `client` role is foundation's SSOT (foundation.yaml, `scope: shared`,
34
+ # `profileKind: role`), so require it, exactly like crm.yaml does. `require`,
35
+ # not `compose`: foundation is a real workspace, not an overlay.
36
+ dependencies:
37
+ - slug: foundation
38
+ kind: workspace
39
+ relation: require
40
+ reason: "Provides the pod-wide `client` role the daily client-briefing automation queries."
41
+
28
42
  profiles:
29
43
  - slug: engagement
30
44
  displayName: Engagement
@@ -140,6 +154,65 @@ profiles:
140
154
  valueType: string
141
155
  inputType: textarea
142
156
 
157
+ # The durable OUTPUT of the daily briefing automation. A recap that only ever
158
+ # exists as a chat message is not knowledge — it cannot be queried, ranked, or
159
+ # linked. This kind is what turns the digest into structure: one dated row per
160
+ # client per run, linked to the client entity and readable long after the
161
+ # message scrolled away. Written to the TEAM side only; never client-facing.
162
+ - slug: client-briefing
163
+ displayName: Client Briefing
164
+ icon: newspaper
165
+ color: "#F59E0B"
166
+ description: "A dated internal recap of one client's recent communications — what moved, what's at risk, what's next. For the team, never for the client."
167
+ properties:
168
+ - slug: briefing-client
169
+ label: Client
170
+ valueType: entity_id
171
+ targetProfileSlug: company
172
+ inputType: entity_select
173
+ - slug: briefing-date
174
+ label: Written At
175
+ valueType: date
176
+ inputType: date
177
+ # Idempotency key, NOT display. `output/entity_create`'s `dedupeBy` matches
178
+ # on this exact value, so a redelivered run reuses the row it already wrote
179
+ # instead of appending a second briefing for the same client and tick.
180
+ - slug: briefing-key
181
+ label: Run Key
182
+ valueType: string
183
+ inputType: text
184
+ - slug: briefing-headline
185
+ label: Headline
186
+ valueType: string
187
+ inputType: text
188
+ - slug: briefing-summary
189
+ label: Summary
190
+ valueType: string
191
+ inputType: textarea
192
+ - slug: briefing-risks
193
+ label: Risks
194
+ valueType: string
195
+ inputType: textarea
196
+ - slug: briefing-next-actions
197
+ label: Next Actions
198
+ valueType: string
199
+ inputType: textarea
200
+ - slug: briefing-message-count
201
+ label: Messages Read
202
+ valueType: number
203
+ inputType: number
204
+
205
+ # Schema relations. `briefing_about_client` MUST be declared here: an automation
206
+ # `relation_create` dispatches `graph.link` → `relations.create`, which rejects
207
+ # any type that is neither a system built-in nor a workspace relation def
208
+ # (routers/relations.ts — "Unknown relation type"). The applier find-or-creates
209
+ # the def from this block, so the automation's link resolves.
210
+ entityLinks:
211
+ - sourceProfileSlug: client-briefing
212
+ targetProfileSlug: company
213
+ type: briefing_about_client
214
+ label: Client
215
+
143
216
  # CURATED views — the delivery motion, not one per profile
144
217
  views:
145
218
  - name: Engagements
@@ -159,6 +232,11 @@ views:
159
232
  type: table
160
233
  scopeProfileSlug: contract
161
234
  description: "Contracts with their status, value, and term."
235
+ - name: Client Briefings
236
+ slug: client-briefings
237
+ type: list
238
+ scopeProfileSlug: client-briefing
239
+ description: "The daily internal recaps, newest first."
162
240
 
163
241
  onboarding:
164
242
  goal: >
@@ -220,3 +298,272 @@ bento:
220
298
  - kind: view
221
299
  viewName: Contracts
222
300
  pos: { x: 6, y: 0, w: 6, h: 5 }
301
+
302
+ # ============================================================================
303
+ # Automations — the daily client-briefing digest.
304
+ #
305
+ # WHAT: once a day, for every `client`, read that client's inbound comms,
306
+ # distil the real signal with ONE batched AI call (never per-message), persist
307
+ # a durable `client-briefing` row, and post the recap to the client's TEAM-side
308
+ # channel. Firewall: the recap NEVER lands in the client-facing channel.
309
+ #
310
+ # GOVERNANCE DESIGN (why this run cannot die on a `proposed` verdict):
311
+ # • The three `capability` nodes call ONLY read-only builtins —
312
+ # `entity.query`, `ai.generate`, `channel.resolve` — all in
313
+ # READ_ONLY_BUILTIN_VERBS, so the capability gate AUTO-RUNS them (no grant,
314
+ # no proposal). A `capability` node THROWS on a `proposed` verdict
315
+ # (automation-executor.ts) — these three can never reach it.
316
+ # • `messages_query` + `condition` are pure read / pure eval — never governed.
317
+ # • The two governed WRITES are `output` nodes, NOT `capability` nodes:
318
+ # – `entity_create` returns `{status:"proposed"}` on a propose verdict
319
+ # (it never throws), and `entity.create` is in DEFAULT_AUTO_APPROVE, so
320
+ # a human-owned or agent-owned run auto-approves and writes directly.
321
+ # – `relation_create` dispatches `graph.link`; the `graph.link` builtin is
322
+ # seeded `approved:true` (rung 2.7 → auto) and `relation.create` is in
323
+ # DEFAULT_AUTO_APPROVE, so it auto-executes — never a propose verdict.
324
+ # – `channel_message` is a direct, firewall-checked insert with NO
325
+ # governance gate at all.
326
+ # • Every write also lives INSIDE the loop body, whose per-item try/catch
327
+ # aborts only THAT client's remaining steps on any error — the RUN itself
328
+ # survives regardless.
329
+ #
330
+ # `status: active` is load-bearing: the cron scheduler selects
331
+ # `status="active" AND nextRunAt <= now`. A `draft` never fires — that was the
332
+ # exact bug this automation replaces. `metadata.templateKey` makes re-apply
333
+ # idempotent (the post-workspace applier dedups by name / templateKey).
334
+ #
335
+ # ⚠️ KNOWN BLOCKER — RETURNS ZERO CLIENTS UNTIL A BACKEND FIX LANDS.
336
+ # `client`/`company` entities are POD-WIDE (`workspaceId: null`). This automation
337
+ # runs under a WORKSPACE LENS, and `workspaceLensWhere`
338
+ # (`packages/api/src/utils/user-visible-where.ts`) excludes pod-wide globals from
339
+ # a focused workspace by design (`includeGlobals` defaults falsy — product
340
+ # decision 2026-06-15). So the `entity.query` verb — which resolves
341
+ # `input.workspaceId ?? ctx.workspaceId` and CANNOT express a global lens (its
342
+ # param is `workspaceId: uuid().optional()`) — enumerates ZERO clients under the
343
+ # Operations lens. The `list-clients` node is deliberately left with NO
344
+ # workspace override so it starts working the moment the backend fix (let
345
+ # `entity.query` express a pod-wide/global lens → `workspaceLensWhere` with
346
+ # `null`/`includeGlobals:true`) lands. NO workaround is baked in here — a
347
+ # hardcoded client list or a workspace-scoped copy would defeat the point.
348
+ #
349
+ # LIMITATION (honest): `messages_query` resolves ONLY the EXTERNAL client-comms
350
+ # channel bound to the client entity — team-side chatter ABOUT a client is not
351
+ # in the digest input. The recap is intentionally still posted to the TEAM
352
+ # channel; it summarises the client conversation, not internal discussion.
353
+ # ============================================================================
354
+ automations:
355
+ - key: operations-daily-client-briefing
356
+ name: Daily client briefing
357
+ description: >-
358
+ Each morning, for every client, distil their recent comms into one
359
+ batched digest, save it as a durable client-briefing, and post the recap
360
+ to the client's internal TEAM channel (never the client-facing channel).
361
+ triggerType: cron
362
+ triggerConfig:
363
+ expression: "0 8 * * *"
364
+ status: active
365
+ metadata:
366
+ templateKey: operations-daily-client-briefing
367
+ # The `list-clients` node uses `scope: pod` — the entity.query pod-wide
368
+ # opt-in. That opt-in must be DEPLOYED for the query to return pod-wide
369
+ # clients; without it a pod-wide client query yields 0 rows and this
370
+ # automation is a silent no-op (fires on cron, loops over nothing).
371
+ requiresPodWideEntityQueryLens: true
372
+ flow:
373
+ nodes:
374
+ - id: trigger
375
+ type: trigger
376
+ position: { x: 0, y: 0 }
377
+ data:
378
+ label: Daily at 08:00
379
+ triggerType: cron
380
+ config:
381
+ expression: "0 8 * * *"
382
+
383
+ # READ-ONLY builtin. Enumerates clients by the pod-wide `client` ROLE
384
+ # (foundation SSOT). `entity.query` fails CLOSED on an unknown slug via
385
+ # `assertKnownProfileSlug` — the `dependencies: require foundation` block
386
+ # above guarantees the `client` role exists, so the first node can't die.
387
+ #
388
+ # `scope: pod` = the EXPLICIT pod-wide opt-in. `client` entities are
389
+ # pod-wide (workspaceId:null); a run's workspace lens hides pod-wide
390
+ # globals by default (workspaceLensWhere, product decision 2026-06-15),
391
+ # so WITHOUT scope:pod this loops over zero clients. scope:pod pins the
392
+ # globals-only, owner-gated lens, returning exactly the caller's pod-wide
393
+ # clients — an explicit request, NOT globals silently bleeding into a
394
+ # focused workspace. Do NOT "fix" a zero result by hardcoding a client
395
+ # list or a workspace-scoped profile — scope:pod IS the fix.
396
+ - id: list-clients
397
+ type: capability
398
+ position: { x: 0, y: 120 }
399
+ data:
400
+ label: List clients (pod-wide `client` role)
401
+ verbId: entity.query
402
+ inputMapping:
403
+ profileSlug: client
404
+ scope: pod
405
+ limit: "100"
406
+
407
+ - id: clients-loop
408
+ type: loop
409
+ position: { x: 0, y: 240 }
410
+ data:
411
+ label: For each client
412
+ iteratorExpression: steps.list-clients.output.entities
413
+ itemVariable: client
414
+
415
+ # READ. Resolves the client's EXTERNAL client-comms channel bound to the
416
+ # client entity and returns its recent window. Team-side chatter is NOT
417
+ # included (see the LIMITATION note above).
418
+ - id: read-comms
419
+ type: messages_query
420
+ position: { x: 0, y: 360 }
421
+ data:
422
+ label: Read this client's recent comms
423
+ subjectEntityId: "{{loop.item.id}}"
424
+ limit: 40
425
+
426
+ # READ-ONLY builtin (pure compute — mutates nothing, auto-runs). ONE
427
+ # batched digest for the whole window; NO per-message AI. Returns STRICT
428
+ # JSON whose fields map onto the client-briefing properties below.
429
+ - id: digest
430
+ type: capability
431
+ position: { x: 0, y: 480 }
432
+ data:
433
+ label: Distil the real signal → JSON
434
+ verbId: ai.generate
435
+ inputMapping:
436
+ json: "true"
437
+ maxTokens: "1200"
438
+ system: >-
439
+ You advise a client-delivery team. You read one client's recent
440
+ inbound messages and surface only what genuinely matters. You
441
+ are firewall-aware: your output is for the INTERNAL team, never
442
+ the client.
443
+ prompt: >-
444
+ Digest the recent communications for the client
445
+ "{{loop.item.title}}" into a concise internal briefing for OUR
446
+ delivery team.
447
+
448
+ PEOPLE GUARD (do not break): distinguish the CLIENT's people
449
+ from OUR team members who also appear in the thread. Never treat
450
+ a teammate as a client contact, lead, or new person. Only real,
451
+ client-side signal counts.
452
+
453
+ Surface ONLY real signal: status changes, decisions,
454
+ deals/opportunities, competitor mentions, new client-side
455
+ leads/contacts, risks/blockers, and deadlines/commitments. Skip
456
+ small talk and noise. Lead with what matters; keep it tight.
457
+
458
+ Respond with STRICT JSON and NOTHING else:
459
+ {
460
+ "hasSignal": "true" or "false" (a STRING),
461
+ "headline": "<one-line summary; empty string when hasSignal is 'false'>",
462
+ "summary": "<a short markdown digest for the team; empty string when 'false'>",
463
+ "risks": "<risks/blockers as short markdown, or empty string>",
464
+ "nextActions": "<recommended next actions as short markdown, or empty string>"
465
+ }
466
+ Set hasSignal to "false" when there is no notable activity (no
467
+ messages, or only chatter).
468
+
469
+ Recent conversation (oldest first, JSON):
470
+ {{steps.read-comms.output.messages}}
471
+
472
+ # PER-ITEM FILTER (continue-semantics). When the digest found no signal,
473
+ # skip the rest of THIS client's body — no briefing row, no post. A
474
+ # `condition` node is a loop-body type; a `switch` is NOT (it would break
475
+ # the loop's contiguous-body chain), so the gate MUST be a `condition`.
476
+ - id: signal-gate
477
+ type: condition
478
+ position: { x: 0, y: 600 }
479
+ data:
480
+ label: Only continue when there's real signal
481
+ expression: "steps.digest.output.hasSignal === true"
482
+
483
+ # GOVERNED WRITE via an `output` node (returns `proposed`, never throws;
484
+ # `entity.create` is auto-approved). Durable, dated log row.
485
+ # `dedupeBy: briefing-key` makes a redelivered run reuse the same row
486
+ # instead of appending a duplicate for the same client + tick.
487
+ - id: write-briefing
488
+ type: output
489
+ position: { x: 0, y: 720 }
490
+ data:
491
+ label: Save the client briefing
492
+ outputType: entity_create
493
+ config:
494
+ profileSlug: client-briefing
495
+ title: "Briefing — {{loop.item.title}}"
496
+ dedupeBy: briefing-key
497
+ properties:
498
+ briefing-key: "{{loop.item.id}}::{{trigger.payload.scheduledAt}}"
499
+ briefing-client: "{{loop.item.id}}"
500
+ briefing-date: "{{trigger.payload.scheduledAt}}"
501
+ briefing-headline: "{{steps.digest.output.headline}}"
502
+ briefing-summary: "{{steps.digest.output.summary}}"
503
+ briefing-risks: "{{steps.digest.output.risks}}"
504
+ briefing-next-actions: "{{steps.digest.output.nextActions}}"
505
+ briefing-message-count: "{{steps.read-comms.output.count}}"
506
+
507
+ # GOVERNED WRITE via `output` node → `graph.link` (auto-executes:
508
+ # builtin approved + relation.create in DEFAULT_AUTO_APPROVE). Links the
509
+ # briefing to its client. The relation TYPE is declared in `entityLinks`
510
+ # above, so `relations.create` accepts it instead of "Unknown relation
511
+ # type". `dedupe` (default) skips a duplicate edge on re-run.
512
+ - id: link-briefing
513
+ type: output
514
+ position: { x: 0, y: 840 }
515
+ data:
516
+ label: Link briefing → client
517
+ outputType: relation_create
518
+ config:
519
+ fromEntityId: "{{steps.write-briefing.output.entityId}}"
520
+ toEntityId: "{{loop.item.id}}"
521
+ relationType: briefing_about_client
522
+
523
+ # READ-ONLY builtin. Resolves the client's TEAM-side channel by
524
+ # branchPurpose (the firewall ROLE), NOT the client-facing channel.
525
+ - id: resolve-team
526
+ type: capability
527
+ position: { x: 0, y: 960 }
528
+ data:
529
+ label: Resolve the client's TEAM channel (FIREWALL)
530
+ verbId: channel.resolve
531
+ inputMapping:
532
+ contextObjectType: entity
533
+ contextObjectId: "{{loop.item.id}}"
534
+ branchPurpose: team
535
+
536
+ # UNGOVERNED direct insert (firewall-checked mirror). Posts to the
537
+ # EXPLICIT resolved team channelId — NOT channelType:'subjectEntity',
538
+ # which needs a run subjectEntityId that CRON runs never carry.
539
+ - id: post-recap
540
+ type: output
541
+ position: { x: 0, y: 1080 }
542
+ data:
543
+ label: Post the recap to the TEAM channel (FIREWALL)
544
+ outputType: channel_message
545
+ config:
546
+ channelId: "{{steps.resolve-team.output.channelId}}"
547
+ content: >-
548
+ **{{loop.item.title}} — daily briefing**
549
+
550
+ {{steps.digest.output.headline}}
551
+
552
+ {{steps.digest.output.summary}}
553
+
554
+ **Risks:** {{steps.digest.output.risks}}
555
+
556
+ **Next:** {{steps.digest.output.nextActions}}
557
+ metadata:
558
+ automationMessage: true
559
+ kind: client-briefing
560
+ edges:
561
+ - { id: e-trigger-list, source: trigger, target: list-clients }
562
+ - { id: e-list-loop, source: list-clients, target: clients-loop }
563
+ - { id: e-loop-read, source: clients-loop, target: read-comms }
564
+ - { id: e-read-digest, source: read-comms, target: digest }
565
+ - { id: e-digest-gate, source: digest, target: signal-gate }
566
+ - { id: e-gate-write, source: signal-gate, target: write-briefing }
567
+ - { id: e-write-link, source: write-briefing, target: link-briefing }
568
+ - { id: e-link-resolve, source: link-briefing, target: resolve-team }
569
+ - { id: e-resolve-post, source: resolve-team, target: post-recap }
@@ -8,6 +8,7 @@
8
8
 
9
9
  meta:
10
10
  slug: outreach-comms
11
+ domain: revenue
11
12
  name: Outreach & Comms
12
13
  description: Personalized LinkedIn and email outreach — sequenced touches against real people and companies, drafted and sent by an agent.
13
14
  icon: send
@@ -189,7 +190,11 @@ playbooks:
189
190
  - { name: target, type: entity, label: Target contact, required: true }
190
191
  - { name: campaign, type: text, label: Campaign name, required: true }
191
192
  - { name: channel, type: choice, label: Channel, defaultValue: linkedin }
192
- - { name: campaignContext, type: text, label: Value prop + call to action }
193
+ - {
194
+ name: campaignContext,
195
+ type: text,
196
+ label: Value prop + call to action,
197
+ }
193
198
  executor: is-agent
194
199
  grants:
195
200
  - linkedin_send_invite
@@ -256,7 +261,7 @@ app:
256
261
  layout:
257
262
  defaultView: Campaigns
258
263
  defaultApp: null
259
- pinnedApps: [dashboard, data]
264
+ pinnedApps: [dashboard, data, chat]
260
265
  sidebarItems:
261
266
  - kind: view
262
267
  viewName: Campaigns
@@ -272,7 +277,10 @@ layout:
272
277
  icon: database
273
278
 
274
279
  bento:
275
- viewName: Campaigns
280
+ # Distinct from the "Campaigns" table it embeds — see meetings-knowledge:
281
+ # the bento becomes its own view row, so a shared name collides in the
282
+ # switcher and makes layout.defaultView ambiguous.
283
+ viewName: Outreach Home
276
284
  blocks:
277
285
  - kind: view
278
286
  viewName: Campaigns