@zooid/core 0.12.0 → 0.14.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/dist/index.d.ts CHANGED
@@ -7,7 +7,8 @@ import { EventEmitter } from 'node:events';
7
7
  * Per-agent ACP block in zooid.yaml. XOR: either a known preset or an
8
8
  * explicit command. The schema parser rejects both/neither.
9
9
  *
10
- * Built-in presets: `claude`, `codex`, `opencode`, `cline`, `kiro`, `gemini`.
10
+ * Built-in presets: `claude`, `codex`, `opencode`, `pi`, `cline`, `kiro`,
11
+ * `gemini`.
11
12
  * See `@zooid/acp-client`'s preset registry for the current list.
12
13
  */
13
14
  type AcpAgentSpec = {
@@ -318,12 +319,74 @@ interface ZooidConfig {
318
319
  pre_turn?: string;
319
320
  post_turn?: string;
320
321
  };
322
+ /** Optional. Map of trigger name → trigger. Empty map when the block is absent. */
323
+ triggers: Record<string, TriggerConfig>;
321
324
  }
322
325
  interface CliFlags {
323
326
  runtime?: string;
324
327
  /** Container image override (shorthand for container.image). */
325
328
  image?: string;
326
329
  }
330
+ /**
331
+ * A webhook trigger: exposes `POST /_zooid/webhooks/<name>`, verifies an HMAC
332
+ * signature over the raw request body, and fires like a schedule trigger
333
+ * once accepted. See [[ZOD082]] §Design 4 and [[ZOD086]].
334
+ */
335
+ interface WebhookTriggerConfig {
336
+ provider: 'github' | 'stripe' | 'slack' | 'standard' | 'custom';
337
+ /** Required. Interpolated from env — see [[ZOD081]] §Design 4, Secrets. */
338
+ secret: string;
339
+ /**
340
+ * `custom` only, required: path to a module exporting the verifier
341
+ * function, resolved to an absolute path against the zooid.yaml directory
342
+ * at parse time. The escape hatch for any scheme without a named provider
343
+ * — ed25519, SHA-1 over sorted params, bespoke timestamped base strings.
344
+ * Code rather than declarative fields, because a config language for
345
+ * signing schemes is a security-critical mini-DSL that still would not
346
+ * cover them all. The daemon imports it at startup — see
347
+ * `loadCustomVerifiers` in the CLI.
348
+ */
349
+ verify?: string;
350
+ }
351
+ /**
352
+ * One message a trigger can post: a room, the agent to mention, the body
353
+ * template, and — webhook triggers only — a CEL `match:` predicate gating
354
+ * whether this message fires for a given delivery. See [[ZOD085]] §Design 4.
355
+ */
356
+ interface TriggerMessage {
357
+ /** Room id or alias the message goes to. */
358
+ room: string;
359
+ /** Agent name (key in `agents`) to mention. Structural, never templated — §Design 3. */
360
+ mention: string;
361
+ /** The message body. Literal for a scheduled trigger; `${...}` is CEL-interpolated for a webhook trigger. */
362
+ text: string;
363
+ /**
364
+ * CEL predicate over the delivery (`event`, `body`, `headers`, `output`).
365
+ * Absent means always fire. Only valid on a `webhook:` trigger — a
366
+ * schedule trigger has no delivery to evaluate.
367
+ */
368
+ match?: string;
369
+ }
370
+ /**
371
+ * A trigger: fires on a cron or a webhook delivery and posts one or more
372
+ * `messages`, each structurally mentioning its agent so the turn starts
373
+ * through the ordinary message path. No emitter/command tier — see
374
+ * [[ZOD081]] §Concept.
375
+ */
376
+ interface TriggerConfig {
377
+ /** Cron expression. Mutually exclusive with `webhook:` — exactly one is required. */
378
+ schedule?: string;
379
+ /** Webhook ingress config. Mutually exclusive with `schedule:` — [[ZOD082]]. */
380
+ webhook?: WebhookTriggerConfig;
381
+ /** Full MXID the trigger posts as, e.g. `@cron:example.org`. */
382
+ as: string;
383
+ /**
384
+ * The messages this trigger can post. A flat `room:`/`mention:`/`text:`/
385
+ * `match:` at the trigger level desugars into a single-entry list here at
386
+ * config load — nothing downstream branches on which spelling was used.
387
+ */
388
+ messages: TriggerMessage[];
389
+ }
327
390
 
328
391
  interface LoadZooidConfigOptions {
329
392
  /**
@@ -333,6 +396,13 @@ interface LoadZooidConfigOptions {
333
396
  * path.
334
397
  */
335
398
  configDir?: string;
399
+ /**
400
+ * Cron-expression validator, called as `validateCron(name, expr)` and
401
+ * expected to throw on an invalid expression. `core` takes no cron
402
+ * dependency — `cli` passes croner's parser at load time. Defaults to a
403
+ * field-count check sufficient to catch malformed input at parse time.
404
+ */
405
+ validateCron?: (name: string, expr: string) => void;
336
406
  }
337
407
  declare function loadZooidConfig(yamlText: string, opts?: LoadZooidConfigOptions): ZooidConfig;
338
408
  declare function findTransport(cfg: ZooidConfig, name: string): TransportConfig | undefined;
@@ -350,6 +420,42 @@ interface FoundConfigFile {
350
420
  declare function findConfigFile(cwd: string): FoundConfigFile | null;
351
421
  declare function mergeCliFlags(base: ZooidConfig, flags: CliFlags): ZooidConfig;
352
422
 
423
+ interface MatchContext {
424
+ event: string | undefined;
425
+ body: unknown;
426
+ headers: Record<string, string>;
427
+ output: string;
428
+ }
429
+ /** Parse-check an expression. Throws on a syntax error, so a typo fails at config load. */
430
+ declare function compileMatch(expr: string): string;
431
+ /**
432
+ * Only an actual `true` fires. Everything else is "no match":
433
+ * - a CelError value (missing field, undeclared variable) — the library
434
+ * returns these rather than throwing, and a merged-PR filter legitimately
435
+ * errors on every issues delivery;
436
+ * - a truthy non-boolean (a string, a number), which must never be coerced;
437
+ * - a throw.
438
+ * Failing closed matters more here than anywhere else in the ingress: this is
439
+ * the only place an operator's typo could otherwise open a filter.
440
+ */
441
+ declare function evaluateMatch(expr: string, ctx: MatchContext): boolean;
442
+
443
+ /**
444
+ * Fill `${...}` placeholders in a trigger's `text:`. Each placeholder is a
445
+ * CEL expression over the same bindings `match:` sees — one expression
446
+ * language, not two. `${output}` is not a special case: `output` is just
447
+ * another bound variable, so a whole-payload dump still works for senders we
448
+ * control ([[ZOD081]] §2).
449
+ *
450
+ * An unresolvable placeholder (missing field, bad expression) renders as
451
+ * empty rather than throwing or pasting an error object into a room — a bad
452
+ * placeholder must not take the message down.
453
+ *
454
+ * The result is never re-scanned, so a payload that itself contains the
455
+ * literal string `${...}` cannot inject a placeholder.
456
+ */
457
+ declare function renderTemplate(template: string, ctx: MatchContext): string;
458
+
353
459
  interface RegisteredApproval {
354
460
  approvalId: string;
355
461
  agentName: string;
@@ -469,8 +575,14 @@ interface AcpAgentRegistryOptions {
469
575
  * when the workspace mount is active; falls back to `agent.workdir`.
470
576
  */
471
577
  cwd?: Record<string, string>;
578
+ /**
579
+ * Called after an ACP session is created or recovered. Context adapters
580
+ * which cannot receive an MCP spawn id (Pi) use this to resolve their
581
+ * daemon-side binding by the ACP session id instead.
582
+ */
583
+ onSessionEstablished?: (agentName: string, sessionKey: string, sessionId: string) => void;
472
584
  }
473
- type ContextSpawnFactory = (threadId: string, channelId?: string) => Promise<{
585
+ type ContextSpawnFactory = (threadId: string, channelId?: string, sessionKey?: string) => Promise<{
474
586
  name: 'zooid-context';
475
587
  command: string;
476
588
  args: string[];
@@ -531,11 +643,21 @@ interface Member {
531
643
  is_agent: boolean;
532
644
  agent_name?: string;
533
645
  }
534
- interface ChannelInfo {
646
+ /** Renamed from ChannelInfo — "channel" is not a Matrix primitive ([[ZOD084]]). */
647
+ interface RoomInfo {
535
648
  id: string;
536
649
  name: string;
537
650
  transport: 'http' | 'matrix';
538
651
  }
652
+ interface SendMessageInput {
653
+ room: string;
654
+ thread_id?: string;
655
+ text: string;
656
+ }
657
+ interface SendMessageResult {
658
+ event_id: string;
659
+ thread_id?: string;
660
+ }
539
661
  interface HistoryPage {
540
662
  messages: Message[];
541
663
  next_before?: string;
@@ -583,7 +705,102 @@ interface TransportContextProvider {
583
705
  getRecentThreads(channelId: string, opts: HistoryOptions): Promise<ThreadOverviewPage>;
584
706
  getThreadHistory(channelId: string, threadId: string, opts: HistoryOptions): Promise<HistoryPage>;
585
707
  getChannelMembers(channelId: string): Promise<Member[]>;
586
- getChannelInfo(channelId: string): Promise<ChannelInfo>;
708
+ /** Renamed from getChannelInfo. */
709
+ getRoomInfo(channelId: string): Promise<RoomInfo>;
710
+ /** Rooms this agent is bound to. Targets for sendMessage. */
711
+ getRooms(): Promise<RoomInfo[]>;
712
+ sendMessage(input: SendMessageInput): Promise<SendMessageResult>;
713
+ }
714
+
715
+ /** Harness-independent delegated-task contracts ([[ZOD072]]). */
716
+ interface TaskCallerRef {
717
+ agentName: string;
718
+ channelId: string;
719
+ threadRoot: string;
720
+ sessionKey: string;
721
+ }
722
+ interface StartTaskSpec {
723
+ agent: string;
724
+ prompt: string;
725
+ }
726
+ interface StartTasksInput {
727
+ tasks: StartTaskSpec[];
728
+ notify?: 'caller' | 'none';
729
+ }
730
+ type StartTaskResult = {
731
+ agent: string;
732
+ status: 'started';
733
+ thread_id: string;
734
+ } | {
735
+ agent: string;
736
+ status: 'refused' | 'failed';
737
+ reason: string;
738
+ attempt_id?: string;
739
+ };
740
+ interface StartTasksOutput {
741
+ results: StartTaskResult[];
742
+ notify: 'caller' | 'none';
743
+ /** Stated at the point of decision, where the model actually reads it. */
744
+ delivery: string;
745
+ }
746
+ interface CompleteTaskInput {
747
+ summary: string;
748
+ }
749
+ interface CompleteTaskOutput {
750
+ status: 'recorded' | 'already_recorded' | 'refused';
751
+ reason?: string;
752
+ }
753
+ /** What this session is, so the surface can gate itself instead of refusing later. */
754
+ interface TaskRole {
755
+ is_task_assignee: boolean;
756
+ can_start_task_threads: boolean;
757
+ }
758
+ interface TaskActions {
759
+ startTasks(caller: TaskCallerRef, input: StartTasksInput): Promise<StartTasksOutput>;
760
+ completeTask(caller: TaskCallerRef, input: CompleteTaskInput): Promise<CompleteTaskOutput>;
761
+ describeRole(caller: TaskCallerRef): Promise<TaskRole>;
762
+ }
763
+ /** One in-thread handoff inside a delegated task. */
764
+ type InvocationState = 'outstanding' | 'returned' | 'cancelled';
765
+ interface InvocationRecord {
766
+ invocationId: string;
767
+ taskId: string;
768
+ callerAgent: string;
769
+ callerSessionKey: string;
770
+ calleeAgent: string;
771
+ callEventId?: string;
772
+ calleeSessionKey?: string;
773
+ state: InvocationState;
774
+ }
775
+ /** Open human-input requests for a session. ZOD078 supplies the implementation. */
776
+ interface PendingInputRegistry {
777
+ countFor(sessionKey: string): number;
778
+ cancelFor(sessionKeys: string[]): void;
779
+ }
780
+ declare const NO_PENDING_INPUT: PendingInputRegistry;
781
+ declare const THREAD_START_FIELD = "dev.zooid.thread_start";
782
+ declare const THREAD_RESULT_FIELD = "dev.zooid.thread_result";
783
+ interface ThreadStartContent {
784
+ version: 1;
785
+ assignee: string;
786
+ attempt_id: string;
787
+ parent: {
788
+ agent: string;
789
+ thread_root: string;
790
+ session_key: string;
791
+ };
792
+ notify: 'caller' | 'none';
793
+ }
794
+ interface ThreadCompletion {
795
+ agent: string;
796
+ thread_id: string;
797
+ status: 'complete' | 'failed' | 'cancelled' | 'partial';
798
+ output?: {
799
+ type: 'message';
800
+ text: string;
801
+ };
802
+ reason?: string;
803
+ error?: string;
587
804
  }
588
805
 
589
- export { AcpAgentRegistry, type AcpAgentRegistryOptions, type AcpAgentSpec, type AcpMount, type AcpRegistry, type AcpRegistryApprovalHandler, type AcpRegistryEventHandler, type AcpRuntime, type AcpSpawnSpec, type AgentConfig, ApprovalCorrelator, type ChannelInfo, type CliFlags, type ContainerConfig, type ContextSpawnFactory, type HistoryOptions, type HistoryPage, type HttpBinding, type HttpTransportConfig, type InboundMessage, type LoadZooidConfigOptions, type MatrixBinding, type MatrixTransportConfig, type Member, type Message, type MountConfig, type RegisterOptions, type RegisteredApproval, type RoomBinding, type ThreadOverview, type ThreadOverviewPage, type ThreadRef, type Transport, type TransportConfig, type TransportContextProvider, type ZooidConfig, type ZooidContainerConfig, findConfigFile, findHttpTransport, findMatrixTransport, findTransport, loadZooidConfig, mergeCliFlags, resolveAcpAgentSpec };
806
+ export { AcpAgentRegistry, type AcpAgentRegistryOptions, type AcpAgentSpec, type AcpMount, type AcpRegistry, type AcpRegistryApprovalHandler, type AcpRegistryEventHandler, type AcpRuntime, type AcpSpawnSpec, type AgentConfig, ApprovalCorrelator, type CliFlags, type CompleteTaskInput, type CompleteTaskOutput, type ContainerConfig, type ContextSpawnFactory, type HistoryOptions, type HistoryPage, type HttpBinding, type HttpTransportConfig, type InboundMessage, type InvocationRecord, type InvocationState, type LoadZooidConfigOptions, type MatchContext, type MatrixBinding, type MatrixTransportConfig, type Member, type Message, type MountConfig, NO_PENDING_INPUT, type PendingInputRegistry, type RegisterOptions, type RegisteredApproval, type RoomBinding, type RoomInfo, type SendMessageInput, type SendMessageResult, type StartTaskResult, type StartTaskSpec, type StartTasksInput, type StartTasksOutput, THREAD_RESULT_FIELD, THREAD_START_FIELD, type TaskActions, type TaskCallerRef, type TaskRole, type ThreadCompletion, type ThreadOverview, type ThreadOverviewPage, type ThreadRef, type ThreadStartContent, type Transport, type TransportConfig, type TransportContextProvider, type TriggerConfig, type TriggerMessage, type WebhookTriggerConfig, type ZooidConfig, type ZooidContainerConfig, compileMatch, evaluateMatch, findConfigFile, findHttpTransport, findMatrixTransport, findTransport, loadZooidConfig, mergeCliFlags, renderTemplate, resolveAcpAgentSpec };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/config.ts
2
2
  import { existsSync } from "fs";
3
3
  import { isAbsolute, join, resolve as pathResolve } from "path";
4
- import { parse } from "yaml";
4
+ import { parse as parse2 } from "yaml";
5
5
  import { isPreset } from "@zooid/acp-client";
6
6
 
7
7
  // src/env-interpolation.ts
@@ -54,7 +54,23 @@ function interpolateString(value, processEnv) {
54
54
  return result.parsed?.[sentinel] ?? "";
55
55
  }
56
56
 
57
+ // src/match-expression.ts
58
+ import { parse, run } from "@bufbuild/cel";
59
+ function compileMatch(expr) {
60
+ parse(expr);
61
+ return expr;
62
+ }
63
+ function evaluateMatch(expr, ctx) {
64
+ try {
65
+ const result = run(expr, { ...ctx });
66
+ return result === true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
57
72
  // src/config.ts
73
+ var WEBHOOK_PROVIDERS = ["github", "stripe", "slack", "standard", "custom"];
58
74
  var SLUG_RE = /^[a-z0-9-]+$/;
59
75
  var AGENT_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
60
76
  var MATRIX_USER_ID_RE = /^@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+$/;
@@ -707,6 +723,203 @@ See [ZOD043].`
707
723
  }
708
724
  return result;
709
725
  }
726
+ function defaultValidateCron(name, expr) {
727
+ const parts = expr.trim().split(/\s+/);
728
+ if (parts.length < 5 || parts.length > 7) {
729
+ throw new Error(
730
+ `triggers.${name}.schedule: invalid cron expression ${JSON.stringify(expr)}`
731
+ );
732
+ }
733
+ }
734
+ function expandTriggerAs(name, as, transports) {
735
+ if (as.includes(":")) {
736
+ if (!MATRIX_USER_ID_RE.test(as)) {
737
+ throw new Error(`triggers.${name}.as: must be a full MXID (got ${JSON.stringify(as)})`);
738
+ }
739
+ return as;
740
+ }
741
+ const bare = as.startsWith("@") ? as : `@${as}`;
742
+ if (!MATRIX_USER_LOCALPART_RE.test(bare)) {
743
+ throw new Error(`triggers.${name}.as: must be a full MXID (got ${JSON.stringify(as)})`);
744
+ }
745
+ const matrixTransports = Object.values(transports).filter(
746
+ (t) => t.type === "matrix"
747
+ );
748
+ if (matrixTransports.length !== 1) {
749
+ throw new Error(
750
+ `triggers.${name}.as: "${as}" is a bare localpart, which requires exactly one matrix transport to expand against (found ${matrixTransports.length}). Use a full MXID instead.`
751
+ );
752
+ }
753
+ const serverName = deriveServerName(matrixTransports[0].user_namespace);
754
+ return `${bare}:${serverName}`;
755
+ }
756
+ function parseTriggers(raw, agents, transports, validateCron, processEnv, configDir) {
757
+ if (raw === void 0 || raw === null) return {};
758
+ if (typeof raw !== "object" || Array.isArray(raw)) {
759
+ throw new Error("triggers: must be a mapping");
760
+ }
761
+ const result = {};
762
+ for (const [name, val] of Object.entries(raw)) {
763
+ if (!val || typeof val !== "object" || Array.isArray(val)) {
764
+ throw new Error(`triggers.${name} must be a mapping`);
765
+ }
766
+ const t = val;
767
+ if (t.run !== void 0) {
768
+ throw new Error(
769
+ `triggers.${name}.run: is not supported \u2014 a trigger posts a message and the agent runs what needs running. See [ZOD081] \xA7Concept.`
770
+ );
771
+ }
772
+ if (t.schedule === void 0 && t.webhook === void 0) {
773
+ throw new Error(`triggers.${name}: must specify schedule: or webhook:`);
774
+ }
775
+ if (t.schedule !== void 0 && t.webhook !== void 0) {
776
+ throw new Error(`triggers.${name}: specify either schedule: or webhook:, not both`);
777
+ }
778
+ let schedule;
779
+ if (t.schedule !== void 0) {
780
+ if (typeof t.schedule !== "string" || t.schedule.length === 0) {
781
+ throw new Error(`triggers.${name}.schedule: must be a non-empty string`);
782
+ }
783
+ validateCron(name, t.schedule);
784
+ schedule = t.schedule;
785
+ }
786
+ let webhook;
787
+ if (t.webhook !== void 0) {
788
+ webhook = parseWebhookTrigger(name, t.webhook, processEnv, configDir);
789
+ }
790
+ if (typeof t.as !== "string" || t.as.length === 0) {
791
+ throw new Error(`triggers.${name}.as: must be a non-empty string`);
792
+ }
793
+ const as = expandTriggerAs(name, t.as, transports);
794
+ const hasFlat = t.room !== void 0 || t.mention !== void 0 || t.text !== void 0 || t.match !== void 0;
795
+ const hasMessages = t.messages !== void 0;
796
+ if (hasFlat && hasMessages) {
797
+ throw new Error(
798
+ `triggers.${name}: specify either room:/mention:/text: or messages:, not both`
799
+ );
800
+ }
801
+ if (!hasFlat && !hasMessages) {
802
+ throw new Error(`triggers.${name}: must specify room:/mention:/text: or messages:`);
803
+ }
804
+ let rawMessages;
805
+ if (hasFlat) {
806
+ if (t.match !== void 0 && !webhook) {
807
+ throw new Error(`triggers.${name}.match: only applies to a webhook: trigger`);
808
+ }
809
+ rawMessages = [{ room: t.room, mention: t.mention, text: t.text, match: t.match }];
810
+ } else {
811
+ if (!Array.isArray(t.messages)) {
812
+ throw new Error(`triggers.${name}.messages: must be a list`);
813
+ }
814
+ if (t.messages.length === 0) {
815
+ throw new Error(`triggers.${name}.messages: must not be empty`);
816
+ }
817
+ rawMessages = t.messages;
818
+ }
819
+ const messages = rawMessages.map(
820
+ (m, i) => parseTriggerMessage(name, i, m, agents, as, !!webhook)
821
+ );
822
+ const entry = { as, messages };
823
+ if (schedule !== void 0) entry.schedule = schedule;
824
+ if (webhook !== void 0) entry.webhook = webhook;
825
+ result[name] = entry;
826
+ }
827
+ return result;
828
+ }
829
+ function parseTriggerMessage(name, index, raw, agents, as, hasWebhook) {
830
+ const label = `triggers.${name}.messages[${index}]`;
831
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
832
+ throw new Error(`${label}: must be a mapping`);
833
+ }
834
+ const m = raw;
835
+ if (typeof m.room !== "string" || m.room.length === 0) {
836
+ throw new Error(`${label}.room: must be a non-empty string`);
837
+ }
838
+ if (!MATRIX_ROOM_IDENT_RE.test(m.room)) {
839
+ throw new Error(`${label}.room: must start with '#' or '!' (got ${JSON.stringify(m.room)})`);
840
+ }
841
+ if (typeof m.mention !== "string" || m.mention.length === 0) {
842
+ throw new Error(`${label}.mention: must be a non-empty string`);
843
+ }
844
+ const mentionedAgent = agents[m.mention];
845
+ if (!mentionedAgent) {
846
+ throw new Error(`${label}.mention: unknown agent "${m.mention}"`);
847
+ }
848
+ if (!mentionedAgent.matrix) {
849
+ throw new Error(
850
+ `${label}.mention: agent "${m.mention}" has no matrix: binding \u2014 a trigger posts through Matrix, so the mentioned agent must be matrix-bound.`
851
+ );
852
+ }
853
+ if (as === mentionedAgent.matrix.user_id) {
854
+ throw new Error(
855
+ `triggers.${name}.as: must not equal the mentioned agent's own MXID (${as}) \u2014 a message an agent sends never routes back to itself, so this trigger would silently never wake "${m.mention}". Post as a different identity (a dedicated bot, or another agent's).`
856
+ );
857
+ }
858
+ if (typeof m.text !== "string" || m.text.length === 0) {
859
+ throw new Error(`${label}.text: must be a non-empty string`);
860
+ }
861
+ const message = { room: m.room, mention: m.mention, text: m.text };
862
+ if (m.match !== void 0) {
863
+ if (typeof m.match !== "string" || m.match.length === 0) {
864
+ throw new Error(`${label}.match: must be a non-empty string`);
865
+ }
866
+ if (!hasWebhook) {
867
+ throw new Error(`${label}.match: only applies to a webhook: trigger`);
868
+ }
869
+ try {
870
+ message.match = compileMatch(m.match);
871
+ } catch (err) {
872
+ throw new Error(`${label}.match: ${err.message}`);
873
+ }
874
+ }
875
+ return message;
876
+ }
877
+ function parseWebhookTrigger(name, raw, processEnv, configDir) {
878
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
879
+ throw new Error(`triggers.${name}.webhook: must be a mapping`);
880
+ }
881
+ const w = raw;
882
+ if (typeof w.provider !== "string" || w.provider.length === 0) {
883
+ throw new Error(`triggers.${name}.webhook.provider: must be a non-empty string`);
884
+ }
885
+ if (!WEBHOOK_PROVIDERS.includes(w.provider)) {
886
+ throw new Error(
887
+ `triggers.${name}.webhook.provider: unknown provider ${JSON.stringify(w.provider)} (expected one of: ${WEBHOOK_PROVIDERS.join(", ")})`
888
+ );
889
+ }
890
+ const provider = w.provider;
891
+ if (w.event !== void 0) {
892
+ throw new Error(`triggers.${name}.webhook.event: no longer supported \u2014 use match:`);
893
+ }
894
+ if (typeof w.secret !== "string" || w.secret.length === 0) {
895
+ throw new Error(`triggers.${name}.webhook.secret: is required`);
896
+ }
897
+ const secret = interpolateString(w.secret, processEnv);
898
+ const config = { provider, secret };
899
+ if (provider !== "custom") {
900
+ if (w.verify !== void 0) {
901
+ throw new Error(
902
+ `triggers.${name}.webhook.verify: only applies to provider: custom (this trigger uses provider: ${provider}, whose signing scheme is built in)`
903
+ );
904
+ }
905
+ return config;
906
+ }
907
+ if (typeof w.verify !== "string" || w.verify.length === 0) {
908
+ throw new Error(
909
+ `triggers.${name}.webhook.verify: is required when provider: custom (path to a module exporting the verifier function)`
910
+ );
911
+ }
912
+ if (isAbsolute(w.verify)) {
913
+ config.verify = w.verify;
914
+ } else if (!configDir) {
915
+ throw new Error(
916
+ `triggers.${name}.webhook.verify: relative path ${JSON.stringify(w.verify)} requires configDir (zooid.yaml directory) \u2014 pass it via loadZooidConfig(yaml, { configDir })`
917
+ );
918
+ } else {
919
+ config.verify = pathResolve(configDir, w.verify);
920
+ }
921
+ return config;
922
+ }
710
923
  function parseRuntime(raw) {
711
924
  const runtime = raw ?? "docker";
712
925
  if (runtime !== "local" && runtime !== "docker" && runtime !== "podman") {
@@ -724,7 +937,7 @@ function zooidHooks(raw) {
724
937
  return out;
725
938
  }
726
939
  function loadZooidConfig(yamlText, opts = {}) {
727
- const raw = parse(yamlText) ?? {};
940
+ const raw = parse2(yamlText) ?? {};
728
941
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
729
942
  throw new Error("zooid.yaml must be a YAML object");
730
943
  }
@@ -767,11 +980,20 @@ function loadZooidConfig(yamlText, opts = {}) {
767
980
  const transports = parseTransports(r.transports, processEnv, workstation);
768
981
  const hooks = zooidHooks(r);
769
982
  const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv, opts.configDir);
983
+ const triggers = parseTriggers(
984
+ r.triggers,
985
+ agents,
986
+ transports,
987
+ opts.validateCron ?? defaultValidateCron,
988
+ processEnv,
989
+ opts.configDir
990
+ );
770
991
  const cfg = {
771
992
  runtime,
772
993
  transports,
773
994
  agents,
774
- hooks
995
+ hooks,
996
+ triggers
775
997
  };
776
998
  if (workstation !== void 0) cfg.workstation = workstation;
777
999
  if (r.container !== void 0 && r.container !== null) {
@@ -834,7 +1056,8 @@ function mergeCliFlags(base, flags) {
834
1056
  runtime,
835
1057
  transports: base.transports,
836
1058
  agents: base.agents,
837
- hooks: { ...base.hooks }
1059
+ hooks: { ...base.hooks },
1060
+ triggers: base.triggers
838
1061
  };
839
1062
  if (runtime === "docker" || runtime === "podman") {
840
1063
  const image = flags.image ?? base.container?.image;
@@ -847,6 +1070,22 @@ function mergeCliFlags(base, flags) {
847
1070
  return merged;
848
1071
  }
849
1072
 
1073
+ // src/render-template.ts
1074
+ import { run as run2, isCelError } from "@bufbuild/cel";
1075
+ var PLACEHOLDER_RE = /\$\{([\s\S]+?)\}/g;
1076
+ function renderTemplate(template, ctx) {
1077
+ return template.replace(PLACEHOLDER_RE, (_match, expr) => {
1078
+ let result;
1079
+ try {
1080
+ result = run2(expr, { ...ctx });
1081
+ } catch {
1082
+ return "";
1083
+ }
1084
+ if (result === void 0 || isCelError(result)) return "";
1085
+ return String(result);
1086
+ });
1087
+ }
1088
+
850
1089
  // src/acp-registry.ts
851
1090
  import { mkdirSync } from "fs";
852
1091
  import { join as join2 } from "path";
@@ -906,7 +1145,9 @@ var AcpAgentRegistry = class {
906
1145
  async ensureSession(name, threadId, channelId, contextThreadId) {
907
1146
  if (!this.hasAgent(name)) throw new Error(`unknown agent: ${name}`);
908
1147
  const client = await this.ensureClient(name);
909
- return client.ensureSession(threadId, channelId, contextThreadId);
1148
+ const sessionId = await client.ensureSession(threadId, channelId, contextThreadId);
1149
+ this.opts.onSessionEstablished?.(name, threadId, sessionId);
1150
+ return sessionId;
910
1151
  }
911
1152
  endSession(name, threadId) {
912
1153
  if (!this.hasAgent(name)) return;
@@ -930,9 +1171,7 @@ var AcpAgentRegistry = class {
930
1171
  return client.prompt(input);
931
1172
  }
932
1173
  async stopAll() {
933
- await Promise.allSettled(
934
- [...this.clients.values()].map((c) => c.stop())
935
- );
1174
+ await Promise.allSettled([...this.clients.values()].map((c) => c.stop()));
936
1175
  this.clients.clear();
937
1176
  }
938
1177
  async ensureClient(name) {
@@ -1069,15 +1308,30 @@ var ApprovalCorrelator = class extends EventEmitter {
1069
1308
  };
1070
1309
  }
1071
1310
  };
1311
+
1312
+ // src/task-actions.ts
1313
+ var NO_PENDING_INPUT = {
1314
+ countFor: () => 0,
1315
+ cancelFor: () => {
1316
+ }
1317
+ };
1318
+ var THREAD_START_FIELD = "dev.zooid.thread_start";
1319
+ var THREAD_RESULT_FIELD = "dev.zooid.thread_result";
1072
1320
  export {
1073
1321
  AcpAgentRegistry,
1074
1322
  ApprovalCorrelator,
1323
+ NO_PENDING_INPUT,
1324
+ THREAD_RESULT_FIELD,
1325
+ THREAD_START_FIELD,
1326
+ compileMatch,
1327
+ evaluateMatch,
1075
1328
  findConfigFile,
1076
1329
  findHttpTransport,
1077
1330
  findMatrixTransport,
1078
1331
  findTransport,
1079
1332
  loadZooidConfig,
1080
1333
  mergeCliFlags,
1334
+ renderTemplate,
1081
1335
  resolveAcpAgentSpec
1082
1336
  };
1083
1337
  //# sourceMappingURL=index.js.map