@synap-core/workspace-templates 0.11.0 → 0.23.0

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 (249) hide show
  1. package/README.md +483 -0
  2. package/automations/bookmark-enrichment.automation.json +321 -0
  3. package/automations/github-pr-events.automation.json +505 -0
  4. package/automations/mail-feed.automation.json +86 -0
  5. package/automations/ship-a-change-checks-green.automation.json +106 -0
  6. package/automations/ship-a-change-checks-red.automation.json +106 -0
  7. package/automations/ship-a-change-deployed.automation.json +56 -0
  8. package/automations/ship-a-change-pr-merged.automation.json +110 -0
  9. package/automations/ship-a-change-pr-opened.automation.json +104 -0
  10. package/automations/url-bookmark-capture.automation.json +132 -0
  11. package/capabilities/agency-skills.capability.json +157 -0
  12. package/capabilities/apify.capability.json +265 -0
  13. package/capabilities/apollo.capability.json +120 -0
  14. package/capabilities/arch-backend.capability.json +97 -0
  15. package/capabilities/arch-client-intelligence.capability.json +401 -0
  16. package/capabilities/backblaze-b2.capability.json +205 -0
  17. package/capabilities/cal-com.capability.json +266 -0
  18. package/capabilities/canva.capability.json +274 -0
  19. package/capabilities/claude-code-routines.capability.json +172 -0
  20. package/capabilities/claude-managed-agents.capability.json +215 -0
  21. package/capabilities/client-comms-report.capability.json +246 -0
  22. package/capabilities/content-idea-to-production.capability.json +29 -0
  23. package/capabilities/datagouv.capability.json +315 -0
  24. package/capabilities/discord-bot.capability.json +157 -0
  25. package/capabilities/dropbox.capability.json +281 -0
  26. package/capabilities/exa.capability.json +109 -0
  27. package/capabilities/fal-ai.capability.json +83 -0
  28. package/capabilities/fal-media-lab.capability.json +158 -0
  29. package/capabilities/fireflies.capability.json +191 -0
  30. package/capabilities/freellmapi.capability.json +114 -0
  31. package/capabilities/generic-apikey.capability.json +92 -0
  32. package/capabilities/github-merge.capability.json +99 -0
  33. package/capabilities/github-read.capability.json +325 -0
  34. package/capabilities/governance-tighten-recommender.capability.json +81 -0
  35. package/capabilities/jev.capability.json +226 -0
  36. package/capabilities/lead-conversion-enrollment.capability.json +246 -0
  37. package/capabilities/mail-feed.capability.json +277 -0
  38. package/capabilities/mailgun.capability.json +52 -0
  39. package/capabilities/nango-google.capability.json +639 -0
  40. package/capabilities/notion.capability.json +417 -0
  41. package/capabilities/omniroute.capability.json +137 -0
  42. package/capabilities/posthog-analytics.capability.json +258 -0
  43. package/capabilities/postiz.capability.json +467 -0
  44. package/capabilities/proactive-digest.capability.json +190 -0
  45. package/capabilities/proton.capability.json +288 -0
  46. package/capabilities/replicate-rvm.capability.json +97 -0
  47. package/capabilities/research-methods.capability.json +57 -0
  48. package/capabilities/shopify.capability.json +395 -0
  49. package/capabilities/slack.capability.json +369 -0
  50. package/capabilities/stripe.capability.json +420 -0
  51. package/capabilities/telegram-bridge.capability.json +56 -0
  52. package/capabilities/treg.capability.json +102 -0
  53. package/capabilities/unipile-linkedin.capability.json +471 -0
  54. package/capabilities/vercel.capability.json +424 -0
  55. package/dist/define-CPS0n8BV.d.ts +1331 -0
  56. package/dist/index.d.ts +189 -900
  57. package/dist/index.js +24654 -21639
  58. package/dist/index.js.map +1 -1
  59. package/dist/wire.d.ts +222 -0
  60. package/dist/wire.js +77 -0
  61. package/dist/wire.js.map +1 -0
  62. package/package.json +41 -27
  63. package/packs/bookmark-views-pack.json +33 -0
  64. package/packs/crm-pack.json +76 -0
  65. package/packs/customer-profile.json +28 -0
  66. package/packs/deep-research.playbook.json +30 -0
  67. package/packs/event-views-pack.json +33 -0
  68. package/packs/habit-profile.json +27 -0
  69. package/packs/hr-pack.json +54 -0
  70. package/packs/invoice-profile.json +28 -0
  71. package/packs/knowledge-base-pack.json +56 -0
  72. package/packs/meeting-profile.json +29 -0
  73. package/packs/product-profile.json +30 -0
  74. package/packs/project-management-pack.json +49 -0
  75. package/packs/recipe-profile.json +29 -0
  76. package/packs/task-views-pack.json +39 -0
  77. package/retired.yaml +136 -0
  78. package/skills/app-factory-build-slice/SKILL.md +70 -0
  79. package/skills/app-factory-build-slice.catalog.json +17 -0
  80. package/skills/app-factory-design-canvas/SKILL.md +96 -0
  81. package/skills/app-factory-design-canvas.catalog.json +17 -0
  82. package/skills/app-factory-design-review/SKILL.md +99 -0
  83. package/skills/app-factory-design-review.catalog.json +18 -0
  84. package/skills/app-factory-research/SKILL.md +123 -0
  85. package/skills/app-factory-research.catalog.json +17 -0
  86. package/skills/bookmark-enrichment/SKILL.md +40 -0
  87. package/skills/carousel-format/carousel-format.skill.json +18 -0
  88. package/skills/collage-broll/collage-broll.skill.json +20 -0
  89. package/skills/content-taste/content-taste.skill.json +18 -0
  90. package/skills/creative-director/creative-director.skill.json +18 -0
  91. package/skills/design-critique/SKILL.md +135 -0
  92. package/skills/design-critique.catalog.json +12 -0
  93. package/skills/designing-with-real-components/SKILL.md +140 -0
  94. package/skills/designing-with-real-components.catalog.json +12 -0
  95. package/skills/edit-loop/edit-loop.skill.json +25 -0
  96. package/skills/facecam-edit/facecam-edit.skill.json +24 -0
  97. package/skills/mapping-the-server/SKILL.md +54 -0
  98. package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
  99. package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
  100. package/skills/open-design-edit/open-design-edit.skill.json +24 -0
  101. package/skills/reading-client-progress/SKILL.md +65 -0
  102. package/skills/routing-to-channels/SKILL.md +54 -0
  103. package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
  104. package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
  105. package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
  106. package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
  107. package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
  108. package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
  109. package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
  110. package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
  111. package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
  112. package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
  113. package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
  114. package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
  115. package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
  116. package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
  117. package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
  118. package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
  119. package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
  120. package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
  121. package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
  122. package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
  123. package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
  124. package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
  125. package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
  126. package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
  127. package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
  128. package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
  129. package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
  130. package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
  131. package/skills/video-direction/video-direction.skill.json +20 -0
  132. package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
  133. package/skills/video-toolbox/video-toolbox.skill.json +20 -0
  134. package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
  135. package/src/agency-os.yaml +122 -0
  136. package/src/agent-os-skill-slugs.test.ts +65 -0
  137. package/src/apify-social-reader.test.ts +317 -0
  138. package/src/base.template.test.ts +25 -19
  139. package/src/base.yaml +95 -39
  140. package/src/blockchain-ecosystem.yaml +1 -0
  141. package/src/brand-library.yaml +525 -14
  142. package/src/builder-dev-fold.template.test.ts +113 -0
  143. package/src/builder.yaml +798 -92
  144. package/src/business-model-catalog-description.test.ts +47 -0
  145. package/src/business-model-grp.template.test.ts +214 -0
  146. package/src/business-model.yaml +598 -0
  147. package/src/catalog-safety.test.ts +198 -0
  148. package/src/catalog.template-items.test.ts +75 -0
  149. package/src/catalog.ts +85 -2
  150. package/src/civic-france.yaml +102 -0
  151. package/src/concept-guard.test.ts +28 -8
  152. package/src/concept-guard.ts +2 -2
  153. package/src/content-creation.template.test.ts +52 -0
  154. package/src/content-creation.yaml +192 -0
  155. package/src/content-os.edit-loop.template.test.ts +210 -0
  156. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  157. package/src/content-os.template.test.ts +322 -0
  158. package/src/content-os.video-takes.template.test.ts +338 -0
  159. package/src/content-os.work-a-take.template.test.ts +125 -0
  160. package/src/content-os.yaml +1830 -39
  161. package/src/content-studio-retired.template.test.ts +82 -0
  162. package/src/corpus-flow-validity.test.ts +4 -5
  163. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  164. package/src/crm-build-plan.template.test.ts +128 -0
  165. package/src/crm-lists.template.test.ts +96 -0
  166. package/src/crm-messages-cadence.template.test.ts +164 -0
  167. package/src/crm-one-send-door.template.test.ts +65 -0
  168. package/src/crm.yaml +1456 -181
  169. package/src/decisions-wave.template.test.ts +186 -0
  170. package/src/define.ts +130 -17
  171. package/src/design-to-implementation.template.test.ts +94 -0
  172. package/src/dist-freshness.test.ts +97 -0
  173. package/src/domain-template-type.test.ts +75 -0
  174. package/src/ecosystem.yaml +27 -66
  175. package/src/enterprise-os.yaml +10 -134
  176. package/src/facecam-edit.audio-studio.test.ts +188 -0
  177. package/src/fal-generate-image.test.ts +200 -0
  178. package/src/fal-media-lab-pod-files.test.ts +180 -0
  179. package/src/finance.yaml +254 -1
  180. package/src/foundation.yaml +130 -28
  181. package/src/frame-host-modules.d.mts +6 -0
  182. package/src/frame-host-modules.mjs +58 -0
  183. package/src/generate.mjs +9 -3
  184. package/src/github-merge.test.ts +210 -0
  185. package/src/grants.yaml +57 -17
  186. package/src/hr.yaml +39 -6
  187. package/src/index.ts +36 -1
  188. package/src/intent.test.ts +161 -0
  189. package/src/internal-runbook.yaml +19 -1
  190. package/src/jev.test.ts +356 -0
  191. package/src/legal.yaml +33 -1
  192. package/src/life-os.yaml +65 -240
  193. package/src/lifecycle-coverage.test.ts +124 -0
  194. package/src/lifecycle-declarations.test.ts +252 -0
  195. package/src/lifecycle-presets.test.ts +468 -0
  196. package/src/lifecycle-presets.ts +494 -0
  197. package/src/lifecycle-stuck.test.ts +46 -0
  198. package/src/lifecycle-waits.parity.test.ts +319 -0
  199. package/src/marketing.yaml +108 -4
  200. package/src/meetings-knowledge.yaml +18 -0
  201. package/src/networking.yaml +23 -2
  202. package/src/operations.yaml +304 -4
  203. package/src/pipeline-views.template.test.ts +88 -0
  204. package/src/playbook-goal-placeholders.test.ts +57 -0
  205. package/src/postiz-contract.test.ts +134 -0
  206. package/src/presentation-declarations.test.ts +174 -0
  207. package/src/process-contract.validate.test.ts +352 -0
  208. package/src/project-management.yaml +39 -740
  209. package/src/publish-body.d.mts +29 -0
  210. package/src/publish-body.mjs +74 -0
  211. package/src/publish-body.test.ts +74 -0
  212. package/src/publish-official-lib.test.ts +176 -0
  213. package/src/replicate-rvm.test.ts +504 -0
  214. package/src/research-base.yaml +119 -20
  215. package/src/research-decision.yaml +60 -3
  216. package/src/reserved-profile-slugs.test.ts +83 -0
  217. package/src/retired-goal-kinds.test.ts +58 -0
  218. package/src/role-reference-and-step-domain.test.ts +192 -0
  219. package/src/seed-obligation.test.ts +176 -0
  220. package/src/send-message-maps.test.ts +227 -0
  221. package/src/shared-role-ssot.test.ts +42 -1
  222. package/src/ship-a-change.template.test.ts +177 -0
  223. package/src/skills.test.ts +170 -0
  224. package/src/social.yaml +85 -27
  225. package/src/space-packages.freshness.test.ts +97 -0
  226. package/src/space-packages.generated.ts +40 -0
  227. package/src/space-packages.test.ts +125 -0
  228. package/src/space-packages.ts +73 -0
  229. package/src/start.test.ts +188 -0
  230. package/src/task-intents.test.ts +293 -0
  231. package/src/templates.ts +21639 -21522
  232. package/src/types.ts +443 -60
  233. package/src/unipile-publish-post.test.ts +162 -0
  234. package/src/validate-flow.drift.test.ts +2 -3
  235. package/src/validate-flow.ts +32 -2
  236. package/src/validate-onboarding.test.ts +59 -0
  237. package/src/validate.test.ts +156 -3
  238. package/src/validate.ts +1384 -7
  239. package/src/wire-core.ts +231 -0
  240. package/src/wire.test.ts +159 -0
  241. package/src/wire.ts +107 -0
  242. package/src/__fixtures__/report-automation-flow.json +0 -393
  243. package/src/agent-fleet.yaml +0 -1645
  244. package/src/business-developer.template.test.ts +0 -351
  245. package/src/business-developer.yaml +0 -860
  246. package/src/content-studio.yaml +0 -361
  247. package/src/dev-dashboard.yaml +0 -990
  248. package/src/outreach-comms.yaml +0 -486
  249. package/src/the-arch.yaml +0 -201
@@ -7,15 +7,18 @@
7
7
  meta:
8
8
  slug: foundation
9
9
  domain: strategy
10
- name: Foundation
10
+ name: Strategy
11
11
  description: The strategic DNA — mission, audience, positioning, and principles — that every workspace inherits.
12
12
  icon: compass
13
13
  color: "#4F46E5"
14
14
  tags: [strategy, mission, positioning, audience]
15
15
  isPublic: true
16
+ # A LIBRARY: it produces nothing a home counts (homes).
17
+ intent:
18
+ label: Strategy
16
19
 
17
20
  workspace:
18
- name: Foundation
21
+ name: Strategy
19
22
  description: The company's strategic DNA — mission, vision, audiences, pains, positioning, and operating principles that downstream workspaces inherit.
20
23
  proposalId: foundation-v1
21
24
  subtype: foundation
@@ -246,8 +249,9 @@ profiles:
246
249
  # every consumer must treat absence as "no commercial axis" rather than an
247
250
  # empty required field. Deliberately thin: what it is, whether it is live, how
248
251
  # it is priced, and the two identity rows it hangs off. Money AMOUNTS live on
249
- # the deal/contract that references the offer, not here — an offer's price is a
250
- # model, not a number.
252
+ # the deal/contract that references the offer, not here. The offer carries only
253
+ # a "starting at" list price (offer-list-price + currency + billing) so it can
254
+ # be advertised; the deal stays authoritative for what is actually paid.
251
255
  # ═══════════════════════════════════════════════════════════════════════════
252
256
  - slug: offer
253
257
  displayName: Offer
@@ -283,6 +287,30 @@ profiles:
283
287
  - usage-based
284
288
  - free
285
289
  - other
290
+ # "Starting at" list price. The deal/contract stays authoritative for what
291
+ # a buyer actually pays; these three only answer "what do we advertise".
292
+ - slug: offer-list-price
293
+ label: Starting at
294
+ valueType: number
295
+ inputType: number
296
+ placeholder: "List price, before any negotiation"
297
+ - slug: offer-currency
298
+ label: Currency
299
+ valueType: string
300
+ inputType: select
301
+ enumValues:
302
+ - EUR
303
+ - USD
304
+ - GBP
305
+ - CHF
306
+ constraints: { defaultValue: EUR }
307
+ - slug: offer-billing
308
+ label: Billing
309
+ valueType: string
310
+ inputType: select
311
+ enumValues:
312
+ - one-off
313
+ - monthly
286
314
  - slug: offer-audience
287
315
  label: Audience
288
316
  valueType: entity_id
@@ -324,17 +352,17 @@ profiles:
324
352
  applicableKinds:
325
353
  - company
326
354
  - person
355
+ # The ONE lifecycle (work model L1): the relationship base as it stands
356
+ # (prospect · active · paused · former · churned; lifecycle-presets.ts).
357
+ # The compiler writes the options and the select's enumValues.
358
+ lifecycle:
359
+ property: clientStatus
360
+ adopts: relationship
327
361
  properties:
328
362
  - slug: clientStatus
329
363
  label: Status
330
364
  valueType: string
331
365
  inputType: select
332
- enumValues:
333
- - prospect
334
- - active
335
- - paused
336
- - former
337
- - churned
338
366
  - slug: accountManager
339
367
  label: Account Manager
340
368
  valueType: string
@@ -379,6 +407,17 @@ profiles:
379
407
  label: Drive folder ID
380
408
  valueType: string
381
409
  inputType: text
410
+ # A state is never a role: an ended relationship is `clientStatus:
411
+ # churned` on THIS facet, not a separate `churned` role (retired
412
+ # 2026-10-11, work model W12/W13). Its two fields moved here with it.
413
+ - slug: churnedDate
414
+ label: Churned Date
415
+ valueType: date
416
+ inputType: date
417
+ - slug: churnReason
418
+ label: Churn reason
419
+ valueType: string
420
+ inputType: textarea
382
421
 
383
422
  # ── Partner (referral source + co-delivery collaborator) ──────────────────
384
423
  - slug: partner
@@ -486,47 +525,107 @@ profiles:
486
525
  applicableKinds:
487
526
  - company
488
527
  - person
528
+ # The ONE lifecycle (work model L1): every option's category. `converted`
529
+ # is a person's move (CRM build plan A4, 2026-10-22): it happens when a
530
+ # person records a booked meeting, which opens the deal; no agent sets it.
531
+ lifecycle:
532
+ property: leadStage
533
+ options:
534
+ - { key: new, category: open }
535
+ - { key: researching, category: open }
536
+ - { key: outreach, category: open }
537
+ - { key: engaged, category: active }
538
+ - { key: qualified, category: active }
539
+ - { key: nurturing, category: active }
540
+ - { key: converted, category: done, humanOnly: true }
541
+ - { key: disqualified, category: lost, humanOnly: true }
489
542
  properties:
543
+ # The lead role's ONE lifecycle (work model W15, 2026-10-11):
544
+ # qualification only. `converted` = a person recorded a booked meeting
545
+ # and a deal was opened (the sale then lives on the deal's commercialStage);
546
+ # `disqualified` = dropped. Business Developer's `lifecycleStage` overlay
547
+ # folded into this; old `prospect` → new, `lost` → disqualified
548
+ # (conversions `w12.lead.*`).
490
549
  - slug: leadStage
491
550
  label: Stage
492
551
  valueType: string
493
552
  inputType: select
494
553
  enumValues:
495
- - prospect
554
+ - new
555
+ - researching
556
+ - outreach
557
+ - engaged
496
558
  - qualified
497
559
  - nurturing
498
560
  - converted
499
- - lost
500
- constraints: { defaultValue: prospect }
561
+ - disqualified
562
+ constraints: { defaultValue: new }
501
563
  - slug: notes
502
564
  label: Notes
503
565
  valueType: string
504
566
  inputType: textarea
505
567
 
506
- # ── Churned (formerly-active relationship that ended) ──────────────────────
507
- - slug: churned
508
- displayName: Churned
509
- icon: user-x
510
- color: gray
511
- description: A relationship (client, partner, or otherwise) that was active and has since ended.
568
+ # ── Roles the BUSINESS MODEL needs, and why they live here ─────────────────
569
+ # `porteur` and `funder` are hats, not kinds: the same person entity is a
570
+ # porteur of one venture and a funder of another, and the hat is revocable
571
+ # while the person is not. They are declared HERE because foundation.yaml is
572
+ # the shared-role SSOT (`shared-role-ssot.test.ts` derives the guarded
573
+ # vocabulary from this file) — declaring them in business-model.yaml instead
574
+ # would fork a second identity for a hat the whole pod refers to.
575
+ - slug: porteur
576
+ displayName: Porteur
577
+ icon: user-check
578
+ color: amber
579
+ description: The person carrying a venture — the one whose commitment, skills, and constraints the model actually rests on. Distinct from `client` (who pays) and `partner` (who collaborates).
512
580
  scope: shared
513
581
  profileKind: role
514
582
  applicableKinds:
515
- - company
516
583
  - person
517
584
  properties:
518
- - slug: churnedDate
519
- label: Churned Date
520
- valueType: date
521
- inputType: date
522
- - slug: churnReason
523
- label: Reason
585
+ - slug: brings
586
+ label: What They Bring
524
587
  valueType: string
525
588
  inputType: textarea
526
- - slug: notes
527
- label: Notes
589
+ description: The specific competence, network, capital, or legitimacy this person contributes that the model depends on.
590
+ - slug: commitmentLevel
591
+ label: Commitment
592
+ valueType: string
593
+ inputType: select
594
+ enumValues: [full-time, part-time, advisory, intermittent]
595
+ - slug: constraints
596
+ label: Constraints
597
+ valueType: string
598
+ inputType: textarea
599
+ description: What limits this porteur — time, cash runway, geography, non-compete, family. A model that ignores these is fiction.
600
+ - slug: funder
601
+ displayName: Funder
602
+ icon: banknote
603
+ color: green
604
+ description: A source of non-revenue money or subsidy — a bank, a public scheme, an investor, a grantor. A hat worn by a company or a person, never a kind of its own.
605
+ scope: shared
606
+ profileKind: role
607
+ applicableKinds:
608
+ - company
609
+ - person
610
+ properties:
611
+ - slug: fundingType
612
+ label: Funding Type
613
+ valueType: string
614
+ inputType: select
615
+ enumValues: [grant, subsidy, loan, equity, guarantee, prize, other]
616
+ - slug: amountCommitted
617
+ label: Amount Committed
618
+ valueType: number
619
+ inputType: number
620
+ - slug: eligibilityConditions
621
+ label: Eligibility Conditions
528
622
  valueType: string
529
623
  inputType: textarea
624
+ description: The conditions the venture must satisfy to keep this money. These are the constraints most often discovered too late.
625
+ - slug: decisionDate
626
+ label: Decision Date
627
+ valueType: date
628
+ inputType: date
530
629
 
531
630
  views:
532
631
  - name: Mission & Vision
@@ -629,10 +728,12 @@ onboarding:
629
728
  alternatives — in one breath, with no fluff.
630
729
  collect:
631
730
  - profileSlug: mission
731
+ min: 1
632
732
  what: The one mission that anchors the company — what we serve, what we change, why now.
633
733
  cardinality: one
634
734
  keyFields: [statement, time-horizon]
635
735
  - profileSlug: audience
736
+ min: 1
636
737
  what: The ideal customer profiles the company is built for.
637
738
  cardinality: few
638
739
  keyFields: [segment, jobs-to-be-done]
@@ -641,6 +742,7 @@ onboarding:
641
742
  cardinality: several
642
743
  keyFields: [description, severity]
643
744
  - profileSlug: positioning
745
+ min: 1
644
746
  what: The market category, who we stand against, and why we win.
645
747
  cardinality: one
646
748
  keyFields: [category, against, because]
@@ -0,0 +1,6 @@
1
+ /** Types for `frame-host-modules.mjs` — the modules a space package's frame gets from its host. */
2
+ export declare const FRAME_HOST_MODULES: readonly string[];
3
+ export declare const FRAME_HOST_MODULE_RE: RegExp;
4
+ export declare function hostRequiresIn(code: string): string[];
5
+ export declare const FRAME_KIT_MODULE: string;
6
+ export declare function kitImportsIn(code: string): string[];
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The modules a space package's frame gets from its HOST, never from its
3
+ * bundle (`@synap-core/web/cell-runtime` frame-import-map.ts): one React, its
4
+ * JSX runtimes, one react-dom, the view SDK, and the shared kit
5
+ * (`@synap/kit`, built from `packages/web/src/space-packages/kit.ts`). Shared
6
+ * by the build (`scripts/build-space-packages.mjs`, which keeps them external
7
+ * and builds the React modules and the kit as the host's frame modules) and
8
+ * the freshness test (which re-checks the committed bundle).
9
+ */
10
+ export const FRAME_HOST_MODULES = [
11
+ 'react',
12
+ 'react/jsx-runtime',
13
+ 'react/jsx-dev-runtime',
14
+ 'react-dom',
15
+ 'react-dom/client',
16
+ '@synap/view-sdk',
17
+ '@synap/kit',
18
+ ];
19
+
20
+ /** The kit module's specifier (a package reaches the shared layers only through it). */
21
+ export const FRAME_KIT_MODULE = '@synap/kit';
22
+
23
+ /**
24
+ * Every name a bundle imports from the kit: `import{A as b,C}from"@synap/kit"`
25
+ * → `['A', 'C']`. A namespace or default import of the kit is returned as
26
+ * `*` / `default` so the build can refuse it (its names cannot be checked).
27
+ */
28
+ export function kitImportsIn(code) {
29
+ const names = new Set();
30
+ for (const m of code.matchAll(/import\s*([^;'"]*?)\s*from\s*["']@synap\/kit["']/g)) {
31
+ const clause = m[1];
32
+ const braces = /\{([^}]*)\}/.exec(clause);
33
+ for (const part of braces ? braces[1].split(',') : []) {
34
+ const name = part.trim().split(/\s+as\s+/)[0];
35
+ if (name) names.add(name);
36
+ }
37
+ const head = clause.replace(/\{[^}]*\}/, '').replace(/,/g, ' ').trim();
38
+ if (/^\*\s*as\s+[\w$]+$/.test(head)) names.add('*');
39
+ else if (/^[\w$]+$/.test(head)) names.add('default');
40
+ }
41
+ return [...names].sort();
42
+ }
43
+
44
+ const NAMES = FRAME_HOST_MODULES.map((m) => m.replace(/[/.]/g, '\\$&')).join('|');
45
+
46
+ /** Matches a host module name exactly (an esbuild `onResolve` filter). */
47
+ export const FRAME_HOST_MODULE_RE = new RegExp(`^(${NAMES})$`);
48
+
49
+ /**
50
+ * Every `require()` of a frame host module left in a bundle: a call to
51
+ * esbuild's `__require` shim, e.g. `jI("react")`. Each one throws in the frame
52
+ * ("Dynamic require of "react" is not supported"), so the module never links.
53
+ * `import("react")` and method calls (`x.f("react")`) are not requires.
54
+ */
55
+ export function hostRequiresIn(code) {
56
+ const call = new RegExp(`(?<![\\w$.])(?!import\\()[A-Za-z_$][\\w$]*\\(\\s*["'](${NAMES})["']\\s*\\)`, 'g');
57
+ return [...code.matchAll(call)].map((m) => m[0]);
58
+ }
package/src/generate.mjs CHANGED
@@ -23,20 +23,26 @@ import { readFileSync, readdirSync } from 'fs';
23
23
  import { dirname, join } from 'path';
24
24
  import { fileURLToPath } from 'url';
25
25
  import yaml from 'js-yaml';
26
+ // `.ts` on purpose: this file runs under Node's type stripping (build.mjs) and
27
+ // vitest; lifecycle-presets.ts has only `import type`.
28
+ import { resolveLifecyclePresets } from './lifecycle-presets.ts';
26
29
 
27
30
  export const SRC_DIR = dirname(fileURLToPath(import.meta.url));
28
31
  export const TEMPLATES_FILE = join(SRC_DIR, 'templates.ts');
29
32
 
30
33
  /**
31
- * Read + parse every `src/*.yaml`, in the same (readdir) order build.mjs used.
32
- * No validation here — validation stays in build.mjs, which is the writer.
34
+ * Read + parse every `src/*.yaml`, in the same (readdir) order build.mjs used,
35
+ * and compile its lifecycle presets (`adopts:` → flat options, generated
36
+ * `enumValues`; see lifecycle-presets.ts). No validation here — validation
37
+ * stays in build.mjs, which is the writer; an overlay that cannot be resolved
38
+ * is left as authored and `validateTemplate` reports it.
33
39
  */
34
40
  export function readYamlTemplates() {
35
41
  return readdirSync(SRC_DIR)
36
42
  .filter((f) => f.endsWith('.yaml'))
37
43
  .map((file) => {
38
44
  const parsed = yaml.load(readFileSync(join(SRC_DIR, file), 'utf-8'));
39
- return { file, slug: parsed?.meta?.slug, data: parsed };
45
+ return { file, slug: parsed?.meta?.slug, data: resolveLifecyclePresets(parsed).template };
40
46
  });
41
47
  }
42
48
 
@@ -0,0 +1,210 @@
1
+ /**
2
+ * github-merge: a ship step as a governed tool verb (decision 4d6d7922: the pod
3
+ * holds the ship credential, an approval is a typed action pinned to a head,
4
+ * GitHub's own checks gate it).
5
+ *
6
+ * SEAM: the verb's REAL `code` body, run the way the pod's executor wraps it
7
+ * (`async (args, context) => { <code> }` with `callProvider` in scope), after
8
+ * the install-time values are put in its `{{repos}}` / `{{requiredChecks}}`
9
+ * the way the install templating does, against a recorded `callProvider`.
10
+ *
11
+ * NOT covered: the pod's own path (the approval, the vault, the sandbox) and
12
+ * GitHub itself (not exercised against a live repository: the proof merge in
13
+ * session 2d2a3473 does that).
14
+ */
15
+ import { readFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { describe, expect, it } from "vitest";
18
+
19
+ const capability = JSON.parse(
20
+ readFileSync(join(import.meta.dirname, "..", "capabilities", "github-merge.capability.json"), "utf8"),
21
+ ) as { spec: Record<string, any> };
22
+ const verb = capability.spec.skills.find((s: { name: string }) => s.name === "github_merge_pr");
23
+
24
+ const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor as new (
25
+ ...args: string[]
26
+ ) => (...a: unknown[]) => Promise<unknown>;
27
+
28
+ const SHA = "a524c103d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6";
29
+ const ARGS = { repo: "Synap-core/app-bases", pr: 12, expectedHeadSha: SHA, base: "main", method: "squash" };
30
+ const PR_PATH = "/repos/Synap-core/app-bases/pulls/12";
31
+
32
+ type Reply = { status: number; body?: unknown };
33
+ type Call = { tool: string; method: string; path: string; body?: unknown };
34
+
35
+ function github(over: Partial<Record<string, Reply>> = {}): Record<string, Reply> {
36
+ return {
37
+ [`GET ${PR_PATH}`]: {
38
+ status: 200,
39
+ body: { state: "open", merged: false, draft: false, head: { sha: SHA }, base: { ref: "main" }, html_url: "https://github.com/Synap-core/app-bases/pull/12" },
40
+ },
41
+ [`GET /repos/Synap-core/app-bases/commits/${SHA}/check-runs?per_page=100`]: { status: 200, body: { check_runs: [] } },
42
+ [`GET /repos/Synap-core/app-bases/commits/${SHA}/status`]: {
43
+ status: 200,
44
+ body: { statuses: [{ context: "synap/devbox", state: "success" }] },
45
+ },
46
+ [`PUT ${PR_PATH}/merge`]: { status: 200, body: { merged: true, sha: "f00dfeed00000000000000000000000000000000" } },
47
+ ...over,
48
+ } as Record<string, Reply>;
49
+ }
50
+
51
+ async function run(
52
+ args: Record<string, unknown>,
53
+ replies: Record<string, Reply> = github(),
54
+ install: { repos?: string; requiredChecks?: string } = {},
55
+ ) {
56
+ const calls: Call[] = [];
57
+ const callProvider = async (tool: string, method: string, path: string, body?: unknown) => {
58
+ calls.push({ tool, method, path, ...(body === undefined ? {} : { body }) });
59
+ const r = replies[`${method} ${path}`];
60
+ if (!r) throw new Error(`unexpected call ${method} ${path}`);
61
+ return r;
62
+ };
63
+ const code = String(verb.code)
64
+ .replaceAll("{{repos}}", install.repos ?? "Synap-core/app-bases, https://github.com/Synap-core/browser")
65
+ .replaceAll("{{requiredChecks}}", install.requiredChecks ?? "synap/devbox");
66
+ const body = new AsyncFunction("args", "context", "callProvider", "propose", code);
67
+ try {
68
+ return { result: await body(args, {}, callProvider, {}), calls, error: null as Error | null };
69
+ } catch (error) {
70
+ return { result: null, calls, error: error as Error };
71
+ }
72
+ }
73
+ const merged = (calls: Call[]) => calls.some((c) => c.method === "PUT");
74
+
75
+ describe("github-merge — the manifest", () => {
76
+ it("one write verb on its own tool, its own token, never pod-wide, separate from github-read", () => {
77
+ expect(capability.spec.key).toBe("github-merge");
78
+ expect(capability.spec.skills.map((s: { name: string }) => s.name)).toEqual(["github_merge_pr"]);
79
+ expect(verb.kind).toBe("code");
80
+ expect(verb.requires).toEqual(["github_merge_api"]);
81
+ expect(verb.metadata).toEqual({ verbType: "write" });
82
+ expect(verb.parameters).toEqual({ repo: "string", pr: "number", expectedHeadSha: "string", base: "string", method: "string?" });
83
+ expect(capability.spec.tools.map((t: { name: string; credentialRef: string }) => [t.name, t.credentialRef])).toEqual([
84
+ ["github_merge_api", "githubMergeTokenSecret"],
85
+ ]);
86
+ expect(capability.spec.vault.filter((v: { podWide?: boolean }) => v.podWide)).toEqual([]);
87
+ // github-read keeps its promise: it holds no write verb and no write tool.
88
+ const read = JSON.parse(readFileSync(join(import.meta.dirname, "..", "capabilities", "github-read.capability.json"), "utf8")).spec;
89
+ expect(read.skills.map((s: { name: string }) => s.name)).not.toContain("github_merge_pr");
90
+ // Every call goes to its own tool.
91
+ const tools = [...String(verb.code).matchAll(/callProvider\(\s*'([^']+)'/g)].map((m) => m[1]);
92
+ expect(tools.length).toBeGreaterThanOrEqual(2);
93
+ expect(new Set(tools)).toEqual(new Set(["github_merge_api"]));
94
+ });
95
+ });
96
+
97
+ describe("github_merge_pr — merges only the approved head, with its checks green", () => {
98
+ it("reads the PR and its checks, then merges pinned to the approved sha", async () => {
99
+ const { result, calls, error } = await run(ARGS);
100
+ expect(error).toBeNull();
101
+ expect(calls.map((c) => `${c.method} ${c.path}`)).toEqual([
102
+ `GET ${PR_PATH}`,
103
+ `GET /repos/Synap-core/app-bases/commits/${SHA}/check-runs?per_page=100`,
104
+ `GET /repos/Synap-core/app-bases/commits/${SHA}/status`,
105
+ `PUT ${PR_PATH}/merge`,
106
+ ]);
107
+ expect(calls.at(-1)!.body).toEqual({ sha: SHA, merge_method: "squash" });
108
+ expect(result).toEqual({
109
+ merged: true,
110
+ mergeCommitSha: "f00dfeed00000000000000000000000000000000",
111
+ url: "https://github.com/Synap-core/app-bases/pull/12",
112
+ detail: "Merged Synap-core/app-bases#12 at a524c103 into main as f00dfeed.",
113
+ });
114
+ });
115
+
116
+ it("a repository not in the install's list is refused before anything is read", async () => {
117
+ const { calls, error } = await run({ ...ARGS, repo: "Synap-core/backend" });
118
+ expect(error?.message).toMatch(/Synap-core\/backend is not in this pod's merge list/);
119
+ expect(calls).toEqual([]);
120
+ });
121
+
122
+ it("a head that moved since the approval is refused, never merged", async () => {
123
+ const replies = github({
124
+ [`GET ${PR_PATH}`]: { status: 200, body: { state: "open", head: { sha: "b".repeat(40) }, base: { ref: "main" } } },
125
+ });
126
+ const { calls, error } = await run(ARGS, replies);
127
+ expect(error?.message).toMatch(/moved since it was approved: approved a524c103, head is now bbbbbbbb/);
128
+ expect(merged(calls)).toBe(false);
129
+ });
130
+
131
+ it("a required check missing, pending or failed refuses; so does any failed check", async () => {
132
+ const statusPath = `GET /repos/Synap-core/app-bases/commits/${SHA}/status`;
133
+ const runsPath = `GET /repos/Synap-core/app-bases/commits/${SHA}/check-runs?per_page=100`;
134
+ const cases: Array<[Record<string, Reply>, RegExp]> = [
135
+ [{ [statusPath]: { status: 200, body: { statuses: [] } } }, /missing: synap\/devbox/],
136
+ [{ [statusPath]: { status: 200, body: { statuses: [{ context: "synap/devbox", state: "pending" }] } } }, /not green: synap\/devbox \(pending\)/],
137
+ [{ [statusPath]: { status: 200, body: { statuses: [{ context: "synap/devbox", state: "failure" }] } } }, /not green: synap\/devbox \(failed\)/],
138
+ [
139
+ { [runsPath]: { status: 200, body: { check_runs: [{ id: 1, name: "lint", status: "completed", conclusion: "failure" }] } } },
140
+ /failed: lint/,
141
+ ],
142
+ ];
143
+ for (const [over, reason] of cases) {
144
+ const { calls, error } = await run(ARGS, github(over));
145
+ expect(error?.message).toMatch(reason);
146
+ expect(merged(calls)).toBe(false);
147
+ }
148
+ });
149
+
150
+ it("a check that failed then passed on a re-run reads by its latest run", async () => {
151
+ const runsPath = `GET /repos/Synap-core/app-bases/commits/${SHA}/check-runs?per_page=100`;
152
+ const replies = github({
153
+ [runsPath]: {
154
+ status: 200,
155
+ body: {
156
+ check_runs: [
157
+ { id: 9, name: "lint", status: "completed", conclusion: "success" },
158
+ { id: 3, name: "lint", status: "completed", conclusion: "failure" },
159
+ ],
160
+ },
161
+ },
162
+ });
163
+ const { error, calls } = await run(ARGS, replies);
164
+ expect(error).toBeNull();
165
+ expect(merged(calls)).toBe(true);
166
+ });
167
+
168
+ it("no required check configured refuses: nothing would vouch for the head", async () => {
169
+ const { calls, error } = await run(ARGS, github(), { requiredChecks: " , " });
170
+ expect(error?.message).toMatch(/no required checks are configured/);
171
+ expect(calls).toEqual([]);
172
+ });
173
+
174
+ it("a PR that is merged, closed, a draft or targets another base is refused", async () => {
175
+ const pr = (body: Record<string, unknown>) =>
176
+ github({ [`GET ${PR_PATH}`]: { status: 200, body: { state: "open", head: { sha: SHA }, base: { ref: "main" }, ...body } } });
177
+ for (const [body, reason] of [
178
+ [{ merged: true }, /already merged/],
179
+ [{ state: "closed" }, /is closed/],
180
+ [{ draft: true }, /is a draft/],
181
+ [{ base: { ref: "release" } }, /targets release, not main/],
182
+ ] as Array<[Record<string, unknown>, RegExp]>) {
183
+ const { calls, error } = await run(ARGS, pr(body));
184
+ expect(error?.message).toMatch(reason);
185
+ expect(merged(calls)).toBe(false);
186
+ }
187
+ });
188
+
189
+ it("arguments that cannot pin the act are refused before any call", async () => {
190
+ for (const bad of [
191
+ { ...ARGS, expectedHeadSha: "a524c103" },
192
+ { ...ARGS, pr: 0 },
193
+ { ...ARGS, base: "" },
194
+ { ...ARGS, method: "force" },
195
+ { ...ARGS, repo: "app-bases" },
196
+ ]) {
197
+ const { calls, error } = await run(bad);
198
+ expect(error, JSON.stringify(bad)).not.toBeNull();
199
+ expect(calls).toEqual([]);
200
+ }
201
+ });
202
+
203
+ it("a failed read or a merge GitHub refuses throws: nothing reads as merged", async () => {
204
+ const unread = await run(ARGS, github({ [`GET ${PR_PATH}`]: { status: 404, body: {} } }));
205
+ expect(unread.error?.message).toMatch(/HTTP 404/);
206
+ expect(merged(unread.calls)).toBe(false);
207
+ const refused = await run(ARGS, github({ [`PUT ${PR_PATH}/merge`]: { status: 409, body: { message: "Head branch was modified" } } }));
208
+ expect(refused.error?.message).toMatch(/refused to merge .*HTTP 409: Head branch was modified/);
209
+ });
210
+ });