@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.
- package/README.md +483 -0
- package/automations/bookmark-enrichment.automation.json +321 -0
- package/automations/github-pr-events.automation.json +505 -0
- package/automations/mail-feed.automation.json +86 -0
- package/automations/ship-a-change-checks-green.automation.json +106 -0
- package/automations/ship-a-change-checks-red.automation.json +106 -0
- package/automations/ship-a-change-deployed.automation.json +56 -0
- package/automations/ship-a-change-pr-merged.automation.json +110 -0
- package/automations/ship-a-change-pr-opened.automation.json +104 -0
- package/automations/url-bookmark-capture.automation.json +132 -0
- package/capabilities/agency-skills.capability.json +157 -0
- package/capabilities/apify.capability.json +265 -0
- package/capabilities/apollo.capability.json +120 -0
- package/capabilities/arch-backend.capability.json +97 -0
- package/capabilities/arch-client-intelligence.capability.json +401 -0
- package/capabilities/backblaze-b2.capability.json +205 -0
- package/capabilities/cal-com.capability.json +266 -0
- package/capabilities/canva.capability.json +274 -0
- package/capabilities/claude-code-routines.capability.json +172 -0
- package/capabilities/claude-managed-agents.capability.json +215 -0
- package/capabilities/client-comms-report.capability.json +246 -0
- package/capabilities/content-idea-to-production.capability.json +29 -0
- package/capabilities/datagouv.capability.json +315 -0
- package/capabilities/discord-bot.capability.json +157 -0
- package/capabilities/dropbox.capability.json +281 -0
- package/capabilities/exa.capability.json +109 -0
- package/capabilities/fal-ai.capability.json +83 -0
- package/capabilities/fal-media-lab.capability.json +158 -0
- package/capabilities/fireflies.capability.json +191 -0
- package/capabilities/freellmapi.capability.json +114 -0
- package/capabilities/generic-apikey.capability.json +92 -0
- package/capabilities/github-merge.capability.json +99 -0
- package/capabilities/github-read.capability.json +325 -0
- package/capabilities/governance-tighten-recommender.capability.json +81 -0
- package/capabilities/jev.capability.json +226 -0
- package/capabilities/lead-conversion-enrollment.capability.json +246 -0
- package/capabilities/mail-feed.capability.json +277 -0
- package/capabilities/mailgun.capability.json +52 -0
- package/capabilities/nango-google.capability.json +639 -0
- package/capabilities/notion.capability.json +417 -0
- package/capabilities/omniroute.capability.json +137 -0
- package/capabilities/posthog-analytics.capability.json +258 -0
- package/capabilities/postiz.capability.json +467 -0
- package/capabilities/proactive-digest.capability.json +190 -0
- package/capabilities/proton.capability.json +288 -0
- package/capabilities/replicate-rvm.capability.json +97 -0
- package/capabilities/research-methods.capability.json +57 -0
- package/capabilities/shopify.capability.json +395 -0
- package/capabilities/slack.capability.json +369 -0
- package/capabilities/stripe.capability.json +420 -0
- package/capabilities/telegram-bridge.capability.json +56 -0
- package/capabilities/treg.capability.json +102 -0
- package/capabilities/unipile-linkedin.capability.json +471 -0
- package/capabilities/vercel.capability.json +424 -0
- package/dist/define-CPS0n8BV.d.ts +1331 -0
- package/dist/index.d.ts +189 -900
- package/dist/index.js +24654 -21639
- package/dist/index.js.map +1 -1
- package/dist/wire.d.ts +222 -0
- package/dist/wire.js +77 -0
- package/dist/wire.js.map +1 -0
- package/package.json +41 -27
- package/packs/bookmark-views-pack.json +33 -0
- package/packs/crm-pack.json +76 -0
- package/packs/customer-profile.json +28 -0
- package/packs/deep-research.playbook.json +30 -0
- package/packs/event-views-pack.json +33 -0
- package/packs/habit-profile.json +27 -0
- package/packs/hr-pack.json +54 -0
- package/packs/invoice-profile.json +28 -0
- package/packs/knowledge-base-pack.json +56 -0
- package/packs/meeting-profile.json +29 -0
- package/packs/product-profile.json +30 -0
- package/packs/project-management-pack.json +49 -0
- package/packs/recipe-profile.json +29 -0
- package/packs/task-views-pack.json +39 -0
- package/retired.yaml +136 -0
- package/skills/app-factory-build-slice/SKILL.md +70 -0
- package/skills/app-factory-build-slice.catalog.json +17 -0
- package/skills/app-factory-design-canvas/SKILL.md +96 -0
- package/skills/app-factory-design-canvas.catalog.json +17 -0
- package/skills/app-factory-design-review/SKILL.md +99 -0
- package/skills/app-factory-design-review.catalog.json +18 -0
- package/skills/app-factory-research/SKILL.md +123 -0
- package/skills/app-factory-research.catalog.json +17 -0
- package/skills/bookmark-enrichment/SKILL.md +40 -0
- package/skills/carousel-format/carousel-format.skill.json +18 -0
- package/skills/collage-broll/collage-broll.skill.json +20 -0
- package/skills/content-taste/content-taste.skill.json +18 -0
- package/skills/creative-director/creative-director.skill.json +18 -0
- package/skills/design-critique/SKILL.md +135 -0
- package/skills/design-critique.catalog.json +12 -0
- package/skills/designing-with-real-components/SKILL.md +140 -0
- package/skills/designing-with-real-components.catalog.json +12 -0
- package/skills/edit-loop/edit-loop.skill.json +25 -0
- package/skills/facecam-edit/facecam-edit.skill.json +24 -0
- package/skills/mapping-the-server/SKILL.md +54 -0
- package/skills/math-explainer-manimgx/math-explainer-manimgx.skill.json +20 -0
- package/skills/motion-quality-gates/motion-quality-gates.skill.json +20 -0
- package/skills/open-design-edit/open-design-edit.skill.json +24 -0
- package/skills/reading-client-progress/SKILL.md +65 -0
- package/skills/routing-to-channels/SKILL.md +54 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-CHEATSHEET.md +133 -0
- package/skills/stellar-scf-grants-advisor/ASSOCIATE-ONBOARDING-GUIDE.md +761 -0
- package/skills/stellar-scf-grants-advisor/CLIENT-FAQ.md +181 -0
- package/skills/stellar-scf-grants-advisor/PARTNER-HANDOVER.md +130 -0
- package/skills/stellar-scf-grants-advisor/SKILL.md +102 -0
- package/skills/stellar-scf-grants-advisor/examples/README.md +43 -0
- package/skills/stellar-scf-grants-advisor/examples/chat-summary-templates.md +158 -0
- package/skills/stellar-scf-grants-advisor/examples/corpus-comparable-rows.md +136 -0
- package/skills/stellar-scf-grants-advisor/examples/q10-team-rewrites.md +109 -0
- package/skills/stellar-scf-grants-advisor/examples/q2-description-rewrites.md +88 -0
- package/skills/stellar-scf-grants-advisor/examples/q6-integration-rewrites.md +107 -0
- package/skills/stellar-scf-grants-advisor/examples/tranche-structures.md +176 -0
- package/skills/stellar-scf-grants-advisor/reference/01-methodology.md +96 -0
- package/skills/stellar-scf-grants-advisor/reference/02-scoring-framework.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/03-rejection-patterns.md +241 -0
- package/skills/stellar-scf-grants-advisor/reference/04-winner-patterns.md +120 -0
- package/skills/stellar-scf-grants-advisor/reference/05-comeback-patterns.md +92 -0
- package/skills/stellar-scf-grants-advisor/reference/06-budget-benchmarks.md +125 -0
- package/skills/stellar-scf-grants-advisor/reference/07-integration-list.md +147 -0
- package/skills/stellar-scf-grants-advisor/reference/08-abstract-fields.md +258 -0
- package/skills/stellar-scf-grants-advisor/reference/09-build-fields.md +259 -0
- package/skills/stellar-scf-grants-advisor/reference/10-corpus-winners.md +135 -0
- package/skills/stellar-scf-grants-advisor/reference/11-corpus-rejects.md +267 -0
- package/skills/stellar-scf-grants-advisor/reference/12-client-communication.md +131 -0
- package/skills/stellar-scf-grants-advisor/reference/13-excel-output-spec.md +199 -0
- package/skills/stellar-scf-grants-advisor/reference/14-worked-examples.md +160 -0
- package/skills/stellar-scf-grants-advisor/templates/abstract-review-builder.py +253 -0
- package/skills/stellar-scf-grants-advisor/templates/build-review-builder.py +213 -0
- package/skills/video-direction/video-direction.skill.json +20 -0
- package/skills/video-style-vocabulary/video-style-vocabulary.skill.json +20 -0
- package/skills/video-toolbox/video-toolbox.skill.json +20 -0
- package/src/__fixtures__/lifecycle-presets.flat-before.json +205 -0
- package/src/agency-os.yaml +122 -0
- package/src/agent-os-skill-slugs.test.ts +65 -0
- package/src/apify-social-reader.test.ts +317 -0
- package/src/base.template.test.ts +25 -19
- package/src/base.yaml +95 -39
- package/src/blockchain-ecosystem.yaml +1 -0
- package/src/brand-library.yaml +525 -14
- package/src/builder-dev-fold.template.test.ts +113 -0
- package/src/builder.yaml +798 -92
- package/src/business-model-catalog-description.test.ts +47 -0
- package/src/business-model-grp.template.test.ts +214 -0
- package/src/business-model.yaml +598 -0
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +85 -2
- package/src/civic-france.yaml +102 -0
- package/src/concept-guard.test.ts +28 -8
- package/src/concept-guard.ts +2 -2
- package/src/content-creation.template.test.ts +52 -0
- package/src/content-creation.yaml +192 -0
- package/src/content-os.edit-loop.template.test.ts +210 -0
- package/src/content-os.publish-due-posts.template.test.ts +261 -0
- package/src/content-os.template.test.ts +322 -0
- package/src/content-os.video-takes.template.test.ts +338 -0
- package/src/content-os.work-a-take.template.test.ts +125 -0
- package/src/content-os.yaml +1830 -39
- package/src/content-studio-retired.template.test.ts +82 -0
- package/src/corpus-flow-validity.test.ts +4 -5
- package/src/crm-absorbs-outreach.template.test.ts +117 -0
- package/src/crm-build-plan.template.test.ts +128 -0
- package/src/crm-lists.template.test.ts +96 -0
- package/src/crm-messages-cadence.template.test.ts +164 -0
- package/src/crm-one-send-door.template.test.ts +65 -0
- package/src/crm.yaml +1456 -181
- package/src/decisions-wave.template.test.ts +186 -0
- package/src/define.ts +130 -17
- package/src/design-to-implementation.template.test.ts +94 -0
- package/src/dist-freshness.test.ts +97 -0
- package/src/domain-template-type.test.ts +75 -0
- package/src/ecosystem.yaml +27 -66
- package/src/enterprise-os.yaml +10 -134
- package/src/facecam-edit.audio-studio.test.ts +188 -0
- package/src/fal-generate-image.test.ts +200 -0
- package/src/fal-media-lab-pod-files.test.ts +180 -0
- package/src/finance.yaml +254 -1
- package/src/foundation.yaml +130 -28
- package/src/frame-host-modules.d.mts +6 -0
- package/src/frame-host-modules.mjs +58 -0
- package/src/generate.mjs +9 -3
- package/src/github-merge.test.ts +210 -0
- package/src/grants.yaml +57 -17
- package/src/hr.yaml +39 -6
- package/src/index.ts +36 -1
- package/src/intent.test.ts +161 -0
- package/src/internal-runbook.yaml +19 -1
- package/src/jev.test.ts +356 -0
- package/src/legal.yaml +33 -1
- package/src/life-os.yaml +65 -240
- package/src/lifecycle-coverage.test.ts +124 -0
- package/src/lifecycle-declarations.test.ts +252 -0
- package/src/lifecycle-presets.test.ts +468 -0
- package/src/lifecycle-presets.ts +494 -0
- package/src/lifecycle-stuck.test.ts +46 -0
- package/src/lifecycle-waits.parity.test.ts +319 -0
- package/src/marketing.yaml +108 -4
- package/src/meetings-knowledge.yaml +18 -0
- package/src/networking.yaml +23 -2
- package/src/operations.yaml +304 -4
- package/src/pipeline-views.template.test.ts +88 -0
- package/src/playbook-goal-placeholders.test.ts +57 -0
- package/src/postiz-contract.test.ts +134 -0
- package/src/presentation-declarations.test.ts +174 -0
- package/src/process-contract.validate.test.ts +352 -0
- package/src/project-management.yaml +39 -740
- package/src/publish-body.d.mts +29 -0
- package/src/publish-body.mjs +74 -0
- package/src/publish-body.test.ts +74 -0
- package/src/publish-official-lib.test.ts +176 -0
- package/src/replicate-rvm.test.ts +504 -0
- package/src/research-base.yaml +119 -20
- package/src/research-decision.yaml +60 -3
- package/src/reserved-profile-slugs.test.ts +83 -0
- package/src/retired-goal-kinds.test.ts +58 -0
- package/src/role-reference-and-step-domain.test.ts +192 -0
- package/src/seed-obligation.test.ts +176 -0
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +42 -1
- package/src/ship-a-change.template.test.ts +177 -0
- package/src/skills.test.ts +170 -0
- package/src/social.yaml +85 -27
- package/src/space-packages.freshness.test.ts +97 -0
- package/src/space-packages.generated.ts +40 -0
- package/src/space-packages.test.ts +125 -0
- package/src/space-packages.ts +73 -0
- package/src/start.test.ts +188 -0
- package/src/task-intents.test.ts +293 -0
- package/src/templates.ts +21639 -21522
- package/src/types.ts +443 -60
- package/src/unipile-publish-post.test.ts +162 -0
- package/src/validate-flow.drift.test.ts +2 -3
- package/src/validate-flow.ts +32 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +156 -3
- package/src/validate.ts +1384 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- package/src/__fixtures__/report-automation-flow.json +0 -393
- package/src/agent-fleet.yaml +0 -1645
- package/src/business-developer.template.test.ts +0 -351
- package/src/business-developer.yaml +0 -860
- package/src/content-studio.yaml +0 -361
- package/src/dev-dashboard.yaml +0 -990
- package/src/outreach-comms.yaml +0 -486
- package/src/the-arch.yaml +0 -201
package/src/foundation.yaml
CHANGED
|
@@ -7,15 +7,18 @@
|
|
|
7
7
|
meta:
|
|
8
8
|
slug: foundation
|
|
9
9
|
domain: strategy
|
|
10
|
-
name:
|
|
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:
|
|
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
|
|
250
|
-
#
|
|
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
|
-
-
|
|
554
|
+
- new
|
|
555
|
+
- researching
|
|
556
|
+
- outreach
|
|
557
|
+
- engaged
|
|
496
558
|
- qualified
|
|
497
559
|
- nurturing
|
|
498
560
|
- converted
|
|
499
|
-
-
|
|
500
|
-
constraints: { defaultValue:
|
|
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
|
-
# ──
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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:
|
|
519
|
-
label:
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
*
|
|
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
|
+
});
|