@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.
- 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 +174 -900
- package/dist/index.js +26148 -23931
- 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 +27 -4
- 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.yaml +85 -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 +779 -91
- 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 +296 -49
- package/src/catalog-safety.test.ts +198 -0
- package/src/catalog.template-items.test.ts +75 -0
- package/src/catalog.ts +66 -2
- 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/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 +78 -38
- 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 +46 -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 +51 -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 +1 -6
- package/src/send-message-maps.test.ts +227 -0
- package/src/shared-role-ssot.test.ts +40 -2
- 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 +20851 -21496
- 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 +22 -2
- package/src/validate-onboarding.test.ts +59 -0
- package/src/validate.test.ts +71 -4
- package/src/validate.ts +1335 -7
- package/src/wire-core.ts +231 -0
- package/src/wire.test.ts +159 -0
- package/src/wire.ts +107 -0
- 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/operations.yaml
CHANGED
|
@@ -12,6 +12,32 @@ meta:
|
|
|
12
12
|
color: "#0F766E"
|
|
13
13
|
tags: [operations, delivery, clients, engagements]
|
|
14
14
|
isPublic: true
|
|
15
|
+
# Onboarding start (meta.start): the "client delivery" card.
|
|
16
|
+
start:
|
|
17
|
+
order: 4
|
|
18
|
+
label: Client delivery
|
|
19
|
+
promise: Every client engagement and where it stands.
|
|
20
|
+
suggestFor: [consultant]
|
|
21
|
+
profile: engagement
|
|
22
|
+
views: [engagements]
|
|
23
|
+
example:
|
|
24
|
+
- { title: "Website redesign for Maison Dupont", values: { engagement-status: active, engagement-value: 18000 } }
|
|
25
|
+
- { title: "Monthly strategy retainer", values: { engagement-status: active, engagement-value: 4500 } }
|
|
26
|
+
- { title: "Brand audit for Nordlys", values: { engagement-status: proposed, engagement-value: 6000 } }
|
|
27
|
+
- { title: "Onboarding workshop for Atelier 9", values: { engagement-status: completed, engagement-value: 2800 } }
|
|
28
|
+
firstSession:
|
|
29
|
+
title: Your live engagements
|
|
30
|
+
goal: "Add the 3 to 5 client engagements you are delivering now, each with its client, a status and a value."
|
|
31
|
+
starters:
|
|
32
|
+
- "I run client projects as a freelancer"
|
|
33
|
+
- "We are a small agency with several retainers"
|
|
34
|
+
- "I deliver workshops and audits to companies"
|
|
35
|
+
# What this space PRODUCES (homes): the purpose word, then its kinds/roles.
|
|
36
|
+
intent:
|
|
37
|
+
label: Delivery
|
|
38
|
+
produces:
|
|
39
|
+
- profile: engagement
|
|
40
|
+
- profile: deliverable
|
|
15
41
|
|
|
16
42
|
workspace:
|
|
17
43
|
name: Operations
|
|
@@ -45,12 +71,21 @@ profiles:
|
|
|
45
71
|
icon: briefcase
|
|
46
72
|
color: "#0F766E"
|
|
47
73
|
description: "A unit of client work — a project or retainer delivered for a company, with scope, dates, and value."
|
|
74
|
+
# The ONE lifecycle (work model L1): the initiative base without its
|
|
75
|
+
# agreed and ready steps (lifecycle-presets.ts). Putting an engagement on
|
|
76
|
+
# hold and cancelling it are a person's call; completing it is not (the
|
|
77
|
+
# close stage has its own human gate).
|
|
78
|
+
lifecycle:
|
|
79
|
+
property: engagement-status
|
|
80
|
+
adopts: initiative
|
|
81
|
+
drop: [agreed, ready]
|
|
82
|
+
rename: { dropped: cancelled }
|
|
83
|
+
humanOnly: { on-hold: true, completed: false, cancelled: true }
|
|
48
84
|
properties:
|
|
49
85
|
- slug: engagement-status
|
|
50
86
|
label: Status
|
|
51
87
|
valueType: string
|
|
52
88
|
inputType: select
|
|
53
|
-
enumValues: [proposed, active, on-hold, completed, cancelled]
|
|
54
89
|
constraints: { defaultValue: proposed }
|
|
55
90
|
- slug: engagement-client
|
|
56
91
|
label: Client
|
|
@@ -81,18 +116,42 @@ profiles:
|
|
|
81
116
|
label: Notes
|
|
82
117
|
valueType: string
|
|
83
118
|
inputType: textarea
|
|
119
|
+
# How the engagement is invoiced (founder decision 2026-10-12): once at
|
|
120
|
+
# close (the default — "Deliver a client engagement" creates the closing
|
|
121
|
+
# invoice), or one invoice per accepted deliverable (milestone billing —
|
|
122
|
+
# "Deliver a deliverable" creates it at Accepted). Not a lifecycle.
|
|
123
|
+
- slug: engagement-billing-mode
|
|
124
|
+
label: Billing
|
|
125
|
+
valueType: string
|
|
126
|
+
inputType: select
|
|
127
|
+
enumValues: [at-close, per-deliverable]
|
|
128
|
+
constraints: { defaultValue: at-close }
|
|
84
129
|
|
|
85
130
|
- slug: deliverable
|
|
86
131
|
displayName: Deliverable
|
|
87
132
|
icon: package-check
|
|
88
133
|
color: "#0EA5E9"
|
|
89
134
|
description: "A concrete output owed inside an engagement — with a due date, an owner, and a status."
|
|
135
|
+
# The ONE lifecycle (work model L1): every option's category.
|
|
136
|
+
lifecycle:
|
|
137
|
+
property: deliverable-status
|
|
138
|
+
options:
|
|
139
|
+
- { key: not-started, category: open }
|
|
140
|
+
- { key: in-progress, category: active }
|
|
141
|
+
- { key: in-review, category: active }
|
|
142
|
+
- { key: delivered, category: active }
|
|
143
|
+
- { key: accepted, category: done }
|
|
144
|
+
# A deliverable the client no longer wants (scope cut, engagement
|
|
145
|
+
# cancelled) ends here, not in `accepted`; dropping it is a person's
|
|
146
|
+
# call, like every `cancelled` (work-item base, plan aac47c4a W5). It is
|
|
147
|
+
# a client decision, not a failure: muted, not the lost default `error`.
|
|
148
|
+
- { key: cancelled, category: lost, tone: textMuted, humanOnly: true }
|
|
90
149
|
properties:
|
|
91
150
|
- slug: deliverable-status
|
|
92
151
|
label: Status
|
|
93
152
|
valueType: string
|
|
94
153
|
inputType: select
|
|
95
|
-
enumValues: [not-started, in-progress, in-review, delivered, accepted]
|
|
154
|
+
enumValues: [not-started, in-progress, in-review, delivered, accepted, cancelled]
|
|
96
155
|
constraints: { defaultValue: not-started }
|
|
97
156
|
- slug: deliverable-engagement
|
|
98
157
|
label: Engagement
|
|
@@ -121,6 +180,16 @@ profiles:
|
|
|
121
180
|
icon: file-signature
|
|
122
181
|
color: "#8B5CF6"
|
|
123
182
|
description: "The agreement that governs an engagement — its status, value, term, and signed document."
|
|
183
|
+
# The ONE lifecycle (work model L1): every option's category.
|
|
184
|
+
lifecycle:
|
|
185
|
+
property: contract-status
|
|
186
|
+
options:
|
|
187
|
+
- { key: draft, category: open }
|
|
188
|
+
- { key: sent, category: active }
|
|
189
|
+
- { key: signed, category: active }
|
|
190
|
+
- { key: active, category: active }
|
|
191
|
+
- { key: expired, category: done, humanOnly: true }
|
|
192
|
+
- { key: terminated, category: lost, humanOnly: true }
|
|
124
193
|
properties:
|
|
125
194
|
- slug: contract-status
|
|
126
195
|
label: Status
|
|
@@ -267,10 +336,12 @@ onboarding:
|
|
|
267
336
|
engagement, who owns it, and when it's due.
|
|
268
337
|
collect:
|
|
269
338
|
- profileSlug: engagement
|
|
339
|
+
min: 1
|
|
270
340
|
what: The client engagements live right now — one per real project or retainer, with its client and dates.
|
|
271
341
|
cardinality: several
|
|
272
342
|
keyFields: [engagement-status, engagement-client, engagement-end]
|
|
273
343
|
- profileSlug: deliverable
|
|
344
|
+
min: 1
|
|
274
345
|
what: The deliverables due inside those engagements, placed at their true status with a due date.
|
|
275
346
|
cardinality: several
|
|
276
347
|
keyFields: [deliverable-status, due-date]
|
|
@@ -591,8 +662,8 @@ automations:
|
|
|
591
662
|
# ══════════════════════════════════════════════════════════════════════════
|
|
592
663
|
# CRM → Operations HANDOFF PICKUP
|
|
593
664
|
#
|
|
594
|
-
# The other half of
|
|
595
|
-
# attaching the pod-wide `client` role with `handoffStatus`; nothing on the
|
|
665
|
+
# The other half of CRM's "Win a deal" (and The Arch's "Win and hand off",
|
|
666
|
+
# moved there from business-developer 2026-10-12). Both end by attaching the pod-wide `client` role with `handoffStatus`; nothing on the
|
|
596
667
|
# delivery side ever picked it up, so a won deal became a client and then
|
|
597
668
|
# nothing happened. This automation is that pickup: once a day it finds
|
|
598
669
|
# clients marked ready/handed_off that have NO engagement yet, and opens the
|
|
@@ -841,3 +912,232 @@ automations:
|
|
|
841
912
|
- { id: h-engagement-link, source: create-engagement, target: link-engagement }
|
|
842
913
|
- { id: h-link-contract, source: link-engagement, target: create-contract }
|
|
843
914
|
- { id: h-contract-link, source: create-contract, target: link-contract }
|
|
915
|
+
|
|
916
|
+
playbooks:
|
|
917
|
+
- name: Deliver a client engagement
|
|
918
|
+
description: Run one client engagement from kick-off to close — scope, contract, deliverables, final review and the closing invoice — with a person approving the start and the close.
|
|
919
|
+
goalTemplate: >-
|
|
920
|
+
Run this client engagement ({engagement}); each stage moves its status.
|
|
921
|
+
KICK-OFF: read the won deal, the proposal and every message with the
|
|
922
|
+
client; write the scope, owner, start and end dates and value on the
|
|
923
|
+
engagement; create one deliverable per promised output (each with a due
|
|
924
|
+
date and an owner); reuse the draft contract already linked to the
|
|
925
|
+
engagement (the daily client pick-up opens one), and create a draft
|
|
926
|
+
contract only when none is linked.
|
|
927
|
+
START: a person confirms the contract is signed and the work may start.
|
|
928
|
+
DELIVER: track every deliverable to accepted (or cancelled by a person),
|
|
929
|
+
post a short weekly status in this session's room, and flag any
|
|
930
|
+
deliverable past its due date.
|
|
931
|
+
CLOSE: once every deliverable is accepted or cancelled, write the closing review
|
|
932
|
+
(what was delivered, what slipped, what the client said), create the
|
|
933
|
+
closing invoice in Finance at the draft status for the engagement value
|
|
934
|
+
still owed (when the engagement bills per deliverable, subtract every
|
|
935
|
+
milestone invoice already created for it), and propose the renewal or
|
|
936
|
+
next engagement. A person
|
|
937
|
+
approves the close. Pausing or cancelling an engagement is a person's
|
|
938
|
+
call.
|
|
939
|
+
subjectProfile:
|
|
940
|
+
profileSlug: engagement
|
|
941
|
+
statusProperty: engagement-status
|
|
942
|
+
humanOnlyStatuses: [on-hold, cancelled]
|
|
943
|
+
activators:
|
|
944
|
+
- { on: created, mode: propose }
|
|
945
|
+
params:
|
|
946
|
+
- { name: engagement, type: entity, label: Engagement, required: true }
|
|
947
|
+
executor: is-agent
|
|
948
|
+
status: active
|
|
949
|
+
requiredIntents: [list_records, send_message]
|
|
950
|
+
expectedOutputs:
|
|
951
|
+
- { kind: deliverable, label: Deliverables, relationToSubject: relates_to }
|
|
952
|
+
- { kind: contract, label: Contract, relationToSubject: contract_for_engagement }
|
|
953
|
+
- { kind: invoice, label: Closing invoice, relationToSubject: relates_to }
|
|
954
|
+
- { kind: document, label: Closing review }
|
|
955
|
+
metadata:
|
|
956
|
+
lineage: { method: "Agency delivery: kick-off, milestone tracking, close-out review" }
|
|
957
|
+
stages:
|
|
958
|
+
- key: kickoff
|
|
959
|
+
name: Kick-off
|
|
960
|
+
category: planned
|
|
961
|
+
subjectStatus: proposed
|
|
962
|
+
goal: Write scope, owner, dates and value; create the deliverables; reuse the linked draft contract or create one when none exists.
|
|
963
|
+
criteria:
|
|
964
|
+
- key: kickoff-scope
|
|
965
|
+
statement: The engagement's scope, owner, start date, end date and value are set.
|
|
966
|
+
check: { kind: judge }
|
|
967
|
+
- key: kickoff-deliverables
|
|
968
|
+
statement: Every output promised in the proposal exists as a deliverable with a due date and an owner.
|
|
969
|
+
check: { kind: judge }
|
|
970
|
+
- key: kickoff-contract
|
|
971
|
+
statement: A contract is linked to the engagement.
|
|
972
|
+
check: { kind: judge }
|
|
973
|
+
- key: start
|
|
974
|
+
name: Start
|
|
975
|
+
category: started
|
|
976
|
+
subjectStatus: active
|
|
977
|
+
goal: A person confirms the contract is signed and the work may start.
|
|
978
|
+
gate: { kind: human }
|
|
979
|
+
onFail: { toStage: kickoff }
|
|
980
|
+
- key: deliver
|
|
981
|
+
name: Deliver
|
|
982
|
+
category: started
|
|
983
|
+
goal: Track every deliverable to accepted or cancelled and post a weekly status in the room.
|
|
984
|
+
indefinite: true
|
|
985
|
+
criteria:
|
|
986
|
+
- key: deliver-all-accepted
|
|
987
|
+
statement: Every deliverable of the engagement is accepted or cancelled.
|
|
988
|
+
check: { kind: judge }
|
|
989
|
+
- key: deliver-weekly-status
|
|
990
|
+
statement: A status was posted in the room for every week the engagement was active.
|
|
991
|
+
check: { kind: judge }
|
|
992
|
+
- key: close
|
|
993
|
+
name: Close
|
|
994
|
+
category: completed
|
|
995
|
+
subjectStatus: completed
|
|
996
|
+
goal: Write the closing review, create the closing invoice, propose the renewal; a person approves the close.
|
|
997
|
+
gate: { kind: human }
|
|
998
|
+
onFail: { toStage: deliver }
|
|
999
|
+
|
|
1000
|
+
# Starts when a person moves the deliverable to In progress (founder
|
|
1001
|
+
# decision 2026-10-12), not when it is created: a deliverable is usually
|
|
1002
|
+
# created at kick-off, weeks before anyone works on it. Not started is
|
|
1003
|
+
# therefore a person's status, and the first stage writes nothing.
|
|
1004
|
+
- name: Deliver a deliverable
|
|
1005
|
+
description: Take one deliverable from in progress to accepted by the client — plan, produce, review internally, send, get the client's sign-off, and bill it when the engagement bills per deliverable.
|
|
1006
|
+
goalTemplate: >-
|
|
1007
|
+
Deliver this deliverable ({deliverable}); each stage moves its status.
|
|
1008
|
+
PLAN: read the engagement's scope and the client's brief, and write what
|
|
1009
|
+
done looks like for this deliverable. PRODUCE: make it, or break it into
|
|
1010
|
+
tasks for the people who will. REVIEW: check it against what done looks
|
|
1011
|
+
like before anyone outside sees it. SEND: a person approves before it
|
|
1012
|
+
goes to the client; record the date it was delivered. ACCEPTED: a person
|
|
1013
|
+
confirms the client accepted it; if the client asked for changes, go
|
|
1014
|
+
back to producing. Then read the engagement's billing: only when it
|
|
1015
|
+
bills per deliverable, create this deliverable's invoice in Finance at
|
|
1016
|
+
the draft status for its share of the engagement value, linked to the
|
|
1017
|
+
deliverable. When it bills at close, create no invoice; the engagement's
|
|
1018
|
+
close bills it.
|
|
1019
|
+
subjectProfile:
|
|
1020
|
+
profileSlug: deliverable
|
|
1021
|
+
statusProperty: deliverable-status
|
|
1022
|
+
humanOnlyStatuses: [not-started, cancelled]
|
|
1023
|
+
activators:
|
|
1024
|
+
- { on: enters_status, status: in-progress, mode: propose }
|
|
1025
|
+
params:
|
|
1026
|
+
- { name: deliverable, type: entity, label: Deliverable, required: true }
|
|
1027
|
+
executor: is-agent
|
|
1028
|
+
status: active
|
|
1029
|
+
expectedOutputs:
|
|
1030
|
+
- { kind: document, label: Delivered file or link }
|
|
1031
|
+
stages:
|
|
1032
|
+
- key: plan
|
|
1033
|
+
name: Plan
|
|
1034
|
+
category: started
|
|
1035
|
+
goal: Write what done looks like for this deliverable from the engagement scope and the client brief.
|
|
1036
|
+
criteria:
|
|
1037
|
+
- key: plan-definition-of-done
|
|
1038
|
+
statement: The deliverable states what done looks like in checkable terms.
|
|
1039
|
+
check: { kind: judge }
|
|
1040
|
+
- key: produce
|
|
1041
|
+
name: Produce
|
|
1042
|
+
category: started
|
|
1043
|
+
subjectStatus: in-progress
|
|
1044
|
+
goal: Make the deliverable, or split it into tasks with owners.
|
|
1045
|
+
criteria:
|
|
1046
|
+
- key: produce-draft-attached
|
|
1047
|
+
statement: A draft of the deliverable is attached or linked.
|
|
1048
|
+
check: { kind: judge }
|
|
1049
|
+
- key: review
|
|
1050
|
+
name: Internal review
|
|
1051
|
+
category: started
|
|
1052
|
+
subjectStatus: in-review
|
|
1053
|
+
goal: Check the draft against what done looks like before the client sees it.
|
|
1054
|
+
gate: { kind: check }
|
|
1055
|
+
onFail: { toStage: produce }
|
|
1056
|
+
criteria:
|
|
1057
|
+
- key: review-meets-done
|
|
1058
|
+
statement: The draft meets every point of the deliverable's definition of done.
|
|
1059
|
+
check: { kind: judge }
|
|
1060
|
+
- key: send
|
|
1061
|
+
name: Send to client
|
|
1062
|
+
category: started
|
|
1063
|
+
subjectStatus: delivered
|
|
1064
|
+
goal: A person approves sending; record the delivered date.
|
|
1065
|
+
gate: { kind: human }
|
|
1066
|
+
onFail: { toStage: produce }
|
|
1067
|
+
criteria:
|
|
1068
|
+
- key: send-date-recorded
|
|
1069
|
+
statement: The deliverable's delivered date is set.
|
|
1070
|
+
check: { kind: evidence, evidenceKey: delivered-date }
|
|
1071
|
+
- key: accepted
|
|
1072
|
+
name: Accepted
|
|
1073
|
+
category: completed
|
|
1074
|
+
subjectStatus: accepted
|
|
1075
|
+
goal: A person confirms the client accepted the deliverable; when the engagement bills per deliverable, create its draft invoice in Finance.
|
|
1076
|
+
gate: { kind: human }
|
|
1077
|
+
onFail: { toStage: produce }
|
|
1078
|
+
# Milestone billing. Only made when the engagement's billing is
|
|
1079
|
+
# per-deliverable; at-close engagements are billed by the engagement's
|
|
1080
|
+
# close. `optional` (synap-backend 30a9ab85): nobody owes the slot, so
|
|
1081
|
+
# an at-close run never shows it pending; the condition itself lives
|
|
1082
|
+
# in the goal.
|
|
1083
|
+
expectedOutputs:
|
|
1084
|
+
- { kind: invoice, label: Milestone invoice (per-deliverable billing only), relationToSubject: relates_to, optional: true }
|
|
1085
|
+
|
|
1086
|
+
- name: Get a contract signed
|
|
1087
|
+
description: Take a client contract from draft to signed and active, with a person approving what is sent and confirming the signature.
|
|
1088
|
+
goalTemplate: >-
|
|
1089
|
+
Get this contract ({contract}) signed; each stage moves its status.
|
|
1090
|
+
DRAFT: fill the contract from the engagement's scope, value, dates and
|
|
1091
|
+
the agency's standard terms, and list every term that differs from the
|
|
1092
|
+
standard. SEND: a person approves the contract before it goes to the
|
|
1093
|
+
client. SIGNED: ask the person to `provide` the signed file; their
|
|
1094
|
+
approval confirms both sides signed. ACTIVE: set the start and end dates and link the signed
|
|
1095
|
+
document. Expiry and termination are a person's call.
|
|
1096
|
+
subjectProfile:
|
|
1097
|
+
profileSlug: contract
|
|
1098
|
+
statusProperty: contract-status
|
|
1099
|
+
humanOnlyStatuses: [expired, terminated]
|
|
1100
|
+
activators:
|
|
1101
|
+
- { on: created, mode: propose }
|
|
1102
|
+
params:
|
|
1103
|
+
- { name: contract, type: entity, label: Contract, required: true }
|
|
1104
|
+
executor: is-agent
|
|
1105
|
+
status: active
|
|
1106
|
+
expectedOutputs:
|
|
1107
|
+
- { kind: document, label: Signed contract }
|
|
1108
|
+
stages:
|
|
1109
|
+
- key: draft
|
|
1110
|
+
name: Draft
|
|
1111
|
+
category: planned
|
|
1112
|
+
subjectStatus: draft
|
|
1113
|
+
goal: Fill the contract from the engagement and the standard terms; list every non-standard term.
|
|
1114
|
+
criteria:
|
|
1115
|
+
- key: contract-value-dates
|
|
1116
|
+
statement: The contract's value, start date and end date are set.
|
|
1117
|
+
check: { kind: judge }
|
|
1118
|
+
- key: contract-deviations-listed
|
|
1119
|
+
statement: Every term that differs from the agency's standard terms is listed.
|
|
1120
|
+
check: { kind: judge }
|
|
1121
|
+
- key: send
|
|
1122
|
+
name: Send
|
|
1123
|
+
category: started
|
|
1124
|
+
subjectStatus: sent
|
|
1125
|
+
goal: A person approves the contract before it goes to the client.
|
|
1126
|
+
gate: { kind: human }
|
|
1127
|
+
onFail: { toStage: draft }
|
|
1128
|
+
- key: signed
|
|
1129
|
+
name: Signed
|
|
1130
|
+
category: started
|
|
1131
|
+
subjectStatus: signed
|
|
1132
|
+
goal: Ask the person to provide the signed file; their approval confirms both sides signed.
|
|
1133
|
+
gate: { kind: human }
|
|
1134
|
+
onFail: { toStage: send }
|
|
1135
|
+
- key: active
|
|
1136
|
+
name: Active
|
|
1137
|
+
category: completed
|
|
1138
|
+
subjectStatus: active
|
|
1139
|
+
goal: Set the dates and link the signed document on the contract.
|
|
1140
|
+
criteria:
|
|
1141
|
+
- key: contract-document-linked
|
|
1142
|
+
statement: The signed document's link is set on the contract.
|
|
1143
|
+
check: { kind: evidence, evidenceKey: contract-url }
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D3 (work model L3, 2026-10-12): a catalog board over a declared lifecycle is
|
|
3
|
+
* a `pipeline` view — ONE rendering of "things in the columns of their
|
|
4
|
+
* status" (the pod's `lifecycle.board`, the one mover), never a kanban beside
|
|
5
|
+
* it that draws a column per drifted value.
|
|
6
|
+
*
|
|
7
|
+
* Reachability, not shape (guards-and-tests.md): a `pipeline` view whose kind
|
|
8
|
+
* declares NO lifecycle renders nothing at all (PipelineView OMITs an
|
|
9
|
+
* undeclared profile), and one whose config names no kind renders "This
|
|
10
|
+
* pipeline names no kind". So every pipeline view must name its kind in
|
|
11
|
+
* `config.profileSlug` (what `readPipelineConfig` reads), and that kind must
|
|
12
|
+
* declare a lifecycle in the template itself or in a template it depends on
|
|
13
|
+
* (the install resolver puts dependencies first — the lead role's lifecycle
|
|
14
|
+
* lives in foundation).
|
|
15
|
+
*
|
|
16
|
+
* The scanned set is DERIVED from every bundled template; a non-vacuity floor
|
|
17
|
+
* keeps it honest. What it does not see: a lifecycle declared only by a pod
|
|
18
|
+
* (not in the catalog), and a pod whose dependency was later removed.
|
|
19
|
+
*/
|
|
20
|
+
import { describe, expect, it } from "vitest";
|
|
21
|
+
import { WORKSPACE_TEMPLATES } from "./templates.js";
|
|
22
|
+
|
|
23
|
+
type View = {
|
|
24
|
+
name: string;
|
|
25
|
+
type: string;
|
|
26
|
+
scopeProfileSlug?: string;
|
|
27
|
+
groupBy?: string;
|
|
28
|
+
config?: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
type Profile = { slug: string; lifecycle?: { property: string } };
|
|
31
|
+
type Template = {
|
|
32
|
+
meta?: { slug?: string };
|
|
33
|
+
profiles?: Profile[];
|
|
34
|
+
views?: View[];
|
|
35
|
+
dependencies?: Array<{ slug: string }>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const templates = WORKSPACE_TEMPLATES as unknown as Record<string, Template>;
|
|
39
|
+
const bySlug = new Map(
|
|
40
|
+
Object.entries(templates).map(([key, t]) => [t.meta?.slug ?? key, t] as const),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
/** The lifecycle property a profile declares here or in a dependency (transitively). */
|
|
44
|
+
function lifecycleOf(t: Template, profileSlug: string, seen = new Set<Template>()): string | null {
|
|
45
|
+
if (seen.has(t)) return null;
|
|
46
|
+
seen.add(t);
|
|
47
|
+
const own = t.profiles?.find((p) => p.slug === profileSlug && p.lifecycle)?.lifecycle?.property;
|
|
48
|
+
if (own) return own;
|
|
49
|
+
for (const dep of t.dependencies ?? []) {
|
|
50
|
+
const d = bySlug.get(dep.slug);
|
|
51
|
+
const found = d ? lifecycleOf(d, profileSlug, seen) : null;
|
|
52
|
+
if (found) return found;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const allViews = Object.values(templates).flatMap((t) => (t.views ?? []).map((v) => ({ t, v })));
|
|
58
|
+
const pipelines = allViews.filter(({ v }) => v.type === "pipeline");
|
|
59
|
+
|
|
60
|
+
describe("catalog pipeline views (D3)", () => {
|
|
61
|
+
it("the scan sees the catalog and the converted boards", () => {
|
|
62
|
+
expect(allViews.length).toBeGreaterThan(50);
|
|
63
|
+
expect(pipelines.map(({ v }) => v.name).sort()).toEqual(
|
|
64
|
+
expect.arrayContaining(["Hiring Pipeline", "Lead Pipeline", "Pipeline"]),
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("every pipeline view names a kind that declares a lifecycle", () => {
|
|
69
|
+
const broken = pipelines.flatMap(({ t, v }) => {
|
|
70
|
+
const slug = v.config?.profileSlug;
|
|
71
|
+
if (typeof slug !== "string" || !slug) return [`${t.meta?.slug} / ${v.name}: no config.profileSlug`];
|
|
72
|
+
if (v.scopeProfileSlug && v.scopeProfileSlug !== slug)
|
|
73
|
+
return [`${t.meta?.slug} / ${v.name}: config.profileSlug ≠ scopeProfileSlug`];
|
|
74
|
+
return lifecycleOf(t, slug) ? [] : [`${t.meta?.slug} / ${v.name}: ${slug} declares no lifecycle`];
|
|
75
|
+
});
|
|
76
|
+
expect(broken).toEqual([]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("no kanban named a pipeline still groups a declared lifecycle (it is a pipeline view)", () => {
|
|
80
|
+
const stray = allViews.flatMap(({ t, v }) => {
|
|
81
|
+
if (v.type !== "kanban" || !/pipeline/i.test(v.name) || !v.scopeProfileSlug) return [];
|
|
82
|
+
const property = lifecycleOf(t, v.scopeProfileSlug);
|
|
83
|
+
const groupBy = v.groupBy ?? v.config?.groupBy ?? v.config?.groupByField;
|
|
84
|
+
return property && groupBy === property ? [`${t.meta?.slug} / ${v.name}`] : [];
|
|
85
|
+
});
|
|
86
|
+
expect(stray).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every shipped playbook goal is fillable by a run.
|
|
3
|
+
*
|
|
4
|
+
* The pod's playbook door (`playbooks.create`, which the package installer
|
|
5
|
+
* reaches) REFUSES a goalTemplate whose placeholder no run fills
|
|
6
|
+
* (synap-backend `services/playbooks/goal-references.ts`, 1c619345):
|
|
7
|
+
* - a bare `{{name}}` — the run door substitutes `{name}` / `@{arg:name}`
|
|
8
|
+
* only, so `{{name}}` reached the agent verbatim; it never worked;
|
|
9
|
+
* - a `{name}` / `@{arg:name}` naming no declared param.
|
|
10
|
+
* A template that ships either now fails its own install. This guard fails the
|
|
11
|
+
* template at build time instead, derived over EVERY bundled template.
|
|
12
|
+
*
|
|
13
|
+
* It mirrors the pod's rule (this package cannot import the api), so it proves
|
|
14
|
+
* the templates agree with THAT rule — not that the rule is right. Rooted
|
|
15
|
+
* automation paths (`{{trigger.x}}`, `{{steps.y}}`) are not placeholders and
|
|
16
|
+
* are out of scope; so is braced prose (`{see below}`, a space inside).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, expect, it } from "vitest";
|
|
20
|
+
import { WORKSPACE_TEMPLATES } from "./templates.js";
|
|
21
|
+
// One matcher, shared with validateCatalog (which runs it on EVERY kind's playbooks).
|
|
22
|
+
import { goalPlaceholderProblems } from "../scripts/catalog-lib.mjs";
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
const goals = Object.entries(WORKSPACE_TEMPLATES).flatMap(([slug, tpl]) =>
|
|
26
|
+
(tpl.playbooks ?? []).map((p) => ({
|
|
27
|
+
where: `${slug} › ${p.name}`,
|
|
28
|
+
goal: p.goalTemplate ?? "",
|
|
29
|
+
declared: new Set((p.params ?? []).map((x) => x.name)),
|
|
30
|
+
}))
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
describe("playbook goals — every placeholder is one a run fills", () => {
|
|
34
|
+
it("non-vacuity: the scan sees real playbooks, and a real {name} placeholder", () => {
|
|
35
|
+
expect(goals.length).toBeGreaterThanOrEqual(15);
|
|
36
|
+
expect(goals.some((g) => /\{[A-Za-z_]\w*\}/.test(g.goal))).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("the matcher catches what it hunts (self-check)", () => {
|
|
40
|
+
expect(goalPlaceholderProblems("Check {{part}} now", new Set(["part"]))).toEqual([
|
|
41
|
+
"{{part}}",
|
|
42
|
+
]);
|
|
43
|
+
expect(goalPlaceholderProblems("Do {x} and @{arg:y}", new Set(["x"]))).toEqual([
|
|
44
|
+
"@{arg:y}",
|
|
45
|
+
]);
|
|
46
|
+
expect(
|
|
47
|
+
goalPlaceholderProblems("Since {{trigger.payload.since}} {see below}", new Set())
|
|
48
|
+
).toEqual([]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("no shipped playbook goal carries {{name}} or an undeclared {name}", () => {
|
|
52
|
+
const bad = goals.flatMap((g) =>
|
|
53
|
+
goalPlaceholderProblems(g.goal, g.declared).map((p) => `${g.where}: ${p}`)
|
|
54
|
+
);
|
|
55
|
+
expect(bad).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Postiz contract: the property keys the backend's Postiz webhook and the
|
|
3
|
+
* `postiz` capability agree on, pinned where the model is authored.
|
|
4
|
+
*
|
|
5
|
+
* The backend finds a post by `postiz-post-id` (the id Postiz returned when the
|
|
6
|
+
* post was queued) and a post queued on a `platform` account sends that
|
|
7
|
+
* account's `postiz-integration-id` and `postiz-provider`. A rename here would
|
|
8
|
+
* leave the webhook matching nothing, silently, so the keys are pinned on the
|
|
9
|
+
* SSOT template (content-os owns `post` and `platform`) and on every template
|
|
10
|
+
* that re-declares those profiles with the Postiz fields.
|
|
11
|
+
*
|
|
12
|
+
* NOT covered: the backend side of the contract (the webhook route and its
|
|
13
|
+
* lookup), which lives in synap-backend; whether Postiz accepts the body
|
|
14
|
+
* (exercised against the openapi, not a live Postiz).
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { describe, expect, it } from "vitest";
|
|
19
|
+
import { WORKSPACE_TEMPLATES } from "./templates.js";
|
|
20
|
+
import type { TemplateProfile, WorkspaceYaml } from "./types.js";
|
|
21
|
+
|
|
22
|
+
const POST_KEYS = ["postiz-post-id"];
|
|
23
|
+
const PLATFORM_KEYS = ["postiz-integration-id", "postiz-provider"];
|
|
24
|
+
|
|
25
|
+
const templates = Object.values(WORKSPACE_TEMPLATES) as WorkspaceYaml[];
|
|
26
|
+
const profile = (tpl: WorkspaceYaml | undefined, slug: string): TemplateProfile | undefined =>
|
|
27
|
+
(tpl?.profiles ?? []).find((p) => p.slug === slug);
|
|
28
|
+
const propertyOf = (p: TemplateProfile | undefined, key: string) =>
|
|
29
|
+
(p?.properties ?? []).find((prop) => prop.slug === key);
|
|
30
|
+
|
|
31
|
+
const capability = JSON.parse(
|
|
32
|
+
readFileSync(join(import.meta.dirname, "..", "capabilities", "postiz.capability.json"), "utf8"),
|
|
33
|
+
) as { spec: { skills: Array<Record<string, any>>; provides?: string[] } };
|
|
34
|
+
|
|
35
|
+
describe("Postiz contract — the keys the webhook and the capability share", () => {
|
|
36
|
+
const contentOs = WORKSPACE_TEMPLATES["content-os"] as WorkspaceYaml | undefined;
|
|
37
|
+
|
|
38
|
+
it.each([
|
|
39
|
+
["post", POST_KEYS],
|
|
40
|
+
["platform", PLATFORM_KEYS],
|
|
41
|
+
] as const)("content-os (the SSOT) declares %s's Postiz keys as text", (slug, keys) => {
|
|
42
|
+
const p = profile(contentOs, slug);
|
|
43
|
+
expect(p, `content-os declares ${slug}`).toBeDefined();
|
|
44
|
+
for (const key of keys) expect(propertyOf(p, key)?.valueType, `${slug}.${key}`).toBe("string");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("every template that re-declares post or platform with a Postiz field declares all of them, as text", () => {
|
|
48
|
+
let seen = 0;
|
|
49
|
+
for (const tpl of templates) {
|
|
50
|
+
for (const [slug, keys] of [
|
|
51
|
+
["post", POST_KEYS],
|
|
52
|
+
["platform", PLATFORM_KEYS],
|
|
53
|
+
] as const) {
|
|
54
|
+
const p = profile(tpl, slug);
|
|
55
|
+
if (!p || !(p.properties ?? []).some((prop) => prop.slug.startsWith("postiz-"))) continue;
|
|
56
|
+
seen += 1;
|
|
57
|
+
for (const key of keys) expect(propertyOf(p, key)?.valueType, `${tpl.meta?.slug}: ${slug}.${key}`).toBe("string");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// content-os and social each declare both profiles.
|
|
61
|
+
expect(seen).toBeGreaterThanOrEqual(4);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("the capability queues a post that can be written back: postiz_schedule_post serves publish_post and returns the ids", () => {
|
|
65
|
+
const verb = capability.spec.skills.find((s) => s.name === "postiz_schedule_post");
|
|
66
|
+
expect(verb?.intent).toBe("publish_post");
|
|
67
|
+
expect(verb?.kind).toBe("code");
|
|
68
|
+
// A write: an agent's run is a proposal (never metadata.readOnly). The pod's
|
|
69
|
+
// post-gate runner writes `result.postId` onto the post named by the
|
|
70
|
+
// `postEntityId` param (backend `metadata.writeBack` hook, lane pz-be).
|
|
71
|
+
expect(verb?.metadata).toEqual({
|
|
72
|
+
verbType: "write",
|
|
73
|
+
writeBack: { entityParam: "postEntityId", kind: "post", properties: { "postiz-post-id": "postId" } },
|
|
74
|
+
});
|
|
75
|
+
for (const p of ["content", "integrationId", "provider", "postEntityId"]) expect(Object.keys(verb?.parameters ?? {})).toContain(p);
|
|
76
|
+
expect(verb?.code).toMatch(/return \{ postId, integrationId, provider,[^}]*postEntityId/);
|
|
77
|
+
expect(capability.spec.provides).toContain("publish_post");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("the channel list hands back what a platform stores: id and provider", () => {
|
|
81
|
+
const verb = capability.spec.skills.find((s) => s.name === "postiz_list_integrations");
|
|
82
|
+
expect(verb?.metadata).toEqual({ readOnly: true });
|
|
83
|
+
expect(verb?.providerSpec?.responseShape?.item).toMatchObject({ id: "id", provider: "identifier" });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("Produce Content requires publish_post and queues at Scheduled", () => {
|
|
87
|
+
const pb = (contentOs?.playbooks ?? []).find((p: { name?: string }) => p.name === "Produce Content") as
|
|
88
|
+
| { requiredIntents?: string[]; stages?: Array<{ key: string; subjectStatus?: string; goal?: string }> }
|
|
89
|
+
| undefined;
|
|
90
|
+
expect(pb?.requiredIntents).toContain("publish_post");
|
|
91
|
+
const schedule = pb?.stages?.find((s) => s.key === "schedule");
|
|
92
|
+
expect(schedule?.subjectStatus).toBe("Scheduled");
|
|
93
|
+
expect(schedule?.goal).toContain("postiz_schedule_post");
|
|
94
|
+
expect(schedule?.goal).toContain("postiz-post-id");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Intent actions (decision ef0e917c, 2026-10-28): a pod app asks the person's
|
|
99
|
+
// Postiz to publish a post with ONE input `{ text, when? }`. Postiz holds one
|
|
100
|
+
// key for several social channels, so each mapped verb names the channel list
|
|
101
|
+
// as its account source; the pod offers one action per channel and refuses a
|
|
102
|
+
// map whose source is not a read listing these fields.
|
|
103
|
+
describe("Postiz — what a pod app may ask it to publish", () => {
|
|
104
|
+
const skill = (name: string) => capability.spec.skills.find((s) => s.name === name)!;
|
|
105
|
+
|
|
106
|
+
it.each(["postiz_schedule", "postiz_publish"])("%s maps the text and the channel picked from postiz_list_integrations", (name) => {
|
|
107
|
+
expect(skill(name).inputMap).toEqual(
|
|
108
|
+
expect.objectContaining({
|
|
109
|
+
params: expect.objectContaining({
|
|
110
|
+
content: "$.text",
|
|
111
|
+
integrationId: "$account.id",
|
|
112
|
+
provider: "$account.provider",
|
|
113
|
+
}),
|
|
114
|
+
accounts: { verb: "postiz_list_integrations", id: "id", label: "name", provider: "provider" },
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("the account source lists the fields it names", () => {
|
|
120
|
+
const list = skill("postiz_list_integrations");
|
|
121
|
+
expect(list.metadata?.readOnly).toBe(true);
|
|
122
|
+
expect(Object.keys(list.providerSpec.responseShape.item)).toEqual(expect.arrayContaining(["id", "name", "provider"]));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("only the scheduled post reads the time; publishing now never does", () => {
|
|
126
|
+
expect(skill("postiz_schedule").inputMap.params.date).toBe("$.when");
|
|
127
|
+
expect(skill("postiz_publish").inputMap.params).not.toHaveProperty("date");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("drafts, uploads and the media post are not offered to apps (P1 maps text and time only)", () => {
|
|
131
|
+
for (const name of ["postiz_draft", "postiz_schedule_post", "postiz_upload_from_url"])
|
|
132
|
+
expect(skill(name).inputMap, name).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|