switchroom 0.15.4 → 0.15.6

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.
@@ -11082,6 +11082,25 @@ var SessionContinuitySchema = exports_external.object({
11082
11082
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
11083
11083
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
11084
11084
  }).optional();
11085
+ var webhookDispatchRule = exports_external.object({
11086
+ description: exports_external.string().optional(),
11087
+ match: exports_external.object({
11088
+ event: exports_external.string(),
11089
+ actions: exports_external.array(exports_external.string()).optional(),
11090
+ labels_any: exports_external.array(exports_external.string()).optional(),
11091
+ labels_all: exports_external.array(exports_external.string()).optional(),
11092
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
11093
+ assignee_any: exports_external.array(exports_external.string()).optional(),
11094
+ mentions_any: exports_external.array(exports_external.string()).optional()
11095
+ }).passthrough(),
11096
+ prompt: exports_external.string(),
11097
+ cooldown: exports_external.string().optional(),
11098
+ quiet_hours: exports_external.object({
11099
+ start: exports_external.number().int().min(0).max(23),
11100
+ end: exports_external.number().int().min(0).max(23),
11101
+ tz: exports_external.string().optional()
11102
+ }).optional()
11103
+ });
11085
11104
  var TelegramChannelSchema = exports_external.object({
11086
11105
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
11087
11106
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -11117,26 +11136,12 @@ var TelegramChannelSchema = exports_external.object({
11117
11136
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
11118
11137
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
11119
11138
  }).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
11120
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11139
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11121
11140
  webhook_dispatch: exports_external.object({
11122
- github: exports_external.array(exports_external.object({
11123
- description: exports_external.string().optional(),
11124
- match: exports_external.object({
11125
- event: exports_external.string(),
11126
- actions: exports_external.array(exports_external.string()).optional(),
11127
- labels_any: exports_external.array(exports_external.string()).optional(),
11128
- labels_all: exports_external.array(exports_external.string()).optional(),
11129
- exclude_authors: exports_external.array(exports_external.string()).optional()
11130
- }).passthrough(),
11131
- prompt: exports_external.string(),
11132
- cooldown: exports_external.string().optional(),
11133
- quiet_hours: exports_external.object({
11134
- start: exports_external.number().int().min(0).max(23),
11135
- end: exports_external.number().int().min(0).max(23),
11136
- tz: exports_external.string().optional()
11137
- }).optional()
11138
- })).optional()
11139
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11141
+ github: exports_external.array(webhookDispatchRule).optional(),
11142
+ generic: exports_external.array(webhookDispatchRule).optional(),
11143
+ linear: exports_external.array(webhookDispatchRule).optional()
11144
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source — 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11140
11145
  webhook_rate_limit: exports_external.object({
11141
11146
  rpm: exports_external.number().int().positive()
11142
11147
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -11313,7 +11318,7 @@ var AgentSchema = exports_external.object({
11313
11318
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
11314
11319
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
11315
11320
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
11316
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11321
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11317
11322
  voice_in: exports_external.object({
11318
11323
  enabled: exports_external.boolean().optional(),
11319
11324
  provider: exports_external.enum(["openai"]).optional(),
@@ -11082,6 +11082,25 @@ var SessionContinuitySchema = exports_external.object({
11082
11082
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
11083
11083
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
11084
11084
  }).optional();
11085
+ var webhookDispatchRule = exports_external.object({
11086
+ description: exports_external.string().optional(),
11087
+ match: exports_external.object({
11088
+ event: exports_external.string(),
11089
+ actions: exports_external.array(exports_external.string()).optional(),
11090
+ labels_any: exports_external.array(exports_external.string()).optional(),
11091
+ labels_all: exports_external.array(exports_external.string()).optional(),
11092
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
11093
+ assignee_any: exports_external.array(exports_external.string()).optional(),
11094
+ mentions_any: exports_external.array(exports_external.string()).optional()
11095
+ }).passthrough(),
11096
+ prompt: exports_external.string(),
11097
+ cooldown: exports_external.string().optional(),
11098
+ quiet_hours: exports_external.object({
11099
+ start: exports_external.number().int().min(0).max(23),
11100
+ end: exports_external.number().int().min(0).max(23),
11101
+ tz: exports_external.string().optional()
11102
+ }).optional()
11103
+ });
11085
11104
  var TelegramChannelSchema = exports_external.object({
11086
11105
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
11087
11106
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -11117,26 +11136,12 @@ var TelegramChannelSchema = exports_external.object({
11117
11136
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
11118
11137
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
11119
11138
  }).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
11120
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11139
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11121
11140
  webhook_dispatch: exports_external.object({
11122
- github: exports_external.array(exports_external.object({
11123
- description: exports_external.string().optional(),
11124
- match: exports_external.object({
11125
- event: exports_external.string(),
11126
- actions: exports_external.array(exports_external.string()).optional(),
11127
- labels_any: exports_external.array(exports_external.string()).optional(),
11128
- labels_all: exports_external.array(exports_external.string()).optional(),
11129
- exclude_authors: exports_external.array(exports_external.string()).optional()
11130
- }).passthrough(),
11131
- prompt: exports_external.string(),
11132
- cooldown: exports_external.string().optional(),
11133
- quiet_hours: exports_external.object({
11134
- start: exports_external.number().int().min(0).max(23),
11135
- end: exports_external.number().int().min(0).max(23),
11136
- tz: exports_external.string().optional()
11137
- }).optional()
11138
- })).optional()
11139
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11141
+ github: exports_external.array(webhookDispatchRule).optional(),
11142
+ generic: exports_external.array(webhookDispatchRule).optional(),
11143
+ linear: exports_external.array(webhookDispatchRule).optional()
11144
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source — 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11140
11145
  webhook_rate_limit: exports_external.object({
11141
11146
  rpm: exports_external.number().int().positive()
11142
11147
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -11313,7 +11318,7 @@ var AgentSchema = exports_external.object({
11313
11318
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
11314
11319
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
11315
11320
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
11316
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11321
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11317
11322
  voice_in: exports_external.object({
11318
11323
  enabled: exports_external.boolean().optional(),
11319
11324
  provider: exports_external.enum(["openai"]).optional(),
@@ -11830,6 +11830,25 @@ var SessionContinuitySchema = exports_external.object({
11830
11830
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
11831
11831
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
11832
11832
  }).optional();
11833
+ var webhookDispatchRule = exports_external.object({
11834
+ description: exports_external.string().optional(),
11835
+ match: exports_external.object({
11836
+ event: exports_external.string(),
11837
+ actions: exports_external.array(exports_external.string()).optional(),
11838
+ labels_any: exports_external.array(exports_external.string()).optional(),
11839
+ labels_all: exports_external.array(exports_external.string()).optional(),
11840
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
11841
+ assignee_any: exports_external.array(exports_external.string()).optional(),
11842
+ mentions_any: exports_external.array(exports_external.string()).optional()
11843
+ }).passthrough(),
11844
+ prompt: exports_external.string(),
11845
+ cooldown: exports_external.string().optional(),
11846
+ quiet_hours: exports_external.object({
11847
+ start: exports_external.number().int().min(0).max(23),
11848
+ end: exports_external.number().int().min(0).max(23),
11849
+ tz: exports_external.string().optional()
11850
+ }).optional()
11851
+ });
11833
11852
  var TelegramChannelSchema = exports_external.object({
11834
11853
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
11835
11854
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -11865,26 +11884,12 @@ var TelegramChannelSchema = exports_external.object({
11865
11884
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
11866
11885
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short \u2014 the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
11867
11886
  }).optional().describe("Interrupt timing \u2014 how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
11868
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
11887
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
11869
11888
  webhook_dispatch: exports_external.object({
11870
- github: exports_external.array(exports_external.object({
11871
- description: exports_external.string().optional(),
11872
- match: exports_external.object({
11873
- event: exports_external.string(),
11874
- actions: exports_external.array(exports_external.string()).optional(),
11875
- labels_any: exports_external.array(exports_external.string()).optional(),
11876
- labels_all: exports_external.array(exports_external.string()).optional(),
11877
- exclude_authors: exports_external.array(exports_external.string()).optional()
11878
- }).passthrough(),
11879
- prompt: exports_external.string(),
11880
- cooldown: exports_external.string().optional(),
11881
- quiet_hours: exports_external.object({
11882
- start: exports_external.number().int().min(0).max(23),
11883
- end: exports_external.number().int().min(0).max(23),
11884
- tz: exports_external.string().optional()
11885
- }).optional()
11886
- })).optional()
11887
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
11889
+ github: exports_external.array(webhookDispatchRule).optional(),
11890
+ generic: exports_external.array(webhookDispatchRule).optional(),
11891
+ linear: exports_external.array(webhookDispatchRule).optional()
11892
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source \u2014 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
11888
11893
  webhook_rate_limit: exports_external.object({
11889
11894
  rpm: exports_external.number().int().positive()
11890
11895
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -12061,7 +12066,7 @@ var AgentSchema = exports_external.object({
12061
12066
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (\u2264140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml \u2014 never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
12062
12067
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` \u2014 a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
12063
12068
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
12064
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED \u2014 moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
12069
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED \u2014 moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
12065
12070
  voice_in: exports_external.object({
12066
12071
  enabled: exports_external.boolean().optional(),
12067
12072
  provider: exports_external.enum(["openai"]).optional(),
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
13520
13520
  });
13521
13521
 
13522
13522
  // src/config/schema.ts
13523
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
13523
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13646,6 +13646,25 @@ var init_schema = __esm(() => {
13646
13646
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
13647
13647
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
13648
13648
  }).optional();
13649
+ webhookDispatchRule = exports_external.object({
13650
+ description: exports_external.string().optional(),
13651
+ match: exports_external.object({
13652
+ event: exports_external.string(),
13653
+ actions: exports_external.array(exports_external.string()).optional(),
13654
+ labels_any: exports_external.array(exports_external.string()).optional(),
13655
+ labels_all: exports_external.array(exports_external.string()).optional(),
13656
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
13657
+ assignee_any: exports_external.array(exports_external.string()).optional(),
13658
+ mentions_any: exports_external.array(exports_external.string()).optional()
13659
+ }).passthrough(),
13660
+ prompt: exports_external.string(),
13661
+ cooldown: exports_external.string().optional(),
13662
+ quiet_hours: exports_external.object({
13663
+ start: exports_external.number().int().min(0).max(23),
13664
+ end: exports_external.number().int().min(0).max(23),
13665
+ tz: exports_external.string().optional()
13666
+ }).optional()
13667
+ });
13649
13668
  TelegramChannelSchema = exports_external.object({
13650
13669
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
13651
13670
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -13681,26 +13700,12 @@ var init_schema = __esm(() => {
13681
13700
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
13682
13701
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short \u2014 the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
13683
13702
  }).optional().describe("Interrupt timing \u2014 how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
13684
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
13703
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
13685
13704
  webhook_dispatch: exports_external.object({
13686
- github: exports_external.array(exports_external.object({
13687
- description: exports_external.string().optional(),
13688
- match: exports_external.object({
13689
- event: exports_external.string(),
13690
- actions: exports_external.array(exports_external.string()).optional(),
13691
- labels_any: exports_external.array(exports_external.string()).optional(),
13692
- labels_all: exports_external.array(exports_external.string()).optional(),
13693
- exclude_authors: exports_external.array(exports_external.string()).optional()
13694
- }).passthrough(),
13695
- prompt: exports_external.string(),
13696
- cooldown: exports_external.string().optional(),
13697
- quiet_hours: exports_external.object({
13698
- start: exports_external.number().int().min(0).max(23),
13699
- end: exports_external.number().int().min(0).max(23),
13700
- tz: exports_external.string().optional()
13701
- }).optional()
13702
- })).optional()
13703
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
13705
+ github: exports_external.array(webhookDispatchRule).optional(),
13706
+ generic: exports_external.array(webhookDispatchRule).optional(),
13707
+ linear: exports_external.array(webhookDispatchRule).optional()
13708
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source \u2014 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
13704
13709
  webhook_rate_limit: exports_external.object({
13705
13710
  rpm: exports_external.number().int().positive()
13706
13711
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -13877,7 +13882,7 @@ var init_schema = __esm(() => {
13877
13882
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (\u2264140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml \u2014 never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
13878
13883
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` \u2014 a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
13879
13884
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
13880
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED \u2014 moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
13885
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED \u2014 moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
13881
13886
  voice_in: exports_external.object({
13882
13887
  enabled: exports_external.boolean().optional(),
13883
13888
  provider: exports_external.enum(["openai"]).optional(),
@@ -50199,8 +50204,8 @@ var {
50199
50204
  } = import__.default;
50200
50205
 
50201
50206
  // src/build-info.ts
50202
- var VERSION = "0.15.4";
50203
- var COMMIT_SHA = "dd68b93e";
50207
+ var VERSION = "0.15.6";
50208
+ var COMMIT_SHA = "3ae297b9";
50204
50209
 
50205
50210
  // src/cli/agent.ts
50206
50211
  init_source();
@@ -66799,6 +66804,7 @@ import { existsSync as existsSync43, readFileSync as readFileSync38, writeFileSy
66799
66804
  import { join as join38 } from "node:path";
66800
66805
 
66801
66806
  // src/web/webhook-dispatch.ts
66807
+ var DISPATCH_SOURCES = ["github", "generic", "linear"];
66802
66808
  function renderTemplate2(template, ctx) {
66803
66809
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => ctx[key] ?? "");
66804
66810
  }
@@ -66815,12 +66821,86 @@ function buildGithubContext(eventType, payload) {
66815
66821
  const rawLabels = obj?.labels ?? [];
66816
66822
  const labels = rawLabels.map((l) => String(l.name ?? "")).join(", ");
66817
66823
  const action = String(payload.action ?? "");
66818
- return { repo, number, title, html_url, author, labels, action, event: eventType };
66824
+ const assignee = String(obj?.assignee?.login ?? "");
66825
+ return {
66826
+ repo,
66827
+ number,
66828
+ title,
66829
+ html_url,
66830
+ author,
66831
+ labels,
66832
+ action,
66833
+ event: eventType,
66834
+ assignee,
66835
+ body: title
66836
+ };
66837
+ }
66838
+ function buildLinearContext(eventType, payload) {
66839
+ const data = payload.data ?? {};
66840
+ const issue = data.issue ?? {};
66841
+ const number = String(data.identifier ?? issue.identifier ?? "");
66842
+ const title = String(data.title ?? issue.title ?? "");
66843
+ const html_url = String(payload.url ?? "");
66844
+ const action = String(payload.action ?? "");
66845
+ const assignee = String(data.assignee?.displayName ?? data.assignee?.name ?? "");
66846
+ const author = String(data.user?.displayName ?? data.user?.name ?? "");
66847
+ const rawLabels = data.labels ?? [];
66848
+ const labels = rawLabels.map((l) => String(l.name ?? "")).join(", ");
66849
+ const commentBody = String(data.body ?? "");
66850
+ const body = [title, commentBody].filter(Boolean).join(`
66851
+ `);
66852
+ return {
66853
+ repo: "linear",
66854
+ number,
66855
+ title,
66856
+ html_url,
66857
+ author,
66858
+ labels,
66859
+ action,
66860
+ event: eventType,
66861
+ assignee,
66862
+ body
66863
+ };
66864
+ }
66865
+ function buildGenericContext(source, payload) {
66866
+ const title = typeof payload.title === "string" ? payload.title : typeof payload.message === "string" ? payload.message : typeof payload.text === "string" ? payload.text : "";
66867
+ const action = String(payload.action ?? "");
66868
+ const html_url = typeof payload.url === "string" ? payload.url : "";
66869
+ const number = String(payload.id ?? payload.number ?? "");
66870
+ const author = String(payload.author ?? payload.user ?? "");
66871
+ const assignee = String(payload.assignee ?? "");
66872
+ const rawLabels = Array.isArray(payload.labels) ? payload.labels.map((l) => typeof l === "string" ? l : String(l?.name ?? "")) : [];
66873
+ const labels = rawLabels.join(", ");
66874
+ return {
66875
+ repo: source,
66876
+ number,
66877
+ title,
66878
+ html_url,
66879
+ author,
66880
+ labels,
66881
+ action,
66882
+ event: source,
66883
+ assignee,
66884
+ body: title
66885
+ };
66886
+ }
66887
+ function buildContext(source, eventType, payload) {
66888
+ switch (source) {
66889
+ case "linear":
66890
+ return buildLinearContext(eventType, payload);
66891
+ case "generic":
66892
+ return buildGenericContext(eventType, payload);
66893
+ default:
66894
+ return buildGithubContext(eventType, payload);
66895
+ }
66819
66896
  }
66820
- function matchesRule(eventType, payload, matcher) {
66897
+ function labelSetFromContext(ctx) {
66898
+ return new Set(ctx.labels.split(",").map((l) => l.trim()).filter(Boolean));
66899
+ }
66900
+ function matchesRule(source, eventType, payload, matcher) {
66821
66901
  if (matcher.event !== eventType)
66822
66902
  return false;
66823
- const ctx = buildGithubContext(eventType, payload);
66903
+ const ctx = buildContext(source, eventType, payload);
66824
66904
  if (matcher.actions && matcher.actions.length > 0) {
66825
66905
  if (!matcher.actions.includes(ctx.action))
66826
66906
  return false;
@@ -66829,22 +66909,24 @@ function matchesRule(eventType, payload, matcher) {
66829
66909
  if (matcher.exclude_authors.includes(ctx.author))
66830
66910
  return false;
66831
66911
  }
66912
+ if (matcher.assignee_any && matcher.assignee_any.length > 0) {
66913
+ if (!matcher.assignee_any.includes(ctx.assignee))
66914
+ return false;
66915
+ }
66916
+ if (matcher.mentions_any && matcher.mentions_any.length > 0) {
66917
+ const hay = ctx.body.toLowerCase();
66918
+ const hasMention = matcher.mentions_any.some((m) => hay.includes(m.toLowerCase()));
66919
+ if (!hasMention)
66920
+ return false;
66921
+ }
66832
66922
  if (matcher.labels_any && matcher.labels_any.length > 0) {
66833
- const pr = payload.pull_request;
66834
- const issue = payload.issue;
66835
- const rawLabels = (pr ?? issue)?.labels ?? [];
66836
- const labelNames = new Set(rawLabels.map((l) => String(l.name ?? "")));
66837
- const hasAny = matcher.labels_any.some((l) => labelNames.has(l));
66838
- if (!hasAny)
66923
+ const labelNames = labelSetFromContext(ctx);
66924
+ if (!matcher.labels_any.some((l) => labelNames.has(l)))
66839
66925
  return false;
66840
66926
  }
66841
66927
  if (matcher.labels_all && matcher.labels_all.length > 0) {
66842
- const pr = payload.pull_request;
66843
- const issue = payload.issue;
66844
- const rawLabels = (pr ?? issue)?.labels ?? [];
66845
- const labelNames = new Set(rawLabels.map((l) => String(l.name ?? "")));
66846
- const hasAll = matcher.labels_all.every((l) => labelNames.has(l));
66847
- if (!hasAll)
66928
+ const labelNames = labelSetFromContext(ctx);
66929
+ if (!matcher.labels_all.every((l) => labelNames.has(l)))
66848
66930
  return false;
66849
66931
  }
66850
66932
  return true;
@@ -67371,7 +67453,11 @@ function registerDispatchVerb(tg, _program) {
67371
67453
  } catch (err) {
67372
67454
  fail(`Could not read payload file '${opts.payload}': ${err.message}`);
67373
67455
  }
67374
- const rules = opts.source === "github" ? dispatchConfig.github ?? [] : [];
67456
+ if (!DISPATCH_SOURCES.includes(opts.source)) {
67457
+ console.log(source_default.yellow(`Source '${opts.source}' does not support dispatch (known: ${DISPATCH_SOURCES.join(", ")}).`));
67458
+ return;
67459
+ }
67460
+ const rules = dispatchConfig[opts.source] ?? [];
67375
67461
  if (rules.length === 0) {
67376
67462
  console.log(source_default.yellow(`No dispatch rules for source '${opts.source}'.`));
67377
67463
  return;
@@ -67380,7 +67466,7 @@ function registerDispatchVerb(tg, _program) {
67380
67466
  const now = new Date;
67381
67467
  for (let i = 0;i < rules.length; i++) {
67382
67468
  const rule = rules[i];
67383
- const matched = matchesRule(opts.event, payload, rule.match);
67469
+ const matched = matchesRule(opts.source, opts.event, payload, rule.match);
67384
67470
  const prefix = matched ? source_default.green("\u2713 MATCH") : source_default.dim("\u2717 no match");
67385
67471
  const desc = rule.description ? ` \u2014 ${rule.description}` : ` \u2014 rule ${i}`;
67386
67472
  console.log(`${prefix} rule ${i}${desc}`);
@@ -67395,7 +67481,7 @@ function registerDispatchVerb(tg, _program) {
67395
67481
  const ms = parseDurationMs(rule.cooldown);
67396
67482
  console.log(` cooldown: ${rule.cooldown} (${ms}ms) \u2014 state tracked in webhook-cooldown.json`);
67397
67483
  }
67398
- const ctx = buildGithubContext(opts.event, payload);
67484
+ const ctx = buildContext(opts.source, opts.event, payload);
67399
67485
  const rendered = renderTemplate2(rule.prompt, ctx);
67400
67486
  console.log(source_default.bold(" rendered prompt:"));
67401
67487
  for (const line of rendered.split(`
@@ -73801,6 +73887,26 @@ function verifyGithubSignature(body, signatureHeader, secret) {
73801
73887
  return { ok: false, reason: "signature-mismatch" };
73802
73888
  return { ok: true };
73803
73889
  }
73890
+ function verifyLinearSignature(body, signatureHeader, secret) {
73891
+ if (!secret || secret.length === 0) {
73892
+ return { ok: false, reason: "no-secret-configured" };
73893
+ }
73894
+ if (!signatureHeader) {
73895
+ return { ok: false, reason: "no-signature-header" };
73896
+ }
73897
+ const provided = signatureHeader.trim();
73898
+ if (!/^[0-9a-f]{64}$/.test(provided)) {
73899
+ return { ok: false, reason: "malformed-hex" };
73900
+ }
73901
+ const expected = createHmac2("sha256", secret).update(body).digest("hex");
73902
+ const a = Buffer.from(provided, "utf-8");
73903
+ const b = Buffer.from(expected, "utf-8");
73904
+ if (a.length !== b.length)
73905
+ return { ok: false, reason: "length-mismatch" };
73906
+ if (!timingSafeEqual(a, b))
73907
+ return { ok: false, reason: "signature-mismatch" };
73908
+ return { ok: true };
73909
+ }
73804
73910
  function verifyBearerToken(authHeader, secret) {
73805
73911
  if (!secret || secret.length === 0) {
73806
73912
  return { ok: false, reason: "no-secret-configured" };
@@ -73866,6 +73972,41 @@ ${compare2}` : ""}`,
73866
73972
  };
73867
73973
  }
73868
73974
  }
73975
+ function renderLinearEvent(eventType, payload) {
73976
+ const action = String(payload.action ?? "");
73977
+ const url = typeof payload.url === "string" ? payload.url : "";
73978
+ const data = payload.data ?? {};
73979
+ switch (eventType) {
73980
+ case "issue": {
73981
+ const id = String(data.identifier ?? "?");
73982
+ const title = String(data.title ?? "");
73983
+ const assignee = data.assignee?.displayName ?? data.assignee?.name ?? "";
73984
+ return {
73985
+ text: `\uD83D\uDCD0 <b>Linear</b> issue ${escapeHtml3(id)} ${escapeHtml3(action)}` + `${assignee ? ` \u2192 @${escapeHtml3(assignee)}` : ""}
73986
+ ` + `${escapeHtml3(title)}${url ? `
73987
+ ${url}` : ""}`,
73988
+ disableLinkPreview: true
73989
+ };
73990
+ }
73991
+ case "comment": {
73992
+ const issue = data.issue ?? {};
73993
+ const id = String(issue.identifier ?? "?");
73994
+ const body = String(data.body ?? "").slice(0, 200);
73995
+ return {
73996
+ text: `\uD83D\uDCD0 <b>Linear</b> comment ${escapeHtml3(action)} on ${escapeHtml3(id)}
73997
+ ` + `${escapeHtml3(body)}${url ? `
73998
+ ${url}` : ""}`,
73999
+ disableLinkPreview: true
74000
+ };
74001
+ }
74002
+ default:
74003
+ return {
74004
+ text: `\uD83D\uDCD0 <b>Linear</b> ${escapeHtml3(eventType)} ${escapeHtml3(action)}` + `${url ? `
74005
+ ${url}` : ""}`,
74006
+ disableLinkPreview: true
74007
+ };
74008
+ }
74009
+ }
73869
74010
  function renderGenericEvent(source, payload) {
73870
74011
  const title = typeof payload.title === "string" ? payload.title : typeof payload.message === "string" ? payload.message : typeof payload.text === "string" ? payload.text : null;
73871
74012
  const summary = title ?? JSON.stringify(payload).slice(0, 200);
@@ -73956,7 +74097,7 @@ function verifyEdgeHeader(headerValue, expectedSecret) {
73956
74097
  }
73957
74098
 
73958
74099
  // src/web/webhook-handler.ts
73959
- var KNOWN_SOURCES = ["github", "generic"];
74100
+ var KNOWN_SOURCES = ["github", "generic", "linear"];
73960
74101
  function jsonReply(status, body, extraHeaders) {
73961
74102
  return {
73962
74103
  status,
@@ -74110,6 +74251,9 @@ async function handleWebhookIngest(args, deps = {}) {
74110
74251
  if (source === "github") {
74111
74252
  const sigHeader = args.headers.get("x-hub-signature-256");
74112
74253
  verifyResult = verifyGithubSignature(args.body, sigHeader, secret);
74254
+ } else if (source === "linear") {
74255
+ const sigHeader = args.headers.get("linear-signature");
74256
+ verifyResult = verifyLinearSignature(args.body, sigHeader, secret);
74113
74257
  } else {
74114
74258
  const authHeader = args.headers.get("authorization");
74115
74259
  verifyResult = verifyBearerToken(authHeader, secret);
@@ -74152,8 +74296,8 @@ async function handleWebhookIngest(args, deps = {}) {
74152
74296
  `);
74153
74297
  return jsonReply(400, { ok: false, error: "malformed json" });
74154
74298
  }
74155
- const eventType = source === "github" ? args.headers.get("x-github-event") ?? "unknown" : args.source;
74156
- const rendered = source === "github" ? renderGithubEvent(eventType, payload) : renderGenericEvent(args.source, payload);
74299
+ const eventType = source === "github" ? args.headers.get("x-github-event") ?? "unknown" : source === "linear" ? String(payload.type ?? "unknown").toLowerCase() : args.source;
74300
+ const rendered = source === "github" ? renderGithubEvent(eventType, payload) : source === "linear" ? renderLinearEvent(eventType, payload) : renderGenericEvent(args.source, payload);
74157
74301
  if (args.viaGateway) {
74158
74302
  const socketPath = join45(resolveAgentDir(args.agent), "telegram", "webhook.sock");
74159
74303
  const forward = deps.forwardFn ?? forwardToGateway;
@@ -81747,7 +81891,7 @@ function registerApplyCommand(program3) {
81747
81891
  }
81748
81892
  }
81749
81893
  const buildLocal = !!opts.buildLocal;
81750
- const buildContext = typeof opts.buildLocal === "string" ? opts.buildLocal : process.cwd();
81894
+ const buildContext2 = typeof opts.buildLocal === "string" ? opts.buildLocal : process.cwd();
81751
81895
  const releaseOverride = opts.channel ? { channel: opts.channel } : opts.pin ? { pin: opts.pin } : undefined;
81752
81896
  if (opts.pin && !/^(sha-[0-9a-f]{7,40}|v\d+\.\d+\.\d+)$/.test(opts.pin)) {
81753
81897
  console.error(source_default.red(`--pin "${opts.pin}" is invalid. Expected sha-<7-40 hex> or v<semver>.`));
@@ -81755,7 +81899,7 @@ function registerApplyCommand(program3) {
81755
81899
  }
81756
81900
  const result = await runApply(config, {
81757
81901
  buildLocal,
81758
- buildContext: buildLocal ? buildContext : undefined,
81902
+ buildContext: buildLocal ? buildContext2 : undefined,
81759
81903
  outPath: opts.out,
81760
81904
  example: opts.example,
81761
81905
  nonInteractive: opts.nonInteractive ?? false,
@@ -13817,6 +13817,25 @@ var SessionContinuitySchema = exports_external.object({
13817
13817
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
13818
13818
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
13819
13819
  }).optional();
13820
+ var webhookDispatchRule = exports_external.object({
13821
+ description: exports_external.string().optional(),
13822
+ match: exports_external.object({
13823
+ event: exports_external.string(),
13824
+ actions: exports_external.array(exports_external.string()).optional(),
13825
+ labels_any: exports_external.array(exports_external.string()).optional(),
13826
+ labels_all: exports_external.array(exports_external.string()).optional(),
13827
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
13828
+ assignee_any: exports_external.array(exports_external.string()).optional(),
13829
+ mentions_any: exports_external.array(exports_external.string()).optional()
13830
+ }).passthrough(),
13831
+ prompt: exports_external.string(),
13832
+ cooldown: exports_external.string().optional(),
13833
+ quiet_hours: exports_external.object({
13834
+ start: exports_external.number().int().min(0).max(23),
13835
+ end: exports_external.number().int().min(0).max(23),
13836
+ tz: exports_external.string().optional()
13837
+ }).optional()
13838
+ });
13820
13839
  var TelegramChannelSchema = exports_external.object({
13821
13840
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
13822
13841
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -13852,26 +13871,12 @@ var TelegramChannelSchema = exports_external.object({
13852
13871
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
13853
13872
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
13854
13873
  }).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
13855
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
13874
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
13856
13875
  webhook_dispatch: exports_external.object({
13857
- github: exports_external.array(exports_external.object({
13858
- description: exports_external.string().optional(),
13859
- match: exports_external.object({
13860
- event: exports_external.string(),
13861
- actions: exports_external.array(exports_external.string()).optional(),
13862
- labels_any: exports_external.array(exports_external.string()).optional(),
13863
- labels_all: exports_external.array(exports_external.string()).optional(),
13864
- exclude_authors: exports_external.array(exports_external.string()).optional()
13865
- }).passthrough(),
13866
- prompt: exports_external.string(),
13867
- cooldown: exports_external.string().optional(),
13868
- quiet_hours: exports_external.object({
13869
- start: exports_external.number().int().min(0).max(23),
13870
- end: exports_external.number().int().min(0).max(23),
13871
- tz: exports_external.string().optional()
13872
- }).optional()
13873
- })).optional()
13874
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
13876
+ github: exports_external.array(webhookDispatchRule).optional(),
13877
+ generic: exports_external.array(webhookDispatchRule).optional(),
13878
+ linear: exports_external.array(webhookDispatchRule).optional()
13879
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source — 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
13875
13880
  webhook_rate_limit: exports_external.object({
13876
13881
  rpm: exports_external.number().int().positive()
13877
13882
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -14048,7 +14053,7 @@ var AgentSchema = exports_external.object({
14048
14053
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
14049
14054
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
14050
14055
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
14051
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
14056
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
14052
14057
  voice_in: exports_external.object({
14053
14058
  enabled: exports_external.boolean().optional(),
14054
14059
  provider: exports_external.enum(["openai"]).optional(),
@@ -11277,7 +11277,7 @@ var init_dist = __esm(() => {
11277
11277
  });
11278
11278
 
11279
11279
  // src/config/schema.ts
11280
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11280
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11281
11281
  var init_schema = __esm(() => {
11282
11282
  init_zod();
11283
11283
  CodeRepoEntrySchema = exports_external.object({
@@ -11403,6 +11403,25 @@ var init_schema = __esm(() => {
11403
11403
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
11404
11404
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
11405
11405
  }).optional();
11406
+ webhookDispatchRule = exports_external.object({
11407
+ description: exports_external.string().optional(),
11408
+ match: exports_external.object({
11409
+ event: exports_external.string(),
11410
+ actions: exports_external.array(exports_external.string()).optional(),
11411
+ labels_any: exports_external.array(exports_external.string()).optional(),
11412
+ labels_all: exports_external.array(exports_external.string()).optional(),
11413
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
11414
+ assignee_any: exports_external.array(exports_external.string()).optional(),
11415
+ mentions_any: exports_external.array(exports_external.string()).optional()
11416
+ }).passthrough(),
11417
+ prompt: exports_external.string(),
11418
+ cooldown: exports_external.string().optional(),
11419
+ quiet_hours: exports_external.object({
11420
+ start: exports_external.number().int().min(0).max(23),
11421
+ end: exports_external.number().int().min(0).max(23),
11422
+ tz: exports_external.string().optional()
11423
+ }).optional()
11424
+ });
11406
11425
  TelegramChannelSchema = exports_external.object({
11407
11426
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
11408
11427
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -11438,26 +11457,12 @@ var init_schema = __esm(() => {
11438
11457
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
11439
11458
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
11440
11459
  }).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
11441
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11460
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11442
11461
  webhook_dispatch: exports_external.object({
11443
- github: exports_external.array(exports_external.object({
11444
- description: exports_external.string().optional(),
11445
- match: exports_external.object({
11446
- event: exports_external.string(),
11447
- actions: exports_external.array(exports_external.string()).optional(),
11448
- labels_any: exports_external.array(exports_external.string()).optional(),
11449
- labels_all: exports_external.array(exports_external.string()).optional(),
11450
- exclude_authors: exports_external.array(exports_external.string()).optional()
11451
- }).passthrough(),
11452
- prompt: exports_external.string(),
11453
- cooldown: exports_external.string().optional(),
11454
- quiet_hours: exports_external.object({
11455
- start: exports_external.number().int().min(0).max(23),
11456
- end: exports_external.number().int().min(0).max(23),
11457
- tz: exports_external.string().optional()
11458
- }).optional()
11459
- })).optional()
11460
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11462
+ github: exports_external.array(webhookDispatchRule).optional(),
11463
+ generic: exports_external.array(webhookDispatchRule).optional(),
11464
+ linear: exports_external.array(webhookDispatchRule).optional()
11465
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source — 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11461
11466
  webhook_rate_limit: exports_external.object({
11462
11467
  rpm: exports_external.number().int().positive()
11463
11468
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -11634,7 +11639,7 @@ var init_schema = __esm(() => {
11634
11639
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
11635
11640
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
11636
11641
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
11637
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11642
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11638
11643
  voice_in: exports_external.object({
11639
11644
  enabled: exports_external.boolean().optional(),
11640
11645
  provider: exports_external.enum(["openai"]).optional(),
@@ -11277,7 +11277,7 @@ var init_zod = __esm(() => {
11277
11277
  });
11278
11278
 
11279
11279
  // src/config/schema.ts
11280
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11280
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
11281
11281
  var init_schema = __esm(() => {
11282
11282
  init_zod();
11283
11283
  CodeRepoEntrySchema = exports_external.object({
@@ -11403,6 +11403,25 @@ var init_schema = __esm(() => {
11403
11403
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
11404
11404
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
11405
11405
  }).optional();
11406
+ webhookDispatchRule = exports_external.object({
11407
+ description: exports_external.string().optional(),
11408
+ match: exports_external.object({
11409
+ event: exports_external.string(),
11410
+ actions: exports_external.array(exports_external.string()).optional(),
11411
+ labels_any: exports_external.array(exports_external.string()).optional(),
11412
+ labels_all: exports_external.array(exports_external.string()).optional(),
11413
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
11414
+ assignee_any: exports_external.array(exports_external.string()).optional(),
11415
+ mentions_any: exports_external.array(exports_external.string()).optional()
11416
+ }).passthrough(),
11417
+ prompt: exports_external.string(),
11418
+ cooldown: exports_external.string().optional(),
11419
+ quiet_hours: exports_external.object({
11420
+ start: exports_external.number().int().min(0).max(23),
11421
+ end: exports_external.number().int().min(0).max(23),
11422
+ tz: exports_external.string().optional()
11423
+ }).optional()
11424
+ });
11406
11425
  TelegramChannelSchema = exports_external.object({
11407
11426
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
11408
11427
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' — the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -11438,26 +11457,12 @@ var init_schema = __esm(() => {
11438
11457
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
11439
11458
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short — the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
11440
11459
  }).optional().describe("Interrupt timing — how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
11441
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11460
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default — webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 — see #577.)"),
11442
11461
  webhook_dispatch: exports_external.object({
11443
- github: exports_external.array(exports_external.object({
11444
- description: exports_external.string().optional(),
11445
- match: exports_external.object({
11446
- event: exports_external.string(),
11447
- actions: exports_external.array(exports_external.string()).optional(),
11448
- labels_any: exports_external.array(exports_external.string()).optional(),
11449
- labels_all: exports_external.array(exports_external.string()).optional(),
11450
- exclude_authors: exports_external.array(exports_external.string()).optional()
11451
- }).passthrough(),
11452
- prompt: exports_external.string(),
11453
- cooldown: exports_external.string().optional(),
11454
- quiet_hours: exports_external.object({
11455
- start: exports_external.number().int().min(0).max(23),
11456
- end: exports_external.number().int().min(0).max(23),
11457
- tz: exports_external.string().optional()
11458
- }).optional()
11459
- })).optional()
11460
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11462
+ github: exports_external.array(webhookDispatchRule).optional(),
11463
+ generic: exports_external.array(webhookDispatchRule).optional(),
11464
+ linear: exports_external.array(webhookDispatchRule).optional()
11465
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source — 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default — opt in per agent. See src/web/webhook-dispatch.ts."),
11461
11466
  webhook_rate_limit: exports_external.object({
11462
11467
  rpm: exports_external.number().int().positive()
11463
11468
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default — when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -11634,7 +11639,7 @@ var init_schema = __esm(() => {
11634
11639
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (≤140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml — never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
11635
11640
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` — a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
11636
11641
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
11637
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11642
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED — moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
11638
11643
  voice_in: exports_external.object({
11639
11644
  enabled: exports_external.boolean().optional(),
11640
11645
  provider: exports_external.enum(["openai"]).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.4",
3
+ "version": "0.15.6",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23802,7 +23802,7 @@ var init_dist = __esm(() => {
23802
23802
  });
23803
23803
 
23804
23804
  // ../src/config/schema.ts
23805
- var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
23805
+ var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, TelegramReactionsPollSchema, PollSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, SwitchroomConfigSchema;
23806
23806
  var init_schema = __esm(() => {
23807
23807
  init_zod();
23808
23808
  CodeRepoEntrySchema = exports_external.object({
@@ -23928,6 +23928,25 @@ var init_schema = __esm(() => {
23928
23928
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
23929
23929
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
23930
23930
  }).optional();
23931
+ webhookDispatchRule = exports_external.object({
23932
+ description: exports_external.string().optional(),
23933
+ match: exports_external.object({
23934
+ event: exports_external.string(),
23935
+ actions: exports_external.array(exports_external.string()).optional(),
23936
+ labels_any: exports_external.array(exports_external.string()).optional(),
23937
+ labels_all: exports_external.array(exports_external.string()).optional(),
23938
+ exclude_authors: exports_external.array(exports_external.string()).optional(),
23939
+ assignee_any: exports_external.array(exports_external.string()).optional(),
23940
+ mentions_any: exports_external.array(exports_external.string()).optional()
23941
+ }).passthrough(),
23942
+ prompt: exports_external.string(),
23943
+ cooldown: exports_external.string().optional(),
23944
+ quiet_hours: exports_external.object({
23945
+ start: exports_external.number().int().min(0).max(23),
23946
+ end: exports_external.number().int().min(0).max(23),
23947
+ tz: exports_external.string().optional()
23948
+ }).optional()
23949
+ });
23931
23950
  TelegramChannelSchema = exports_external.object({
23932
23951
  enabled: exports_external.boolean().default(true).describe("Master switch for the per-agent Telegram gateway sidecar. " + "When false, start.sh skips the gateway supervise loop and the " + "agent boots without bot-token requirements (smoke-test + " + "offline-dev use case)."),
23933
23952
  plugin: exports_external.enum(["switchroom", "official"]).optional().describe("Which Telegram MCP plugin to load. Default is 'switchroom' \u2014 the " + "enhanced fork with streaming edits, reactions, history, and " + "access control. Set to 'official' for the upstream marketplace " + "plugin (basic send/receive only)."),
@@ -23963,26 +23982,12 @@ var init_schema = __esm(() => {
23963
23982
  safe_boundary: exports_external.boolean().optional().describe("When true (the default), a `!`-prefix interrupt that arrives while " + "the agent is mid-tool-call is DEFERRED: the SIGINT and the " + "replacement turn wait until the in-flight tool call finishes (a " + "clean boundary) instead of C-c'ing the agent mid-write/mid-bash. If " + "no tool is in flight the interrupt still fires immediately. Bounded " + "by max_wait_ms so a long tool never strands the user. Set false to " + "fire synchronously the moment `!` is received (historical " + "behaviour). Rapid repeated `!` while one is pending coalesce into a " + "single deferred interrupt carrying the latest body."),
23964
23983
  max_wait_ms: exports_external.number().int().positive().optional().describe("Upper bound (ms) the gateway waits for a safe boundary before firing " + "a deferred `!` interrupt anyway. Only consulted when safe_boundary is " + "true. Default 8000. Keep it short \u2014 the user explicitly asked to " + "interrupt, so a long in-flight tool shouldn't ghost them; the cap " + "trades a tiny risk of a mid-tool C-c for a guaranteed response.")
23965
23984
  }).optional().describe("Interrupt timing \u2014 how a `!`-prefix interrupt behaves when it lands " + "mid-tool-call. Off by default (fire immediately). Cascades from " + "defaults.channels.telegram.interrupt."),
23966
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
23985
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("External webhook sources allowed to ingest events into this agent's " + "log. POST /webhook/<agent>/<source> on the switchroom web server. " + "Each source has its own signature verification ('github' = " + "X-Hub-Signature-256 HMAC-SHA256, 'generic' = Bearer token, " + "'linear' = Linear-Signature bare-hex HMAC-SHA256 of the raw body). " + "Per-source secret read from ~/.switchroom/webhook-secrets.json " + "keyed by [agent][source]. Verified events append to " + "<agent>/telegram/webhook-events.jsonl for the agent to read on " + "demand. Off by default \u2014 webhook is the only untrusted-inbound " + "surface in the system, so opt-in is mandatory. " + "Cascades from defaults.channels.telegram.webhook_sources. " + "(Migrated from per-agent root in #596 \u2014 see #577.)"),
23967
23986
  webhook_dispatch: exports_external.object({
23968
- github: exports_external.array(exports_external.object({
23969
- description: exports_external.string().optional(),
23970
- match: exports_external.object({
23971
- event: exports_external.string(),
23972
- actions: exports_external.array(exports_external.string()).optional(),
23973
- labels_any: exports_external.array(exports_external.string()).optional(),
23974
- labels_all: exports_external.array(exports_external.string()).optional(),
23975
- exclude_authors: exports_external.array(exports_external.string()).optional()
23976
- }).passthrough(),
23977
- prompt: exports_external.string(),
23978
- cooldown: exports_external.string().optional(),
23979
- quiet_hours: exports_external.object({
23980
- start: exports_external.number().int().min(0).max(23),
23981
- end: exports_external.number().int().min(0).max(23),
23982
- tz: exports_external.string().optional()
23983
- }).optional()
23984
- })).optional()
23985
- }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Supports cooldowns, quiet hours, and label/action matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
23987
+ github: exports_external.array(webhookDispatchRule).optional(),
23988
+ generic: exports_external.array(webhookDispatchRule).optional(),
23989
+ linear: exports_external.array(webhookDispatchRule).optional()
23990
+ }).optional().describe("Auto-dispatch rules: when a verified webhook event matches a rule, " + "inject the rendered prompt into the agent's live session (#1625). " + "Rules are keyed by source \u2014 'github', 'generic', or 'linear' (#2272). " + "Supports cooldowns, quiet hours, label/action matchers, and (for " + "linear/generic) assignee_any / mentions_any matchers. " + "Off by default \u2014 opt in per agent. See src/web/webhook-dispatch.ts."),
23986
23991
  webhook_rate_limit: exports_external.object({
23987
23992
  rpm: exports_external.number().int().positive()
23988
23993
  }).optional().describe("Per-source rate limit for the webhook ingest path (#714). " + "Off by default \u2014 when this key is absent the handler skips " + "rate-limit checks entirely. Opt in by setting `rpm` to an " + "integer requests-per-minute (token bucket per (agent, source); " + "burst equal to rpm). When enabled, exceeding the limit returns " + "429 with Retry-After header; first throttle event per " + "(agent, source) per 60s window is written to " + "<agent>/telegram/issues.jsonl. " + "Cascades from defaults.channels.telegram.webhook_rate_limit."),
@@ -24159,7 +24164,7 @@ var init_schema = __esm(() => {
24159
24164
  purpose: exports_external.string().max(140).optional().describe("One-line description of what this agent does (\u2264140 chars). Shown to " + "peer agents when they call the agent-config MCP `peers_list` tool, so " + "every agent on the instance can answer 'is there an agent that does X' " + "without baking the fleet into prompts. Sourced live from " + "switchroom.yaml \u2014 never memorized into Hindsight. Falls back to " + "`topic_name` when absent."),
24160
24165
  role: exports_external.enum(["assistant", "foreman"]).optional().describe("Agent role. Default (omitted) is `assistant` \u2014 a fleet agent doing " + "user-facing tasks. `foreman` opts the agent in to switchroom's bundled " + "operator skills (switchroom-architecture / cli / health / install / manage " + "/ status), auto-symlinked into the agent's .claude/skills/ on scaffold and " + "reconcile. Fleet agents (assistant role) get no operator skills; reconcile " + "actively retracts them if the role flips back. See docs/skills.md for the model."),
24161
24166
  topic_id: exports_external.number().optional().describe("Telegram topic thread ID (auto-populated by switchroom topics sync)"),
24162
- webhook_sources: exports_external.array(exports_external.enum(["github", "generic"])).optional().describe("[DEPRECATED \u2014 moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
24167
+ webhook_sources: exports_external.array(exports_external.enum(["github", "generic", "linear"])).optional().describe("[DEPRECATED \u2014 moved to channels.telegram.webhook_sources in #596] " + "Old per-agent location. Still read but logs a deprecation warning. " + "See channels.telegram.webhook_sources for the canonical spot."),
24163
24168
  voice_in: exports_external.object({
24164
24169
  enabled: exports_external.boolean().optional(),
24165
24170
  provider: exports_external.enum(["openai"]).optional(),
@@ -46718,6 +46723,7 @@ function createInjectIpcClient(options) {
46718
46723
  }
46719
46724
 
46720
46725
  // ../src/web/webhook-dispatch.ts
46726
+ var DISPATCH_SOURCES = ["github", "generic", "linear"];
46721
46727
  function renderTemplate(template, ctx) {
46722
46728
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => ctx[key] ?? "");
46723
46729
  }
@@ -46734,12 +46740,86 @@ function buildGithubContext(eventType, payload) {
46734
46740
  const rawLabels = obj?.labels ?? [];
46735
46741
  const labels = rawLabels.map((l) => String(l.name ?? "")).join(", ");
46736
46742
  const action = String(payload.action ?? "");
46737
- return { repo, number, title, html_url, author, labels, action, event: eventType };
46743
+ const assignee = String(obj?.assignee?.login ?? "");
46744
+ return {
46745
+ repo,
46746
+ number,
46747
+ title,
46748
+ html_url,
46749
+ author,
46750
+ labels,
46751
+ action,
46752
+ event: eventType,
46753
+ assignee,
46754
+ body: title
46755
+ };
46756
+ }
46757
+ function buildLinearContext(eventType, payload) {
46758
+ const data = payload.data ?? {};
46759
+ const issue = data.issue ?? {};
46760
+ const number = String(data.identifier ?? issue.identifier ?? "");
46761
+ const title = String(data.title ?? issue.title ?? "");
46762
+ const html_url = String(payload.url ?? "");
46763
+ const action = String(payload.action ?? "");
46764
+ const assignee = String(data.assignee?.displayName ?? data.assignee?.name ?? "");
46765
+ const author = String(data.user?.displayName ?? data.user?.name ?? "");
46766
+ const rawLabels = data.labels ?? [];
46767
+ const labels = rawLabels.map((l) => String(l.name ?? "")).join(", ");
46768
+ const commentBody = String(data.body ?? "");
46769
+ const body = [title, commentBody].filter(Boolean).join(`
46770
+ `);
46771
+ return {
46772
+ repo: "linear",
46773
+ number,
46774
+ title,
46775
+ html_url,
46776
+ author,
46777
+ labels,
46778
+ action,
46779
+ event: eventType,
46780
+ assignee,
46781
+ body
46782
+ };
46738
46783
  }
46739
- function matchesRule(eventType, payload, matcher) {
46784
+ function buildGenericContext(source, payload) {
46785
+ const title = typeof payload.title === "string" ? payload.title : typeof payload.message === "string" ? payload.message : typeof payload.text === "string" ? payload.text : "";
46786
+ const action = String(payload.action ?? "");
46787
+ const html_url = typeof payload.url === "string" ? payload.url : "";
46788
+ const number = String(payload.id ?? payload.number ?? "");
46789
+ const author = String(payload.author ?? payload.user ?? "");
46790
+ const assignee = String(payload.assignee ?? "");
46791
+ const rawLabels = Array.isArray(payload.labels) ? payload.labels.map((l) => typeof l === "string" ? l : String(l?.name ?? "")) : [];
46792
+ const labels = rawLabels.join(", ");
46793
+ return {
46794
+ repo: source,
46795
+ number,
46796
+ title,
46797
+ html_url,
46798
+ author,
46799
+ labels,
46800
+ action,
46801
+ event: source,
46802
+ assignee,
46803
+ body: title
46804
+ };
46805
+ }
46806
+ function buildContext(source, eventType, payload) {
46807
+ switch (source) {
46808
+ case "linear":
46809
+ return buildLinearContext(eventType, payload);
46810
+ case "generic":
46811
+ return buildGenericContext(eventType, payload);
46812
+ default:
46813
+ return buildGithubContext(eventType, payload);
46814
+ }
46815
+ }
46816
+ function labelSetFromContext(ctx) {
46817
+ return new Set(ctx.labels.split(",").map((l) => l.trim()).filter(Boolean));
46818
+ }
46819
+ function matchesRule(source, eventType, payload, matcher) {
46740
46820
  if (matcher.event !== eventType)
46741
46821
  return false;
46742
- const ctx = buildGithubContext(eventType, payload);
46822
+ const ctx = buildContext(source, eventType, payload);
46743
46823
  if (matcher.actions && matcher.actions.length > 0) {
46744
46824
  if (!matcher.actions.includes(ctx.action))
46745
46825
  return false;
@@ -46748,22 +46828,24 @@ function matchesRule(eventType, payload, matcher) {
46748
46828
  if (matcher.exclude_authors.includes(ctx.author))
46749
46829
  return false;
46750
46830
  }
46831
+ if (matcher.assignee_any && matcher.assignee_any.length > 0) {
46832
+ if (!matcher.assignee_any.includes(ctx.assignee))
46833
+ return false;
46834
+ }
46835
+ if (matcher.mentions_any && matcher.mentions_any.length > 0) {
46836
+ const hay = ctx.body.toLowerCase();
46837
+ const hasMention = matcher.mentions_any.some((m) => hay.includes(m.toLowerCase()));
46838
+ if (!hasMention)
46839
+ return false;
46840
+ }
46751
46841
  if (matcher.labels_any && matcher.labels_any.length > 0) {
46752
- const pr = payload.pull_request;
46753
- const issue = payload.issue;
46754
- const rawLabels = (pr ?? issue)?.labels ?? [];
46755
- const labelNames = new Set(rawLabels.map((l) => String(l.name ?? "")));
46756
- const hasAny = matcher.labels_any.some((l) => labelNames.has(l));
46757
- if (!hasAny)
46842
+ const labelNames = labelSetFromContext(ctx);
46843
+ if (!matcher.labels_any.some((l) => labelNames.has(l)))
46758
46844
  return false;
46759
46845
  }
46760
46846
  if (matcher.labels_all && matcher.labels_all.length > 0) {
46761
- const pr = payload.pull_request;
46762
- const issue = payload.issue;
46763
- const rawLabels = (pr ?? issue)?.labels ?? [];
46764
- const labelNames = new Set(rawLabels.map((l) => String(l.name ?? "")));
46765
- const hasAll = matcher.labels_all.every((l) => labelNames.has(l));
46766
- if (!hasAll)
46847
+ const labelNames = labelSetFromContext(ctx);
46848
+ if (!matcher.labels_all.every((l) => labelNames.has(l)))
46767
46849
  return false;
46768
46850
  }
46769
46851
  return true;
@@ -46786,8 +46868,8 @@ function parseDurationMs(d) {
46786
46868
  return n;
46787
46869
  }
46788
46870
  }
46789
- function cooldownKey(eventType, repo, number, ruleIndex) {
46790
- return `${eventType}:${repo}:${number}:${ruleIndex}`;
46871
+ function cooldownKey(source, eventType, repo, number, ruleIndex) {
46872
+ return `${source}:${eventType}:${repo}:${number}:${ruleIndex}`;
46791
46873
  }
46792
46874
  function loadCooldownFile(path) {
46793
46875
  try {
@@ -46899,16 +46981,16 @@ function evaluateDispatch(args, deps = {}) {
46899
46981
  const nowDate = deps.nowDate ?? (() => new Date(now));
46900
46982
  const resolveAgentDir = deps.resolveAgentDir ?? ((a) => join19(homedir9(), ".switchroom", "agents", a));
46901
46983
  const cooldownStore = deps.cooldownStore ?? createFileCooldownStore(resolveAgentDir);
46902
- if (args.source !== "github")
46984
+ if (!DISPATCH_SOURCES.includes(args.source))
46903
46985
  return 0;
46904
- const rules = args.dispatchConfig.github;
46986
+ const rules = args.dispatchConfig[args.source];
46905
46987
  if (!rules || rules.length === 0)
46906
46988
  return 0;
46907
- const ctx = buildGithubContext(args.eventType, args.payload);
46989
+ const ctx = buildContext(args.source, args.eventType, args.payload);
46908
46990
  let fired = 0;
46909
46991
  for (let i = 0;i < rules.length; i++) {
46910
46992
  const rule = rules[i];
46911
- if (!matchesRule(args.eventType, args.payload, rule.match))
46993
+ if (!matchesRule(args.source, args.eventType, args.payload, rule.match))
46912
46994
  continue;
46913
46995
  if (rule.quiet_hours && isQuietHour(rule.quiet_hours, nowDate())) {
46914
46996
  log(`webhook-dispatch: agent='${args.agent}' rule=${i} skipped (quiet hours)
@@ -46917,7 +46999,7 @@ function evaluateDispatch(args, deps = {}) {
46917
46999
  }
46918
47000
  const cooldownMs = rule.cooldown ? parseDurationMs(rule.cooldown) : 0;
46919
47001
  if (cooldownMs > 0) {
46920
- const ck = cooldownKey(args.eventType, ctx.repo, ctx.number, i);
47002
+ const ck = cooldownKey(args.source, args.eventType, ctx.repo, ctx.number, i);
46921
47003
  if (cooldownStore.isCoolingDown(args.agent, ck, cooldownMs, now)) {
46922
47004
  log(`webhook-dispatch: agent='${args.agent}' rule=${i} skipped (cooldown)
46923
47005
  `);
@@ -47009,7 +47091,7 @@ function recordWebhookEvent(rec, deps = {}) {
47009
47091
  const config = (deps.loadConfig ?? loadConfig)();
47010
47092
  const rawAgent = config.agents?.[agent];
47011
47093
  const dispatchConfig = rawAgent ? resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.webhook_dispatch : undefined;
47012
- if (dispatchConfig && rec.source === "github") {
47094
+ if (dispatchConfig && DISPATCH_SOURCES.includes(rec.source)) {
47013
47095
  const target = resolveChannelTarget(config, agent);
47014
47096
  if (!target) {
47015
47097
  log(`webhook-gateway: agent='${agent}' dispatch skipped \u2014 no chat target (forum_chat_id / chat_id unset)
@@ -53601,10 +53683,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
53601
53683
  }
53602
53684
 
53603
53685
  // ../src/build-info.ts
53604
- var VERSION = "0.15.4";
53605
- var COMMIT_SHA = "dd68b93e";
53606
- var COMMIT_DATE = "2026-06-11T14:24:12Z";
53607
- var LATEST_PR = 2280;
53686
+ var VERSION = "0.15.6";
53687
+ var COMMIT_SHA = "3ae297b9";
53688
+ var COMMIT_DATE = "2026-06-12T03:43:06Z";
53689
+ var LATEST_PR = 2284;
53608
53690
  var COMMITS_AHEAD_OF_TAG = 0;
53609
53691
 
53610
53692
  // gateway/boot-version.ts