@synap-core/workspace-templates 0.12.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 (245) 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 +174 -900
  57. package/dist/index.js +26148 -23931
  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 +27 -4
  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.yaml +85 -39
  139. package/src/blockchain-ecosystem.yaml +1 -0
  140. package/src/brand-library.yaml +525 -14
  141. package/src/builder-dev-fold.template.test.ts +113 -0
  142. package/src/builder.yaml +779 -91
  143. package/src/business-model-catalog-description.test.ts +47 -0
  144. package/src/business-model-grp.template.test.ts +214 -0
  145. package/src/business-model.yaml +296 -49
  146. package/src/catalog-safety.test.ts +198 -0
  147. package/src/catalog.template-items.test.ts +75 -0
  148. package/src/catalog.ts +66 -2
  149. package/src/concept-guard.test.ts +28 -8
  150. package/src/concept-guard.ts +2 -2
  151. package/src/content-creation.template.test.ts +52 -0
  152. package/src/content-creation.yaml +192 -0
  153. package/src/content-os.edit-loop.template.test.ts +210 -0
  154. package/src/content-os.publish-due-posts.template.test.ts +261 -0
  155. package/src/content-os.template.test.ts +322 -0
  156. package/src/content-os.video-takes.template.test.ts +338 -0
  157. package/src/content-os.work-a-take.template.test.ts +125 -0
  158. package/src/content-os.yaml +1830 -39
  159. package/src/content-studio-retired.template.test.ts +82 -0
  160. package/src/corpus-flow-validity.test.ts +4 -5
  161. package/src/crm-absorbs-outreach.template.test.ts +117 -0
  162. package/src/crm-build-plan.template.test.ts +128 -0
  163. package/src/crm-lists.template.test.ts +96 -0
  164. package/src/crm-messages-cadence.template.test.ts +164 -0
  165. package/src/crm-one-send-door.template.test.ts +65 -0
  166. package/src/crm.yaml +1456 -181
  167. package/src/decisions-wave.template.test.ts +186 -0
  168. package/src/define.ts +130 -17
  169. package/src/design-to-implementation.template.test.ts +94 -0
  170. package/src/domain-template-type.test.ts +75 -0
  171. package/src/ecosystem.yaml +27 -66
  172. package/src/enterprise-os.yaml +10 -134
  173. package/src/facecam-edit.audio-studio.test.ts +188 -0
  174. package/src/fal-generate-image.test.ts +200 -0
  175. package/src/fal-media-lab-pod-files.test.ts +180 -0
  176. package/src/finance.yaml +254 -1
  177. package/src/foundation.yaml +78 -38
  178. package/src/frame-host-modules.d.mts +6 -0
  179. package/src/frame-host-modules.mjs +58 -0
  180. package/src/generate.mjs +9 -3
  181. package/src/github-merge.test.ts +210 -0
  182. package/src/grants.yaml +46 -17
  183. package/src/hr.yaml +39 -6
  184. package/src/index.ts +36 -1
  185. package/src/intent.test.ts +161 -0
  186. package/src/internal-runbook.yaml +19 -1
  187. package/src/jev.test.ts +356 -0
  188. package/src/legal.yaml +33 -1
  189. package/src/life-os.yaml +51 -240
  190. package/src/lifecycle-coverage.test.ts +124 -0
  191. package/src/lifecycle-declarations.test.ts +252 -0
  192. package/src/lifecycle-presets.test.ts +468 -0
  193. package/src/lifecycle-presets.ts +494 -0
  194. package/src/lifecycle-stuck.test.ts +46 -0
  195. package/src/lifecycle-waits.parity.test.ts +319 -0
  196. package/src/marketing.yaml +108 -4
  197. package/src/meetings-knowledge.yaml +18 -0
  198. package/src/networking.yaml +23 -2
  199. package/src/operations.yaml +304 -4
  200. package/src/pipeline-views.template.test.ts +88 -0
  201. package/src/playbook-goal-placeholders.test.ts +57 -0
  202. package/src/postiz-contract.test.ts +134 -0
  203. package/src/presentation-declarations.test.ts +174 -0
  204. package/src/process-contract.validate.test.ts +352 -0
  205. package/src/project-management.yaml +39 -740
  206. package/src/publish-body.d.mts +29 -0
  207. package/src/publish-body.mjs +74 -0
  208. package/src/publish-body.test.ts +74 -0
  209. package/src/publish-official-lib.test.ts +176 -0
  210. package/src/replicate-rvm.test.ts +504 -0
  211. package/src/research-base.yaml +119 -20
  212. package/src/research-decision.yaml +60 -3
  213. package/src/reserved-profile-slugs.test.ts +83 -0
  214. package/src/retired-goal-kinds.test.ts +58 -0
  215. package/src/role-reference-and-step-domain.test.ts +192 -0
  216. package/src/seed-obligation.test.ts +1 -6
  217. package/src/send-message-maps.test.ts +227 -0
  218. package/src/shared-role-ssot.test.ts +40 -2
  219. package/src/ship-a-change.template.test.ts +177 -0
  220. package/src/skills.test.ts +170 -0
  221. package/src/social.yaml +85 -27
  222. package/src/space-packages.freshness.test.ts +97 -0
  223. package/src/space-packages.generated.ts +40 -0
  224. package/src/space-packages.test.ts +125 -0
  225. package/src/space-packages.ts +73 -0
  226. package/src/start.test.ts +188 -0
  227. package/src/task-intents.test.ts +293 -0
  228. package/src/templates.ts +20851 -21496
  229. package/src/types.ts +443 -60
  230. package/src/unipile-publish-post.test.ts +162 -0
  231. package/src/validate-flow.drift.test.ts +2 -3
  232. package/src/validate-flow.ts +22 -2
  233. package/src/validate-onboarding.test.ts +59 -0
  234. package/src/validate.test.ts +71 -4
  235. package/src/validate.ts +1335 -7
  236. package/src/wire-core.ts +231 -0
  237. package/src/wire.test.ts +159 -0
  238. package/src/wire.ts +107 -0
  239. package/src/agent-fleet.yaml +0 -1645
  240. package/src/business-developer.template.test.ts +0 -351
  241. package/src/business-developer.yaml +0 -860
  242. package/src/content-studio.yaml +0 -361
  243. package/src/dev-dashboard.yaml +0 -990
  244. package/src/outreach-comms.yaml +0 -486
  245. 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,43 +525,41 @@ 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 }
501
- - slug: notes
502
- label: Notes
503
- valueType: string
504
- inputType: textarea
505
-
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.
512
- scope: shared
513
- profileKind: role
514
- applicableKinds:
515
- - company
516
- - person
517
- properties:
518
- - slug: churnedDate
519
- label: Churned Date
520
- valueType: date
521
- inputType: date
522
- - slug: churnReason
523
- label: Reason
524
- valueType: string
525
- inputType: textarea
561
+ - disqualified
562
+ constraints: { defaultValue: new }
526
563
  - slug: notes
527
564
  label: Notes
528
565
  valueType: string
@@ -691,10 +728,12 @@ onboarding:
691
728
  alternatives — in one breath, with no fluff.
692
729
  collect:
693
730
  - profileSlug: mission
731
+ min: 1
694
732
  what: The one mission that anchors the company — what we serve, what we change, why now.
695
733
  cardinality: one
696
734
  keyFields: [statement, time-horizon]
697
735
  - profileSlug: audience
736
+ min: 1
698
737
  what: The ideal customer profiles the company is built for.
699
738
  cardinality: few
700
739
  keyFields: [segment, jobs-to-be-done]
@@ -703,6 +742,7 @@ onboarding:
703
742
  cardinality: several
704
743
  keyFields: [description, severity]
705
744
  - profileSlug: positioning
745
+ min: 1
706
746
  what: The market category, who we stand against, and why we win.
707
747
  cardinality: one
708
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
+ });
package/src/grants.yaml CHANGED
@@ -67,6 +67,14 @@ profiles:
67
67
  color: blue
68
68
  description: A single grant application — one project, one round, one outcome. The pipeline card and analysis unit.
69
69
  scope: WORKSPACE
70
+ # The ONE lifecycle (work model L1): every option's category.
71
+ lifecycle:
72
+ property: stage
73
+ options:
74
+ - { key: scope, category: open }
75
+ - { key: abstract, category: active }
76
+ - { key: build, category: active }
77
+ - { key: awaiting-results, category: active }
70
78
  properties:
71
79
  # Pipeline + workflow state
72
80
  - slug: stage
@@ -693,6 +701,15 @@ profiles:
693
701
  applicableKinds:
694
702
  - person
695
703
  - company
704
+ # The ONE lifecycle (work model L1): every option's category.
705
+ lifecycle:
706
+ property: clientStatus
707
+ options:
708
+ - { key: prospect, category: open }
709
+ - { key: active, category: active }
710
+ - { key: paused, category: paused }
711
+ - { key: former, category: done }
712
+ - { key: churned, category: lost }
696
713
  properties:
697
714
  - slug: clientStatus
698
715
  label: Status
@@ -844,16 +861,12 @@ profiles:
844
861
 
845
862
  views:
846
863
  - name: Pipeline
847
- type: kanban
864
+ # The ONE board (work model L3, D3): columns ARE the submission's declared
865
+ # lifecycle (`stage`), moves go through the one mover.
866
+ type: pipeline
848
867
  scopeProfileSlug: grant_submission
849
868
  config:
850
- groupByField: stage
851
- cardFields:
852
- - summary
853
- - requestedAmount
854
- - awardedAmount
855
- - outcome
856
- - status
869
+ profileSlug: grant_submission
857
870
  - name: Submissions
858
871
  type: table
859
872
  scopeProfileSlug: grant_submission
@@ -983,8 +996,8 @@ entityLinks:
983
996
  # trancheModel, nextDeadline) are READ from the `grantProgram` entity at runtime,
984
997
  # NOT hardcoded per chain — so one playbook serves Stellar SCF, Optimism RetroPGF,
985
998
  # etc. Only a genuinely different-SHAPED program later earns its own override
986
- # playbook (base + data-driven-override decision). Generalizes the Stellar staged
987
- # advisor (CP `stellar-grant-client`) into a template. Bound to `contract` (the
999
+ # playbook (base + data-driven-override decision). Generalizes the retired
1000
+ # Stellar staged advisor capability into a template (it was removed, D5). Bound to `contract` (the
988
1001
  # engagement, per decision — NOT company); targets a `grant-program` param — the
989
1002
  # 3-layer spine (contract → grant-program → round → grant_submission) made runnable.
990
1003
  playbooks:
@@ -1033,13 +1046,16 @@ playbooks:
1033
1046
  # `awarded` stays `started` because delivery follows an award — a rejected
1034
1047
  # pursuit's terminality lives on the submission's own `outcome` field, not
1035
1048
  # here.
1049
+ # Every step is worked where the grant submissions live: grants composes
1050
+ # onto Operations, so the DOMAIN is `operations` (the workspace template
1051
+ # slug the workspace carries — an overlay's own slug is never a domain).
1036
1052
  stages:
1037
- - { key: identify, name: Identify, category: planned, description: "Understand the project and which of the program's submission tracks fit — before any writing.", goal: "A clear project understanding and the fitting submission track; submission stage = scope." }
1038
- - { key: draft, name: Draft, category: started, description: "Write the abstract, then the full proposal, from the client's real material.", goal: "Abstract then full proposal drafted; submission stage abstract → build with body fields filled." }
1039
- - { key: submit, name: Submit, category: started, description: "Finalize against the program deadline and submit the application.", goal: "Application submitted on time; submission stage = awaiting-results." }
1040
- - { key: awaiting, name: Awaiting, category: paused, description: "Track the decision and keep the client status current.", goal: "Submission status reflects where the decision stands (in-review / waiting-client / needs-document / blocked)." }
1041
- - { key: awarded, name: Awarded, category: started, description: "Record the funder's decision and the awarded amount.", goal: "Submission outcome set (awarded / rejected / partial) with the awarded amount." }
1042
- - { key: delivering, name: Delivering, category: started, description: "Work the program's tranche model — ship and record each tranche.", goal: "Tranche deliverables filled and trancheCompletion updated as milestones ship." }
1053
+ - { key: identify, name: Identify, category: planned, domain: operations, description: "Understand the project and which of the program's submission tracks fit — before any writing.", goal: "A clear project understanding and the fitting submission track; submission stage = scope." }
1054
+ - { key: draft, name: Draft, category: started, domain: operations, description: "Write the abstract, then the full proposal, from the client's real material.", goal: "Abstract then full proposal drafted; submission stage abstract → build with body fields filled." }
1055
+ - { key: submit, name: Submit, category: started, domain: operations, description: "Finalize against the program deadline and submit the application.", goal: "Application submitted on time; submission stage = awaiting-results." }
1056
+ - { key: awaiting, name: Awaiting, category: paused, domain: operations, description: "Track the decision and keep the client status current.", goal: "Submission status reflects where the decision stands (in-review / waiting-client / needs-document / blocked)." }
1057
+ - { key: awarded, name: Awarded, category: started, domain: operations, description: "Record the funder's decision and the awarded amount.", goal: "Submission outcome set (awarded / rejected / partial) with the awarded amount." }
1058
+ - { key: delivering, name: Delivering, category: started, domain: operations, description: "Work the program's tranche model — ship and record each tranche.", goal: "Tranche deliverables filled and trancheCompletion updated as milestones ship." }
1043
1059
  executor: is-agent
1044
1060
  grants:
1045
1061
  - entity.create
@@ -1285,10 +1301,23 @@ bento:
1285
1301
  w: 4
1286
1302
  h: 6
1287
1303
  integrations:
1288
- - templateKey: stellar-grant-client
1289
1304
  - templateKey: agency-skills
1290
1305
  - templateKey: nango-google
1291
1306
  - templateKey: discord-bot
1307
+
1308
+ # ── taskIntents — WHAT THIS WORKSPACE NEEDS TO FUNCTION ───────────────────
1309
+ # Closed vocabulary: `@synap-core/types/capability-intents` (pod migrations
1310
+ # 0283/0284). DERIVED from the declared intents of this template's own
1311
+ # `integrations` capability packs — not hand-guessed. Checked at authoring
1312
+ # time by `validateTemplate` (`task-intent-unknown`) and by
1313
+ # `src/task-intents.test.ts`.
1314
+ taskIntents:
1315
+ - send_message
1316
+ - search_external
1317
+ - list_records
1318
+ - schedule_event
1319
+ - manage_file
1320
+ - fetch_record
1292
1321
  onboarding:
1293
1322
  goal: Capture the agency's live grant pipeline — the funding rounds and their deadlines, the projects being advised, and the open submissions in flight — so the workspace mirrors the real book of work, not a generic template.
1294
1323
  framing: "Act as a seasoned blockchain-grants advisor who has shepherded dozens of applications through programs like SCF, Optimism RetroPGF, and Arbitrum. Speak to how a submission actually wins: a crisp problem, a defensible reason the chain matters, evidence of traction, and milestones a reviewer can grade. Translate the founder's words into rounds, projects, and submissions on the pipeline."