@oneie/sdk 0.14.7 → 0.14.9

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/dist/receivers.js CHANGED
@@ -668,28 +668,39 @@ export const RECEIVERS = {
668
668
  }),
669
669
  "world:announce": receiver({
670
670
  receiver: "world:announce",
671
- summary: "Broadcast a tagged signal; the world fans it to every subscriber whose follow-tags intersect",
671
+ summary: "Broadcast a tagged signal; the world fans it to every subscriber whose follow-tags intersect. reply:true also invokes up to 5 of the matched actors and returns their answers inline (repliesTruncated:true if more matched)",
672
672
  request: z.object({
673
673
  tags: z.array(z.string()),
674
674
  content: z.string().optional(),
675
+ limit: z.number().optional(),
676
+ reply: z.boolean().optional(),
675
677
  board: z.literal("marketplace").optional(),
676
678
  slug: z.string().optional(),
677
679
  workspace: z.string().optional(),
680
+ }).catchall(z.unknown()), // extra keys = the message payload — carried through to every fanned-out actor, never stripped
681
+ response: z.object({
682
+ ok: z.boolean(),
683
+ matched: z.number().optional(),
684
+ actors: z.array(z.string()).optional(),
685
+ replies: z.array(z.object({
686
+ actorId: z.string(), ok: z.boolean(), text: z.string().optional(), error: z.string().optional(),
687
+ })).optional(),
688
+ repliesTruncated: z.boolean().optional(),
678
689
  }),
679
- response: z.object({ ok: z.boolean(), matched: z.number().optional(), actors: z.array(z.string()).optional() }),
680
690
  effect: "ask",
681
691
  }),
682
692
  "world": receiver({
683
693
  receiver: "world",
684
- summary: "THE door — say it once, the world decides. Routes a tagged signal via the ladder (learned path → staked subscribers → CEO); fan:'all' broadcasts to every matching subscriber instead. Address form world:<tag-expr> carries tags in the receiver",
694
+ summary: "THE door — say it once, the world decides. Routes a tagged signal via the ladder (learned path → staked subscribers → CEO); fan:'all' broadcasts to every matching subscriber instead. Address form world:<tag-expr> carries tags in the receiver. reply:true also invokes the resolved actor and returns its answer inline (single-actor routes only, not fan:'all')",
685
695
  request: z.object({
686
696
  tags: z.array(z.string()).optional(),
687
697
  fan: z.enum(["one", "all"]).optional(),
688
698
  deliver: z.boolean().optional(),
699
+ reply: z.boolean().optional(),
689
700
  board: z.literal("marketplace").optional(),
690
701
  slug: z.string().optional(),
691
702
  workspace: z.string().optional(),
692
- }),
703
+ }).catchall(z.unknown()), // extra keys = the message payload (e.g. content) — carried through to the resolved actor, never stripped
693
704
  response: z.object({
694
705
  ok: z.boolean(),
695
706
  routed: z.enum(["learned", "staked", "explore", "ceo"]).optional(),
@@ -697,6 +708,7 @@ export const RECEIVERS = {
697
708
  matched: z.number().optional(),
698
709
  actors: z.array(z.string()).optional(),
699
710
  tags: z.array(z.string()).optional(),
711
+ reply: z.object({ ok: z.boolean(), text: z.string().optional(), error: z.string().optional() }).optional(),
700
712
  }),
701
713
  effect: "ask",
702
714
  }),
@@ -722,19 +734,21 @@ export const RECEIVERS = {
722
734
  }),
723
735
  "world:route": receiver({
724
736
  receiver: "world:route",
725
- summary: "Route a tagged signal to THE handler via the ladder: learned tag-node path (follow_route) → staked subscribers (reputation-ranked) → CEO standing workflow; marks the delivery path so the next route is smarter",
737
+ summary: "Route a tagged signal to THE handler via the ladder: learned tag-node path (follow_route) → staked subscribers (reputation-ranked) → CEO standing workflow; marks the delivery path so the next route is smarter. reply:true also invokes the resolved actor and returns its answer inline",
726
738
  request: z.object({
727
739
  tags: z.array(z.string()),
728
740
  deliver: z.boolean().optional(),
741
+ reply: z.boolean().optional(),
729
742
  board: z.literal("marketplace").optional(),
730
743
  slug: z.string().optional(),
731
744
  workspace: z.string().optional(),
732
- }),
745
+ }).catchall(z.unknown()), // extra keys = the message payload (e.g. content) — carried through to the resolved actor, never stripped
733
746
  response: z.object({
734
747
  ok: z.boolean(),
735
748
  routed: z.enum(["learned", "staked", "explore", "ceo"]).optional(),
736
749
  actor: z.string().optional(),
737
750
  tags: z.array(z.string()).optional(),
751
+ reply: z.object({ ok: z.boolean(), text: z.string().optional(), error: z.string().optional() }).optional(),
738
752
  }),
739
753
  effect: "ask",
740
754
  }),
@@ -762,8 +776,129 @@ export const RECEIVERS = {
762
776
  name: z.string(),
763
777
  tags: z.array(z.string()),
764
778
  weight: z.number(),
779
+ // The context a puller needs to act: notes = the prose goal / what "done" looks
780
+ // like; contextDocs = the doc stems (`<slug>-plan`, `<slug>-todo`, …) to load
781
+ // before running /do <slug>. Empty until the task is enriched (seed/backfill).
782
+ notes: z.string().optional(),
783
+ contextDocs: z.array(z.string()).optional(),
784
+ })),
785
+ }),
786
+ effect: "ask", idempotent: true,
787
+ }),
788
+ "tasks:everywhere": receiver({
789
+ receiver: "tasks:everywhere",
790
+ summary: "Every workspace I belong to, one queue: open+picked tasks visible by assignment/follow/ownership, with claimable + mine + offeredByMe flags; closed:true returns the logbook (done/verified, newest first)",
791
+ request: z.object({ limit: z.number().optional(), closed: z.boolean().optional() }),
792
+ response: z.object({
793
+ ok: z.boolean(),
794
+ tasks: z.array(z.object({
795
+ id: z.string(),
796
+ name: z.string(),
797
+ tags: z.array(z.string()),
798
+ weight: z.number(),
799
+ notes: z.string().optional(),
800
+ contextDocs: z.array(z.string()).optional(),
801
+ workspace: z.string(),
802
+ status: z.string(),
803
+ dueAt: z.string().optional(),
804
+ assignee: z.string().optional(),
805
+ mine: z.boolean(),
806
+ claimable: z.boolean(),
807
+ offeredByMe: z.boolean(),
808
+ closedAt: z.string().optional(),
809
+ })),
810
+ }),
811
+ effect: "ask", idempotent: true,
812
+ }),
813
+ "tasks:create": receiver({
814
+ receiver: "tasks:create",
815
+ summary: "Write one open task to the substrate and announce it by its tags in the same act — the quick-add entry point",
816
+ request: z.object({
817
+ title: z.string(),
818
+ tags: z.array(z.string()).optional(),
819
+ assignee: z.string().optional(),
820
+ // Prose goal / what "done" looks like — the context a human attaches at create so
821
+ // the task is pickable by an agent or a Claude Code session without a round trip.
822
+ notes: z.string().optional(),
823
+ // The viewed /u/<slug> workspace to file the task under. Honored only when the
824
+ // caller is authorized for it (attested staff or owner-tree control); otherwise
825
+ // the resolver falls back to the caller's own slug. Reconciles the create tag
826
+ // with the /api/things read filter so a created task survives reload.
827
+ workspace: z.string().optional(),
828
+ }),
829
+ response: z.object({ ok: z.boolean(), tid: z.string().optional(), tags: z.array(z.string()).optional() }),
830
+ effect: "ask", idempotent: false,
831
+ }),
832
+ "tasks:claim": receiver({
833
+ receiver: "tasks:claim",
834
+ summary: "Take a task off the queue: blocker-gate → status picked → tag the claimant @<slug>. The claimant is the attested caller, never a body field",
835
+ request: z.object({
836
+ tid: z.string(),
837
+ // Viewed workspace — same authorization contract as tasks:create's workspace.
838
+ workspace: z.string().optional(),
839
+ }),
840
+ response: z.object({
841
+ ok: z.boolean().optional(),
842
+ tid: z.string().optional(),
843
+ status: z.string().optional(),
844
+ claimant: z.string().optional(),
845
+ error: z.string().optional(),
846
+ }),
847
+ effect: "ask", idempotent: true,
848
+ }),
849
+ "tasks:undepend": receiver({
850
+ receiver: "tasks:undepend",
851
+ summary: "Remove one blocks edge — the inverse of tasks:depend. Deletes exactly the (blockedBy → tid) pair; does not touch task status. Auth: attested caller only, both ends in the caller's workspace",
852
+ request: z.object({
853
+ tid: z.string(),
854
+ blockedBy: z.string(),
855
+ workspace: z.string().optional(),
856
+ }),
857
+ response: z.object({
858
+ ok: z.boolean().optional(),
859
+ tid: z.string().optional(),
860
+ blockedBy: z.string().optional(),
861
+ error: z.string().optional(),
862
+ }),
863
+ effect: "ask", idempotent: true,
864
+ }),
865
+ "tasks:launch": receiver({
866
+ receiver: "tasks:launch",
867
+ summary: "Batch-launch ready agent-assigned tasks: per task re-check blockers, resolve the @assignee, set picked, fire agent:run, mark owner→assignee on success or revert to open + warn on failure. Per-task receipts — a batch is never all-or-nothing. Caps at 20 tids; skips human assignees",
868
+ request: z.object({
869
+ tids: z.array(z.string()),
870
+ workspace: z.string().optional(),
871
+ }),
872
+ response: z.object({
873
+ ok: z.boolean(),
874
+ results: z.array(z.object({
875
+ tid: z.string(),
876
+ ok: z.boolean(),
877
+ actorId: z.string().optional(),
878
+ reason: z.enum(["unassigned", "human-assignee", "blocked", "forbidden", "fire_failed"]).optional(),
765
879
  })),
766
880
  }),
881
+ effect: "ask", idempotent: false,
882
+ }),
883
+ "tasks:link": receiver({
884
+ receiver: "tasks:link",
885
+ summary: "Close the handoff: record that a claimed task was built via /do <slug> — append a provenance line to the task's notes (the id back-ref + doc stems) and move it to done. Auth: attested caller only",
886
+ request: z.object({
887
+ tid: z.string(),
888
+ // The kebab /do slug the task was built into (the docs backfilled under text/<slug>*).
889
+ slug: z.string(),
890
+ // Doc stems the /do run produced (e.g. ["usage-billing", "usage-billing-plan"]).
891
+ docs: z.array(z.string()).optional(),
892
+ workspace: z.string().optional(),
893
+ }),
894
+ response: z.object({
895
+ ok: z.boolean().optional(),
896
+ tid: z.string().optional(),
897
+ slug: z.string().optional(),
898
+ docs: z.array(z.string()).optional(),
899
+ status: z.string().optional(),
900
+ error: z.string().optional(),
901
+ }),
767
902
  effect: "ask", idempotent: true,
768
903
  }),
769
904
  // ── agents (TRADE / lifecycle) ──
@@ -1236,12 +1371,13 @@ export const RECEIVERS = {
1236
1371
  }),
1237
1372
  "notify": receiver({
1238
1373
  receiver: "notify",
1239
- summary: "Notify any actor (human or agent) by uid — channels routes the door (Telegram/Discord/peer inbox), mirrors into the web inbox, and nudges an open client live. One verb, both actor kinds.",
1374
+ summary: "Notify any actor (human or agent) by uid — channels routes the door (Telegram/Discord/peer inbox), mirrors into the web inbox, nudges an open client live, and sends a web push if the recipient subscribed. One verb, both actor kinds. An optional action renders as one tap-through link.",
1240
1375
  request: z.object({
1241
1376
  receiver: z.string(),
1242
1377
  kind: z.string().optional(),
1243
1378
  body: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(),
1244
1379
  content: z.string().optional(),
1380
+ action: z.object({ label: z.string(), redirect: z.string() }).optional(),
1245
1381
  }),
1246
1382
  response: z.object({
1247
1383
  ok: z.boolean(),
@@ -1276,6 +1412,17 @@ export const RECEIVERS = {
1276
1412
  response: z.object({ ok: z.boolean(), ts: z.number().optional(), error: z.string().optional() }),
1277
1413
  effect: "ask", idempotent: true,
1278
1414
  }),
1415
+ "push:subscribe": receiver({
1416
+ receiver: "push:subscribe",
1417
+ summary: "Store a browser's Web Push subscription so notify() can reach the caller off-screen — the send side behind the shipped inbox service worker. Optional slug keys the row to a workspace the caller controls (else their own slug).",
1418
+ request: z.object({
1419
+ endpoint: z.string(),
1420
+ keys: z.object({ p256dh: z.string(), auth: z.string() }),
1421
+ slug: z.string().optional(),
1422
+ }),
1423
+ response: z.object({ ok: z.boolean(), error: z.string().optional() }),
1424
+ effect: "ask", idempotent: true,
1425
+ }),
1279
1426
  "thread:append": receiver({
1280
1427
  receiver: "thread:append",
1281
1428
  summary: "Gateway-callable write into web threads — carries tags into entity_tags so slug/in renders every channel by tag. Called by the channels bridge after every inbound signal.",
@@ -1893,7 +2040,14 @@ export const RECEIVERS = {
1893
2040
  response: z.object({ ok: z.boolean(), threadId: z.string().optional(), error: z.string().optional() }),
1894
2041
  effect: "ask", cost: "free", reversible: false, idempotent: false, auth: "member",
1895
2042
  }),
1896
- // ── billing — handlers in resolvers/market.ts ──────────────────────────────────
2043
+ "pages:view": receiver({
2044
+ receiver: "pages:view",
2045
+ summary: "Read one published page's Puck render data by workspace + page slug (public) — the read an external site's page-editor plugin renders through; drafts are invisible (see pages:get for the member-scoped editor read)",
2046
+ request: z.object({ slug: z.string(), page: z.string() }),
2047
+ response: z.object({ ok: z.boolean(), slug: z.string().optional(), title: z.string().optional(), data: z.unknown().optional(), url: z.string().optional(), error: z.string().optional() }),
2048
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "public",
2049
+ }),
2050
+ // ──billing — handlers in resolvers/market.ts ──────────────────────────────────
1897
2051
  "billing:plan": receiver({
1898
2052
  receiver: "billing:plan",
1899
2053
  summary: "Change a workspace's plan — returns a checkout URL to complete the upgrade",
@@ -1940,6 +2094,14 @@ export const RECEIVERS = {
1940
2094
  service: z.object({
1941
2095
  service_id: z.string(), name: z.string(), duration_min: z.number(),
1942
2096
  price: z.number(), currency: z.string(),
2097
+ description: z.string().nullable().optional(), // shown on /book, read by the agent (§12.2)
2098
+ location: z.string().nullable().optional(), // 'video' | 'phone' | free-text address
2099
+ intake: z.array(z.object({
2100
+ key: z.string(), label: z.string(),
2101
+ type: z.enum(["text", "number", "choice"]).optional(),
2102
+ required: z.boolean().optional(),
2103
+ options: z.array(z.string()).optional(),
2104
+ })).optional(), // per-service intake questions — both doors ask them
1943
2105
  }).nullable(),
1944
2106
  slots: z.array(z.object({ date: z.string(), times: z.array(z.string()) })), // CalendarCard shape
1945
2107
  error: z.string().optional(),
@@ -1947,6 +2109,35 @@ export const RECEIVERS = {
1947
2109
  effect: "ask", cost: "free", idempotent: true, simulatable: false,
1948
2110
  examples: [{ slug: "clearwater", date: "2026-06-10" }],
1949
2111
  }),
2112
+ // booking:list-services — public discover: the service menu (§12.3). Lets the
2113
+ // chat agent offer "in-home survey, video walkthrough, or storage?" instead of
2114
+ // silently defaulting to the first active row. Active services, public fields
2115
+ // only (hours/buffer are inferable from list-slots output, so not secrets).
2116
+ "booking:list-services": receiver({
2117
+ receiver: "booking:list-services",
2118
+ summary: "List a workspace's active bookable services (menu: name, duration, price, description, location, intake)",
2119
+ request: z.object({ slug: z.string() }),
2120
+ response: z.object({
2121
+ services: z.array(z.object({
2122
+ service_id: z.string(), name: z.string(), duration_min: z.number(),
2123
+ price: z.number(), currency: z.string(),
2124
+ description: z.string().nullable().optional(),
2125
+ location: z.string().nullable().optional(),
2126
+ buffer_min: z.number().optional(),
2127
+ tz: z.string().optional(),
2128
+ hours_json: z.array(z.array(z.object({ start: z.string(), end: z.string() }))).optional(),
2129
+ intake: z.array(z.object({
2130
+ key: z.string(), label: z.string(),
2131
+ type: z.enum(["text", "number", "choice"]).optional(),
2132
+ required: z.boolean().optional(),
2133
+ options: z.array(z.string()).optional(),
2134
+ })).optional(),
2135
+ })),
2136
+ error: z.string().optional(),
2137
+ }),
2138
+ effect: "ask", cost: "free", idempotent: true, auth: "public",
2139
+ examples: [{ slug: "clearwater" }],
2140
+ }),
1950
2141
  "booking:create": receiver({
1951
2142
  receiver: "booking:create",
1952
2143
  summary: "Hold a slot for a customer; returns a ref and a payment intent when price > 0",
@@ -1955,7 +2146,12 @@ export const RECEIVERS = {
1955
2146
  service: z.string(),
1956
2147
  slotDate: z.string(), // 'YYYY-MM-DD'
1957
2148
  slotTime: z.string(), // 'HH:MM' 24h, slot start
1958
- customer: z.object({ name: z.string(), email: z.string().email().optional() }),
2149
+ customer: z.object({
2150
+ name: z.string(),
2151
+ email: z.string().email().optional(),
2152
+ phone: z.string().optional(), // speed-to-lead callback number (§12.3)
2153
+ }),
2154
+ details: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(), // intake answers OR free text; 2KB serialized cap at the resolver, never shape-validated
1959
2155
  paymentRail: z.enum(["stripe", "sui"]).optional(), // resolver overrides by caller kind
1960
2156
  }),
1961
2157
  response: z.object({
@@ -1965,7 +2161,7 @@ export const RECEIVERS = {
1965
2161
  paymentRail: z.enum(["stripe", "sui"]).nullable().optional(),
1966
2162
  paymentIntent: z.object({ clientSecret: z.string(), amount: z.number() }).optional(), // human, price>0
1967
2163
  escrow: z.object({ escrowId: z.string(), bounty: z.number(), deadline: z.string() }).optional(), // agent
1968
- error: z.string().optional(), // 'slot_taken' | 'closed' | 'unknown_service'
2164
+ error: z.string().optional(), // 'slot_taken' | 'closed' | 'unknown_service' | 'details_too_large'
1969
2165
  }),
1970
2166
  effect: "ask", cost: "variable", settles: "offchain", reversible: false,
1971
2167
  simulatable: true, idempotent: true, // idempotencyKey dedupes a retried hold
@@ -1990,6 +2186,15 @@ export const RECEIVERS = {
1990
2186
  hours_json: z.array(z.array(z.object({ start: z.string(), end: z.string() }))), // 7 entries, 0=Sun
1991
2187
  tz: z.string().optional(),
1992
2188
  active: z.boolean().optional(),
2189
+ description: z.string().optional(), // shown on /book + read by the agent (§12.1)
2190
+ location: z.string().optional(), // 'video' | 'phone' | free-text address
2191
+ buffer_min: z.number().int().nonnegative().optional(), // drive time between slots (§12.5)
2192
+ intake: z.array(z.object({
2193
+ key: z.string(), label: z.string(),
2194
+ type: z.enum(["text", "number", "choice"]).optional(),
2195
+ required: z.boolean().optional(),
2196
+ options: z.array(z.string()).optional(),
2197
+ })).optional(), // per-service intake questions (§12.2); full-body upsert — omitted clears
1993
2198
  }),
1994
2199
  response: z.object({ ok: z.boolean(), service_id: z.string().optional(), error: z.string().optional() }),
1995
2200
  effect: "ask", cost: "free", idempotent: true, reversible: true, auth: "manage_clients",
@@ -2011,25 +2216,56 @@ export const RECEIVERS = {
2011
2216
  id: z.string(), ref: z.string(), service_name: z.string(),
2012
2217
  slot_date: z.string(), slot_time: z.string(),
2013
2218
  customer_name: z.string(), status: z.string(),
2219
+ customer_phone: z.string().nullable().optional(), // §12.3 — the job, not just the name
2220
+ details: z.union([z.string(), z.record(z.string(), z.unknown())]).nullable().optional(), // parsed details_json (record) or verbatim free text; untrusted display data
2014
2221
  })),
2015
2222
  error: z.string().optional(),
2016
2223
  }),
2017
2224
  effect: "ask", cost: "free", idempotent: true, auth: "manage_clients",
2018
2225
  examples: [{ slug: "clearwater", month: "2026-06" }],
2019
2226
  }),
2020
- // booking:cancel — provider dissolves a booking by ref, releasing the slot
2021
- // immediately (the partial unique index drops dissolved rows from scope, §9).
2022
- // Tenant-private write: scoped to the authenticated owner. Idempotent.
2227
+ // booking:cancel — dual-path (§12.6). Provider dissolves by ref (ctx-gated to
2228
+ // the attested owner). Customer self-cancels with ref + matching contact (public;
2229
+ // possession IS the gate, exactly like the public book path). Either way
2230
+ // status='dissolved' releases the slot via the partial unique index (§9).
2023
2231
  "booking:cancel": receiver({
2024
2232
  receiver: "booking:cancel",
2025
- summary: "Provider-gated: dissolve a booking by ref, releasing the slot",
2233
+ summary: "Dissolve a booking by ref provider (ctx-gated) or customer (ref + matching contact)",
2026
2234
  request: z.object({
2027
2235
  slug: z.string(),
2028
2236
  ref: z.string(),
2237
+ email: z.string().email().optional(), // customer path: possession = ref + matching contact
2238
+ phone: z.string().optional(),
2029
2239
  }),
2030
2240
  response: z.object({ ok: z.boolean(), error: z.string().optional() }),
2031
- effect: "ask", cost: "free", idempotent: true, reversible: false, auth: "manage_clients",
2032
- examples: [{ slug: "clearwater", ref: "CLE-2026-4821" }],
2241
+ effect: "ask", cost: "free", idempotent: true, reversible: false, auth: "public",
2242
+ examples: [{ slug: "clearwater", ref: "CLE-2026-4821", email: "s@ex.com" }],
2243
+ }),
2244
+ // booking:reschedule — customer self-serve move (§12.6). Public; the gate is
2245
+ // ref + matching contact. Hold-new-first: the resolver INSERTs the new slot
2246
+ // (same guarded INSERT — can lose with slot_taken) THEN dissolves the old row,
2247
+ // so a lost race leaves the original booking standing. Payment rail/ref carry over.
2248
+ "booking:reschedule": receiver({
2249
+ receiver: "booking:reschedule",
2250
+ summary: "Customer self-serve: move a booking to a new slot by ref + matching contact (hold-new-first)",
2251
+ request: z.object({
2252
+ slug: z.string(),
2253
+ ref: z.string(),
2254
+ email: z.string().email().optional(),
2255
+ phone: z.string().optional(),
2256
+ newDate: z.string(), // 'YYYY-MM-DD'
2257
+ newTime: z.string(), // 'HH:MM' 24h slot start
2258
+ }),
2259
+ response: z.object({
2260
+ ok: z.boolean(),
2261
+ ref: z.string().optional(), // the NEW booking ref (ref is UNIQUE — a moved booking gets a fresh ref)
2262
+ status: z.enum(["pending", "confirmed"]).optional(),
2263
+ slotDate: z.string().optional(),
2264
+ slotTime: z.string().optional(),
2265
+ error: z.string().optional(), // 'not_found' | 'slot_taken' | 'closed' | 'unknown_service'
2266
+ }),
2267
+ effect: "ask", cost: "free", idempotent: true, reversible: false, auth: "public",
2268
+ examples: [{ slug: "clearwater", ref: "CLE-2026-4821", email: "s@ex.com", newDate: "2026-06-12", newTime: "10:30" }],
2033
2269
  }),
2034
2270
  // ── workflows — define an SOP, run it, watch it learn (text/workflows-plan.md) ──
2035
2271
  // Every workflow op is a typed receiver. No new REST routes. apply-diff is
@@ -2090,6 +2326,23 @@ export const RECEIVERS = {
2090
2326
  effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
2091
2327
  examples: [{ workflowId: "wf_abc", limit: 20 }, { workflowId: "wf_abc", runId: "run_1" }],
2092
2328
  }),
2329
+ "workflow:preview-step": receiver({
2330
+ receiver: "workflow:preview-step",
2331
+ summary: "Resolve one step's receiver + args through the real substitution code, with an optional mock trigger/step payload standing in for run history — never dispatches, reads no run history",
2332
+ request: z.object({
2333
+ workflowId: z.string(),
2334
+ stepId: z.string(),
2335
+ mockTrigger: z.record(z.string(), z.unknown()).optional(),
2336
+ mockSteps: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
2337
+ }),
2338
+ response: z.object({
2339
+ receiver: z.string().nullable(),
2340
+ data: z.record(z.string(), z.unknown()),
2341
+ error: z.string().optional(),
2342
+ }),
2343
+ effect: "ask", cost: "free", reversible: false, idempotent: true, simulatable: false, auth: "view_workflows",
2344
+ examples: [{ workflowId: "wf_abc", stepId: "step_1", mockTrigger: { email: "a@b.com" } }],
2345
+ }),
2093
2346
  "workflow:create": receiver({
2094
2347
  receiver: "workflow:create",
2095
2348
  summary: "Create a blank workflow (group group-type=workflow) — optionally cloned from a template",
@@ -2294,13 +2547,21 @@ export const RECEIVERS = {
2294
2547
  "skills:list": receiver({
2295
2548
  receiver: "skills:list",
2296
2549
  summary: "List the caller's workspace skill catalog (R2) — powers the canvas SkillPicker",
2297
- request: z.object({}),
2550
+ request: z.object({ name: z.string().optional() }),
2298
2551
  response: z.object({
2299
- skills: z.array(z.object({ name: z.string(), size: z.number(), sealed: z.boolean().optional() })),
2552
+ skills: z.array(z.object({ name: z.string(), size: z.number(), sealed: z.boolean().optional(), description: z.string().optional(), inputs: z.array(z.object({ name: z.string(), label: z.string(), required: z.boolean().optional() })).optional() })),
2300
2553
  }),
2301
2554
  effect: "ask", cost: "free", idempotent: true, simulatable: false, auth: "view_workflows",
2302
2555
  examples: [{}],
2303
2556
  }),
2557
+ "skill:save": receiver({
2558
+ receiver: "skill:save",
2559
+ summary: "Write a skill body into a workspace's R2 catalog — the store skill:run/skills:list read; explicit-slug, ownership-walk authorized (mirrors world:publish-agent)",
2560
+ request: z.object({ slug: z.string(), name: z.string(), content: z.string() }),
2561
+ response: z.object({ ok: z.boolean(), name: z.string(), bytes: z.number() }),
2562
+ effect: "ask", auth: "update_group", idempotent: true,
2563
+ examples: [{ slug: "elitemoversca", name: "quote-estimate", content: "---\nname: quote-estimate\n---\n…" }],
2564
+ }),
2304
2565
  "skill:run": receiver({
2305
2566
  receiver: "skill:run",
2306
2567
  summary: "Run a workspace skill — loads its body from R2, runs one bounded turn via channels, returns { text }",
@@ -2594,6 +2855,48 @@ export const RECEIVERS = {
2594
2855
  response: z.object({ broadcast: z.record(z.string(), z.unknown()) }),
2595
2856
  effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "read",
2596
2857
  }),
2858
+ "segment:create": receiver({
2859
+ receiver: "segment:create",
2860
+ summary: "Create an audience segment from a rule definition (SegmentDef: {all, any?})",
2861
+ request: z.object({ workspace: z.string(), name: z.string(), description: z.string().optional(), definition: z.record(z.string(), z.unknown()) }),
2862
+ response: z.object({ id: z.string() }),
2863
+ effect: "ask", cost: "free", reversible: true, idempotent: false, auth: "write",
2864
+ }),
2865
+ "segment:update": receiver({
2866
+ receiver: "segment:update",
2867
+ summary: "Update an audience segment — name, description, or definition",
2868
+ request: z.object({ workspace: z.string(), id: z.string(), name: z.string().optional(), description: z.string().optional(), definition: z.record(z.string(), z.unknown()).optional() }),
2869
+ response: z.object({ ok: z.boolean() }),
2870
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "write",
2871
+ }),
2872
+ "segment:list": receiver({
2873
+ receiver: "segment:list",
2874
+ summary: "List audience segments for a workspace",
2875
+ request: z.object({ workspace: z.string() }),
2876
+ response: z.object({ segments: z.array(z.record(z.string(), z.unknown())) }),
2877
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "read",
2878
+ }),
2879
+ "segment:get": receiver({
2880
+ receiver: "segment:get",
2881
+ summary: "Get one audience segment with its rule definition",
2882
+ request: z.object({ workspace: z.string(), id: z.string() }),
2883
+ response: z.object({ segment: z.record(z.string(), z.unknown()) }),
2884
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "read",
2885
+ }),
2886
+ "segment:preview": receiver({
2887
+ receiver: "segment:preview",
2888
+ summary: "Preview an audience segment — live count + up to 10 sample addresses (read-only)",
2889
+ request: z.object({ workspace: z.string(), definition: z.record(z.string(), z.unknown()), channel: z.enum(["email", "sms", "whatsapp"]).optional() }),
2890
+ response: z.object({ count: z.number(), sample: z.array(z.string()) }),
2891
+ effect: "ask", cost: "free", reversible: false, idempotent: true, auth: "read",
2892
+ }),
2893
+ "segment:delete": receiver({
2894
+ receiver: "segment:delete",
2895
+ summary: "Delete an audience segment and its membership snapshot",
2896
+ request: z.object({ workspace: z.string(), id: z.string() }),
2897
+ response: z.object({ ok: z.boolean() }),
2898
+ effect: "ask", cost: "free", reversible: true, idempotent: true, auth: "write",
2899
+ }),
2597
2900
  "broadcast:test": receiver({
2598
2901
  receiver: "broadcast:test",
2599
2902
  summary: "Send a test email for a broadcast to one address",