···3434 pdsSubtle: "oklch(0.22 0.04 300)",
3535 apps: "oklch(0.65 0.22 262)",
3636 appsSubtle: "oklch(0.22 0.05 262)",
3737+ // Semble's brand orange (#FF6400) at hue 45, kept clear of the amber accent
3838+ // (hue 65) so a Semble tile next to an accent button stays distinguishable.
3939+ cosmik: "oklch(0.75 0.18 45)",
4040+ cosmikSubtle: "oklch(0.22 0.05 45)",
3741 // Hue shifted toward 220 and chroma nudged so dark-mode Sifa reads distinctly
3842 // from Bluesky (245) when both tiles appear together.
3943 sifa: "oklch(0.68 0.14 220)",
···7882 pdsSubtle: "oklch(0.96 0.03 300)",
7983 apps: "#2563EB",
8084 appsSubtle: "oklch(0.95 0.04 262)",
8585+ cosmik: "#FF6400",
8686+ cosmikSubtle: "oklch(0.95 0.05 45)",
8187 sifa: "#4385BE",
8288 sifaSubtle: "oklch(0.96 0.03 220)",
8389 tangled: "oklch(0.30 0 0)",
+2-2
lexicons/run/airglow/automation.json
···344344 },
345345 "followAction": {
346346 "type": "object",
347347- "description": "Follow a subject DID on an AT Protocol social graph (Bluesky, Tangled, or Sifa). All three produce records with the same shape; only the collection NSID differs.",
347347+ "description": "Follow a subject DID on an AT Protocol social graph (Bluesky, Tangled, Sifa, or Semble). All produce records with the same shape; only the collection NSID differs.",
348348 "required": ["target", "subject"],
349349 "properties": {
350350 "target": {
351351 "type": "string",
352352 "description": "Which social graph to follow on.",
353353- "knownValues": ["bluesky", "tangled", "sifa"],
353353+ "knownValues": ["bluesky", "cosmik", "sifa", "tangled"],
354354 "maxLength": 32
355355 },
356356 "subject": {
+16-1
lib/actions/follow.test.ts
···9696 expect(logs[0]!.statusCode).toBe(200);
9797 });
98989999- it("maps tangled and sifa targets to their collections", async () => {
9999+ it("maps tangled, sifa, and cosmik targets to their collections", async () => {
100100 mockCreateRecord.mockResolvedValue({ uri: "at://x/_/rk", cid: "c" });
101101102102 const tangled = makeFollowAction({
···109109 const sifa = makeFollowAction({ target: "sifa", subject: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" });
110110 await executeFollow(makeMatch({ automation: { actions: [sifa] } }), 0);
111111 expect(mockCreateRecord.mock.calls[1]![1]).toBe("id.sifa.graph.follow");
112112+113113+ const cosmik = makeFollowAction({
114114+ target: "cosmik",
115115+ subject: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa",
116116+ });
117117+ await executeFollow(makeMatch({ automation: { actions: [cosmik] } }), 0);
118118+ expect(mockCreateRecord.mock.calls[2]![1]).toBe("network.cosmik.follow");
112119 });
113120114121 it("fails without retry when subject renders to an empty string", async () => {
···246253 const sifa = makeFollowAction({ target: "sifa", subject: EVENT_DID });
247254 await executeFollow(makeMatch({ automation: { actions: [sifa] } }), 0);
248255 expect(mockFetchRecord.mock.calls[1]![0]).toBe(`at://${EVENT_DID}/id.sifa.profile.self/self`);
256256+257257+ // Semble re-uses the Bluesky profile lexicon, so the cosmik pre-check
258258+ // hits app.bsky.actor.profile/self rather than a Cosmik-specific NSID.
259259+ const cosmik = makeFollowAction({ target: "cosmik", subject: EVENT_DID });
260260+ await executeFollow(makeMatch({ automation: { actions: [cosmik] } }), 0);
261261+ expect(mockFetchRecord.mock.calls[2]![0]).toBe(
262262+ `at://${EVENT_DID}/app.bsky.actor.profile/self`,
263263+ );
249264 });
250265251266 it("proceeds with the follow when a profile lookup throws (fail-open)", async () => {