switchroom 0.15.11 → 0.15.12

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.
@@ -13882,6 +13882,11 @@ var TelegramChannelSchema = exports_external.object({
13882
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."),
13883
13883
  webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
13884
13884
  webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
13885
+ linear_agent: exports_external.object({
13886
+ enabled: exports_external.boolean(),
13887
+ token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
13888
+ workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
13889
+ }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
13885
13890
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
13886
13891
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
13887
13892
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
@@ -11480,6 +11480,11 @@ var init_schema = __esm(() => {
11480
11480
  }).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."),
11481
11481
  webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11482
11482
  webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
11483
+ linear_agent: exports_external.object({
11484
+ enabled: exports_external.boolean(),
11485
+ token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
11486
+ workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
11487
+ }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
11483
11488
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11484
11489
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11485
11490
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
@@ -11480,6 +11480,11 @@ var init_schema = __esm(() => {
11480
11480
  }).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."),
11481
11481
  webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
11482
11482
  webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge — the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
11483
+ linear_agent: exports_external.object({
11484
+ enabled: exports_external.boolean(),
11485
+ token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
11486
+ workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational — used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
11487
+ }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default — opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
11483
11488
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID — overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
11484
11489
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted — set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field — the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
11485
11490
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot → alerts, hostd → admin, etc.). " + "Cascades per-key through defaults → profile → agent.")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.11",
3
+ "version": "0.15.12",
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": {
@@ -455,6 +455,30 @@ const TOOL_SCHEMAS = [
455
455
  required: ['chat_id', 'key'],
456
456
  },
457
457
  },
458
+ {
459
+ name: 'linear_agent_activity',
460
+ description:
461
+ 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) — pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge → work → result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
462
+ inputSchema: {
463
+ type: 'object',
464
+ properties: {
465
+ agent_session_id: {
466
+ type: 'string',
467
+ description: 'The Linear AgentSession id — copy it verbatim from the woken turn\'s meta.agent_session_id.',
468
+ },
469
+ type: {
470
+ type: 'string',
471
+ enum: ['thought', 'message', 'complete', 'error'],
472
+ description: 'Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure.',
473
+ },
474
+ body: {
475
+ type: 'string',
476
+ description: 'Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary).',
477
+ },
478
+ },
479
+ required: ['agent_session_id', 'type'],
480
+ },
481
+ },
458
482
  ]
459
483
 
460
484
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }))
@@ -24934,6 +24934,29 @@ var TOOL_SCHEMAS = [
24934
24934
  },
24935
24935
  required: ["chat_id", "key"]
24936
24936
  }
24937
+ },
24938
+ {
24939
+ name: "linear_agent_activity",
24940
+ description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
24941
+ inputSchema: {
24942
+ type: "object",
24943
+ properties: {
24944
+ agent_session_id: {
24945
+ type: "string",
24946
+ description: "The Linear AgentSession id \u2014 copy it verbatim from the woken turn's meta.agent_session_id."
24947
+ },
24948
+ type: {
24949
+ type: "string",
24950
+ enum: ["thought", "message", "complete", "error"],
24951
+ description: "Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure."
24952
+ },
24953
+ body: {
24954
+ type: "string",
24955
+ description: "Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary)."
24956
+ }
24957
+ },
24958
+ required: ["agent_session_id", "type"]
24959
+ }
24937
24960
  }
24938
24961
  ];
24939
24962
  mcp.setRequestHandler(ListToolsRequestSchema2, async () => ({ tools: TOOL_SCHEMAS }));
@@ -23993,6 +23993,11 @@ var init_schema = __esm(() => {
23993
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."),
23994
23994
  webhook_via_gateway: exports_external.boolean().optional().describe("Route verified webhook events to the agent's in-container gateway " + "over a peercred-gated UDS (<agent>/telegram/webhook.sock) instead " + "of having the host-side web receiver write the agent dir directly. " + "Required under the Docker runtime: the receiver runs as the host " + "operator UID and cannot write the per-agent-UID-owned agent dir " + "(EACCES 500) nor connect the gateway socket. When true the gateway " + "(running as the agent UID) becomes the sole writer of " + "webhook-events.jsonl + dedup/cooldown state and also fires " + "webhook_dispatch. Off by default for back-compat with host-runtime " + "installs. See docs/rfcs/webhook-via-gateway-socket.md."),
23995
23995
  webhook_require_edge: exports_external.boolean().optional().describe("Cloudflare-only edge lock: require the X-Switchroom-Edge header " + "(injected by a Cloudflare Transform Rule on hooks.switchroom.ai) to " + "match the operator's edge secret at ~/.switchroom/webhook-edge-secret " + "before any HMAC verification; reject 403 otherwise. Proves the " + "request entered through our Cloudflare edge \u2014 the per-agent HMAC " + "alone can't (it proves body provenance, not network path). Stacks " + "on the GitHub-IP WAF + per-agent HMAC. Fail-closed: when required " + "but the secret file is missing/empty every request is rejected. Off " + "by default. See docs/rfcs/webhook-cloudflare-edge-lock.md."),
23996
+ linear_agent: exports_external.object({
23997
+ enabled: exports_external.boolean(),
23998
+ token: exports_external.string().describe("vault:<key> reference to the Linear OAuth app token (actor=app). " + "Resolved at runtime via the vault broker (canonically " + "vault:linear/<agent>/token). Never an inline literal."),
23999
+ workspace_id: exports_external.string().optional().describe("Optional Linear workspace (organization) id this agent is " + "installed into. Informational \u2014 used for setup hints and " + "multi-workspace disambiguation; the token already scopes the " + "app to its workspace.")
24000
+ }).optional().describe("Linear first-class agent integration (#2298). When enabled, the " + "agent appears in a Linear workspace as an app actor (own name/" + "avatar, @-mentionable, delegate-assignable). Linear AgentSessionEvent " + "webhooks (mention / delegation) wake the agent instantly via the " + "same gateway inject path as webhook_dispatch, tagged " + 'meta.source="linear" with the agent_session_id, and the agent ' + "responds with structured AgentActivity (thought/message/complete/" + "error) via the linear_agent_activity MCP tool. Builds the " + "session-lifecycle layer on top of the plain webhook_sources:[linear] " + "+ webhook_dispatch support (#2272). The OAuth app token is stored in " + "the vault and referenced here as vault:linear/<agent>/token; run " + "`switchroom linear-agent setup <agent>` to provision it. Off by " + "default \u2014 opt in per agent. Cascades from " + "defaults.channels.telegram.linear_agent."),
23996
24001
  chat_id: exports_external.string().regex(/^-\d+$/, 'supergroup chat_id must be a negative integer as a string (e.g. "-1001234567890")').optional().describe("Per-agent supergroup ID \u2014 overrides fleet `telegram.forum_chat_id`. " + "When set, requires `default_topic_id`. Negative integer as string. " + "Forbidden when `dm_only: true`. See docs/rfcs/supergroup-mode.md."),
23997
24002
  default_topic_id: exports_external.number().int().positive().optional().describe("Forum topic ID this agent's automated outbounds default to when " + "no more-specific alias resolves. Defaults to General (topic 1) when " + "`chat_id` is set and this is omitted \u2014 set it only to pin a different " + "fallback topic. " + "Telegram's General topic is `id=1` at MTProto but sends omit the " + "field \u2014 the outbound wrapper strips `message_thread_id === 1` " + "on send. Forbidden when `dm_only: true`."),
23998
24003
  topic_aliases: exports_external.record(exports_external.string(), exports_external.number().int().positive()).optional().describe("Operator-friendly names for forum topic IDs (e.g. " + "`{ general: 1, planning: 17, cron: 23, admin: 31, alerts: 41 }`). " + "Referenced from per-cron `topic:` fields and the outbound router " + "for autonomous events (boot \u2192 alerts, hostd \u2192 admin, etc.). " + "Cascades per-key through defaults \u2192 profile \u2192 agent.")
@@ -44920,7 +44925,7 @@ function labelTag(label) {
44920
44925
  }
44921
44926
 
44922
44927
  // gateway/model-command.ts
44923
- var MODEL_ALIASES = ["opus", "sonnet", "haiku", "default"];
44928
+ var MODEL_ALIASES = ["opus", "sonnet", "haiku", "fable", "default"];
44924
44929
  var MODEL_ARG_RE = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,99}$/;
44925
44930
  function isValidModelArg(arg) {
44926
44931
  return MODEL_ARG_RE.test(arg);
@@ -47026,6 +47031,53 @@ function injectWebhookInbound(agent, prompt, ctx, deps = {}) {
47026
47031
  `)).catch((err) => log(`webhook-dispatch: agent='${agent}' inject failed: ${String(err)}
47027
47032
  `));
47028
47033
  }
47034
+ function parseLinearAgentSession(payload) {
47035
+ const type = String(payload.type ?? "").toLowerCase();
47036
+ if (type !== "agentsessionevent")
47037
+ return null;
47038
+ const action = String(payload.action ?? "").toLowerCase();
47039
+ const session = payload.agentSession ?? payload.agent_session ?? {};
47040
+ const sessionId = String(session.id ?? payload.agentSessionId ?? payload.agent_session_id ?? "");
47041
+ if (!sessionId)
47042
+ return null;
47043
+ const explicitTrigger = String(session.trigger ?? payload.trigger ?? "").toLowerCase();
47044
+ const comment = session.comment;
47045
+ const trigger = explicitTrigger === "mention" || explicitTrigger === "delegation" ? explicitTrigger : comment ? "mention" : "delegation";
47046
+ const promptContext = typeof payload.promptContext === "string" ? payload.promptContext : typeof session.promptContext === "string" ? session.promptContext : undefined;
47047
+ const issue = session.issue ?? {};
47048
+ const issueId = String(issue.identifier ?? "");
47049
+ const issueTitle = String(issue.title ?? "");
47050
+ const commentBody = typeof comment?.body === "string" ? comment.body : "";
47051
+ const summaryParts = [];
47052
+ summaryParts.push(trigger === "mention" ? `You were @mentioned in Linear` : `An issue was delegated to you in Linear`);
47053
+ if (issueId || issueTitle) {
47054
+ summaryParts.push(`Issue ${issueId}${issueTitle ? `: ${issueTitle}` : ""}`.trim());
47055
+ }
47056
+ if (commentBody)
47057
+ summaryParts.push(commentBody);
47058
+ const summary = summaryParts.join(`
47059
+ `);
47060
+ return { sessionId, trigger, action, promptContext, summary };
47061
+ }
47062
+ function buildLinearInbound(session, ctx, now) {
47063
+ const content = session.promptContext && session.promptContext.trim().length > 0 ? session.promptContext : session.summary;
47064
+ return {
47065
+ type: "inbound",
47066
+ chatId: ctx.chatId,
47067
+ ...ctx.threadId !== undefined ? { threadId: ctx.threadId } : {},
47068
+ messageId: now,
47069
+ user: "linear",
47070
+ userId: 0,
47071
+ ts: now,
47072
+ text: content,
47073
+ meta: {
47074
+ source: "linear",
47075
+ agent_session_id: session.sessionId,
47076
+ linear_trigger: session.trigger,
47077
+ linear_event: `agent_session.${session.action || "event"}`
47078
+ }
47079
+ };
47080
+ }
47029
47081
  function evaluateDispatch(args, deps = {}) {
47030
47082
  const log = deps.log ?? ((s) => process.stderr.write(s));
47031
47083
  const now = (deps.now ?? Date.now)();
@@ -47137,6 +47189,38 @@ function recordWebhookEvent(rec, deps = {}) {
47137
47189
  }
47138
47190
  log(`webhook-gateway: agent='${agent}' source='${rec.source}' event='${rec.event_type}' recorded ts=${now}
47139
47191
  `);
47192
+ if (rec.source === "linear") {
47193
+ try {
47194
+ const session = parseLinearAgentSession(rec.payload);
47195
+ if (session) {
47196
+ const config = (deps.loadConfig ?? loadConfig)();
47197
+ const rawAgent = config.agents?.[agent];
47198
+ const linearAgent = rawAgent ? resolveAgentConfig(config.defaults, config.profiles, rawAgent).channels?.telegram?.linear_agent : undefined;
47199
+ if (!linearAgent?.enabled) {
47200
+ log(`linear-agent: agent='${agent}' session='${session.sessionId}' ignored \u2014 linear_agent not enabled
47201
+ `);
47202
+ } else {
47203
+ const target = resolveChannelTarget(config, agent);
47204
+ if (!target) {
47205
+ log(`linear-agent: agent='${agent}' session='${session.sessionId}' skipped \u2014 no chat target (forum_chat_id / chat_id unset)
47206
+ `);
47207
+ } else {
47208
+ const inbound = buildLinearInbound(session, {
47209
+ chatId: target.chatId,
47210
+ ...target.threadId !== undefined ? { threadId: target.threadId } : {}
47211
+ }, now);
47212
+ const ok = deps.inject ? deps.inject(agent, inbound) : false;
47213
+ log(`linear-agent: agent='${agent}' session='${session.sessionId}' trigger='${session.trigger}' ` + `${ok ? "injected" : "NOT injected (no inject sink)"}
47214
+ `);
47215
+ return { status: "ok", ts: now, dispatched: ok ? 1 : 0 };
47216
+ }
47217
+ }
47218
+ }
47219
+ } catch (err) {
47220
+ log(`linear-agent: agent='${agent}' session inject error (event recorded): ${err.message}
47221
+ `);
47222
+ }
47223
+ }
47140
47224
  let dispatched = 0;
47141
47225
  try {
47142
47226
  const config = (deps.loadConfig ?? loadConfig)();
@@ -53793,10 +53877,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
53793
53877
  }
53794
53878
 
53795
53879
  // ../src/build-info.ts
53796
- var VERSION = "0.15.11";
53797
- var COMMIT_SHA = "43331954";
53798
- var COMMIT_DATE = "2026-06-13T03:24:01Z";
53799
- var LATEST_PR = 2308;
53880
+ var VERSION = "0.15.12";
53881
+ var COMMIT_SHA = "18b7b6e6";
53882
+ var COMMIT_DATE = "2026-06-13T04:55:14Z";
53883
+ var LATEST_PR = 2311;
53800
53884
  var COMMITS_AHEAD_OF_TAG = 0;
53801
53885
 
53802
53886
  // gateway/boot-version.ts
@@ -54087,6 +54171,110 @@ async function revokeGrantViaBroker(id, opts) {
54087
54171
  return { kind: "error", msg: "unexpected broker response" };
54088
54172
  }
54089
54173
 
54174
+ // gateway/linear-activity.ts
54175
+ init_client2();
54176
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
54177
+ async function defaultResolveLinearToken(agent) {
54178
+ const key = `linear/${agent}/token`;
54179
+ const token = readVaultTokenFile(agent) ?? undefined;
54180
+ const result = await getViaBrokerStructured(key, token ? { token } : {});
54181
+ if (result.kind === "ok" && result.entry.kind === "string") {
54182
+ return { ok: true, token: result.entry.value };
54183
+ }
54184
+ if (result.kind === "unreachable")
54185
+ return { ok: false, reason: "unreachable" };
54186
+ if (result.kind === "not_found")
54187
+ return { ok: false, reason: "not_found" };
54188
+ if (result.kind === "denied")
54189
+ return { ok: false, reason: "denied" };
54190
+ return { ok: false, reason: "unknown" };
54191
+ }
54192
+ async function emitLinearAgentActivity(args, deps = {}) {
54193
+ const log = deps.log ?? ((s) => process.stderr.write(s));
54194
+ const sessionId = args.agent_session_id;
54195
+ if (!sessionId)
54196
+ throw new Error("linear_agent_activity: agent_session_id is required");
54197
+ const type = args.type;
54198
+ if (!type || !["thought", "message", "complete", "error"].includes(type)) {
54199
+ throw new Error("linear_agent_activity: type must be one of thought|message|complete|error");
54200
+ }
54201
+ const body = args.body;
54202
+ if (type !== "complete" && (body == null || body === "")) {
54203
+ throw new Error(`linear_agent_activity: body is required for type='${type}'`);
54204
+ }
54205
+ const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? "-";
54206
+ const resolveToken = deps.resolveToken ?? defaultResolveLinearToken;
54207
+ const tokenResult = await resolveToken(agent);
54208
+ if (!tokenResult.ok) {
54209
+ if (tokenResult.reason === "denied" || tokenResult.reason === "not_found") {
54210
+ return {
54211
+ content: [
54212
+ {
54213
+ type: "text",
54214
+ text: `linear_agent_activity failed: no Linear token (vault ${tokenResult.reason}). ` + `Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.`
54215
+ }
54216
+ ]
54217
+ };
54218
+ }
54219
+ return {
54220
+ content: [
54221
+ {
54222
+ type: "text",
54223
+ text: `linear_agent_activity failed: vault broker ${tokenResult.reason} resolving 'linear/${agent}/token'.`
54224
+ }
54225
+ ]
54226
+ };
54227
+ }
54228
+ const content = { type };
54229
+ if (body != null && body !== "")
54230
+ content.body = body;
54231
+ const mutation = "mutation AgentActivityCreate($input: AgentActivityCreateInput!) { " + "agentActivityCreate(input: $input) { success agentActivity { id } } }";
54232
+ const variables = { input: { agentSessionId: sessionId, content } };
54233
+ const fetchImpl = deps.fetchImpl ?? fetch;
54234
+ let resp;
54235
+ try {
54236
+ resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54237
+ method: "POST",
54238
+ headers: {
54239
+ "Content-Type": "application/json",
54240
+ Authorization: tokenResult.token
54241
+ },
54242
+ body: JSON.stringify({ query: mutation, variables })
54243
+ });
54244
+ } catch (err) {
54245
+ return {
54246
+ content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
54247
+ };
54248
+ }
54249
+ if (!resp.ok) {
54250
+ const txt = await resp.text().catch(() => "");
54251
+ return {
54252
+ content: [
54253
+ { type: "text", text: `linear_agent_activity failed: Linear API ${resp.status}${txt ? ` \u2014 ${txt.slice(0, 200)}` : ""}` }
54254
+ ]
54255
+ };
54256
+ }
54257
+ let json;
54258
+ try {
54259
+ json = await resp.json();
54260
+ } catch {
54261
+ return { content: [{ type: "text", text: "linear_agent_activity failed: malformed Linear API response" }] };
54262
+ }
54263
+ if (json.errors && json.errors.length > 0) {
54264
+ return {
54265
+ content: [
54266
+ { type: "text", text: `linear_agent_activity failed: ${json.errors.map((e) => e.message ?? "error").join("; ").slice(0, 300)}` }
54267
+ ]
54268
+ };
54269
+ }
54270
+ if (json.data?.agentActivityCreate?.success === false) {
54271
+ return { content: [{ type: "text", text: "linear_agent_activity failed: Linear reported success=false" }] };
54272
+ }
54273
+ log(`telegram gateway: linear_agent_activity: emitted type=${type} session=${sessionId} agent=${agent}
54274
+ `);
54275
+ return { content: [{ type: "text", text: `Linear ${type} emitted on session ${sessionId}` }] };
54276
+ }
54277
+
54090
54278
  // vault-approval-posture.ts
54091
54279
  function resolveVaultApprovalPosture(broker) {
54092
54280
  if (broker?.approvalAuth === "telegram-id") {
@@ -57294,7 +57482,8 @@ var ALLOWED_TOOLS = new Set([
57294
57482
  "send_gif",
57295
57483
  "vault_request_save",
57296
57484
  "vault_request_access",
57297
- "request_secret"
57485
+ "request_secret",
57486
+ "linear_agent_activity"
57298
57487
  ]);
57299
57488
  async function executeToolCall(tool, args) {
57300
57489
  if (!ALLOWED_TOOLS.has(tool)) {
@@ -57339,6 +57528,8 @@ async function executeToolCall(tool, args) {
57339
57528
  return executeVaultRequestAccess(args);
57340
57529
  case "request_secret":
57341
57530
  return executeRequestSecret(args);
57531
+ case "linear_agent_activity":
57532
+ return executeLinearAgentActivity(args);
57342
57533
  default:
57343
57534
  throw new Error(`unknown tool: ${tool}`);
57344
57535
  }
@@ -57369,6 +57560,9 @@ async function executeSendChecklist(args) {
57369
57560
  `);
57370
57561
  return { content: [{ type: "text", text: `checklist sent (id: ${sent.message_id})` }] };
57371
57562
  }
57563
+ async function executeLinearAgentActivity(args) {
57564
+ return emitLinearAgentActivity(args);
57565
+ }
57372
57566
  async function executeUpdateChecklist(args) {
57373
57567
  const chat_id = args.chat_id;
57374
57568
  if (!chat_id)
@@ -24631,6 +24631,29 @@ var init_bridge = __esm(async () => {
24631
24631
  },
24632
24632
  required: ["chat_id", "key"]
24633
24633
  }
24634
+ },
24635
+ {
24636
+ name: "linear_agent_activity",
24637
+ description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
24638
+ inputSchema: {
24639
+ type: "object",
24640
+ properties: {
24641
+ agent_session_id: {
24642
+ type: "string",
24643
+ description: "The Linear AgentSession id \u2014 copy it verbatim from the woken turn's meta.agent_session_id."
24644
+ },
24645
+ type: {
24646
+ type: "string",
24647
+ enum: ["thought", "message", "complete", "error"],
24648
+ description: "Activity kind. thought = visible reasoning ack (emit within ~10s); message = progress update; complete = terminal success; error = terminal failure."
24649
+ },
24650
+ body: {
24651
+ type: "string",
24652
+ description: "Activity text (Markdown). Required for thought/message/error; optional for complete (a closing summary)."
24653
+ }
24654
+ },
24655
+ required: ["agent_session_id", "type"]
24656
+ }
24634
24657
  }
24635
24658
  ];
24636
24659
  mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS }));
@@ -467,6 +467,7 @@ import {
467
467
  listGrantsViaBroker,
468
468
  revokeGrantViaBroker,
469
469
  } from '../../src/vault/broker/client.js'
470
+ import { emitLinearAgentActivity } from './linear-activity.js'
470
471
  import {
471
472
  approvalRequest,
472
473
  approvalConsume,
@@ -6718,6 +6719,7 @@ const ALLOWED_TOOLS = new Set([
6718
6719
  'vault_request_save',
6719
6720
  'vault_request_access',
6720
6721
  'request_secret',
6722
+ 'linear_agent_activity',
6721
6723
  ])
6722
6724
 
6723
6725
  async function executeToolCall(tool: string, args: Record<string, unknown>): Promise<unknown> {
@@ -6763,6 +6765,8 @@ async function executeToolCall(tool: string, args: Record<string, unknown>): Pro
6763
6765
  return executeVaultRequestAccess(args)
6764
6766
  case 'request_secret':
6765
6767
  return executeRequestSecret(args)
6768
+ case 'linear_agent_activity':
6769
+ return executeLinearAgentActivity(args)
6766
6770
  default:
6767
6771
  throw new Error(`unknown tool: ${tool}`)
6768
6772
  }
@@ -6794,6 +6798,10 @@ async function executeSendChecklist(args: Record<string, unknown>): Promise<{ co
6794
6798
  return { content: [{ type: 'text', text: `checklist sent (id: ${sent.message_id})` }] }
6795
6799
  }
6796
6800
 
6801
+ async function executeLinearAgentActivity(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
6802
+ return emitLinearAgentActivity(args)
6803
+ }
6804
+
6797
6805
  async function executeUpdateChecklist(args: Record<string, unknown>): Promise<{ content: Array<{ type: string; text: string }> }> {
6798
6806
  const chat_id = args.chat_id as string
6799
6807
  if (!chat_id) throw new Error('update_checklist: chat_id is required')
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Linear AgentActivity emission (#2298).
3
+ *
4
+ * The `linear_agent_activity` MCP tool lets an agent that was woken by a
5
+ * Linear agent session respond with structured activities (thought /
6
+ * message / complete / error) that Linear renders as status chips + a
7
+ * timeline on the issue. This module owns the pure logic — token
8
+ * resolution + the `agentActivityCreate` GraphQL POST — behind injectable
9
+ * deps so it is testable without a vault broker or the network. The
10
+ * gateway wires it into `executeToolCall`.
11
+ *
12
+ * The agent's Linear app token is resolved from the vault under
13
+ * `linear/<agent>/token` via the broker (never an inline literal, never an
14
+ * env file). On a vault denial the tool returns actionable text telling the
15
+ * agent to `vault_request_access` for that key rather than failing opaquely.
16
+ */
17
+
18
+ import {
19
+ getViaBrokerStructured,
20
+ readVaultTokenFile,
21
+ } from '../../src/vault/broker/client.js'
22
+
23
+ export const LINEAR_GRAPHQL_ENDPOINT = 'https://api.linear.app/graphql'
24
+
25
+ export type LinearTokenResult =
26
+ | { ok: true; token: string }
27
+ | { ok: false; reason: 'denied' | 'unreachable' | 'not_found' | 'unknown' }
28
+
29
+ export interface LinearActivityDeps {
30
+ /** Resolve the Linear app token for `agent` from the vault. */
31
+ resolveToken?: (agent: string) => Promise<LinearTokenResult>
32
+ /** Injectable fetch (tests). */
33
+ fetchImpl?: typeof fetch
34
+ /** Agent slug (defaults to SWITCHROOM_AGENT_NAME). */
35
+ agent?: string
36
+ /** Log sink — stderr in production. */
37
+ log?: (line: string) => void
38
+ }
39
+
40
+ export type ToolTextResult = { content: Array<{ type: string; text: string }> }
41
+
42
+ /** Default token resolver: vault broker get on `linear/<agent>/token`. */
43
+ export async function defaultResolveLinearToken(agent: string): Promise<LinearTokenResult> {
44
+ const key = `linear/${agent}/token`
45
+ const token = readVaultTokenFile(agent) ?? undefined
46
+ const result = await getViaBrokerStructured(key, token ? { token } : {})
47
+ if (result.kind === 'ok' && result.entry.kind === 'string') {
48
+ return { ok: true, token: result.entry.value }
49
+ }
50
+ if (result.kind === 'unreachable') return { ok: false, reason: 'unreachable' }
51
+ if (result.kind === 'not_found') return { ok: false, reason: 'not_found' }
52
+ if (result.kind === 'denied') return { ok: false, reason: 'denied' }
53
+ return { ok: false, reason: 'unknown' }
54
+ }
55
+
56
+ /**
57
+ * Emit a Linear AgentActivity. Validates args, resolves the token, POSTs
58
+ * the `agentActivityCreate` mutation, and returns an MCP text result. Never
59
+ * throws on a vault/network failure — returns actionable error text so the
60
+ * agent can recover (e.g. via vault_request_access).
61
+ */
62
+ export async function emitLinearAgentActivity(
63
+ args: Record<string, unknown>,
64
+ deps: LinearActivityDeps = {},
65
+ ): Promise<ToolTextResult> {
66
+ const log = deps.log ?? ((s) => process.stderr.write(s))
67
+
68
+ const sessionId = args.agent_session_id as string | undefined
69
+ if (!sessionId) throw new Error('linear_agent_activity: agent_session_id is required')
70
+ const type = args.type as string | undefined
71
+ if (!type || !['thought', 'message', 'complete', 'error'].includes(type)) {
72
+ throw new Error('linear_agent_activity: type must be one of thought|message|complete|error')
73
+ }
74
+ const body = args.body as string | undefined
75
+ if (type !== 'complete' && (body == null || body === '')) {
76
+ throw new Error(`linear_agent_activity: body is required for type='${type}'`)
77
+ }
78
+
79
+ const agent = deps.agent ?? process.env.SWITCHROOM_AGENT_NAME ?? '-'
80
+ const resolveToken = deps.resolveToken ?? defaultResolveLinearToken
81
+ const tokenResult = await resolveToken(agent)
82
+ if (!tokenResult.ok) {
83
+ if (tokenResult.reason === 'denied' || tokenResult.reason === 'not_found') {
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text:
89
+ `linear_agent_activity failed: no Linear token (vault ${tokenResult.reason}). ` +
90
+ `Call vault_request_access for key 'linear/${agent}/token' (scope read), then retry.`,
91
+ },
92
+ ],
93
+ }
94
+ }
95
+ return {
96
+ content: [
97
+ {
98
+ type: 'text',
99
+ text: `linear_agent_activity failed: vault broker ${tokenResult.reason} resolving 'linear/${agent}/token'.`,
100
+ },
101
+ ],
102
+ }
103
+ }
104
+
105
+ // AgentActivity content discriminated by type. thought/message/error carry
106
+ // a body; complete is terminal with an optional summary body.
107
+ const content: Record<string, unknown> = { type }
108
+ if (body != null && body !== '') content.body = body
109
+
110
+ const mutation =
111
+ 'mutation AgentActivityCreate($input: AgentActivityCreateInput!) { ' +
112
+ 'agentActivityCreate(input: $input) { success agentActivity { id } } }'
113
+ const variables = { input: { agentSessionId: sessionId, content } }
114
+
115
+ const fetchImpl = deps.fetchImpl ?? fetch
116
+ let resp: Response
117
+ try {
118
+ resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
119
+ method: 'POST',
120
+ headers: {
121
+ 'Content-Type': 'application/json',
122
+ Authorization: tokenResult.token,
123
+ },
124
+ body: JSON.stringify({ query: mutation, variables }),
125
+ })
126
+ } catch (err) {
127
+ return {
128
+ content: [{ type: 'text', text: `linear_agent_activity failed: request error: ${(err as Error).message}` }],
129
+ }
130
+ }
131
+
132
+ if (!resp.ok) {
133
+ const txt = await resp.text().catch(() => '')
134
+ return {
135
+ content: [
136
+ { type: 'text', text: `linear_agent_activity failed: Linear API ${resp.status}${txt ? ` — ${txt.slice(0, 200)}` : ''}` },
137
+ ],
138
+ }
139
+ }
140
+
141
+ let json: { data?: { agentActivityCreate?: { success?: boolean } }; errors?: Array<{ message?: string }> }
142
+ try {
143
+ json = (await resp.json()) as typeof json
144
+ } catch {
145
+ return { content: [{ type: 'text', text: 'linear_agent_activity failed: malformed Linear API response' }] }
146
+ }
147
+ if (json.errors && json.errors.length > 0) {
148
+ return {
149
+ content: [
150
+ { type: 'text', text: `linear_agent_activity failed: ${json.errors.map((e) => e.message ?? 'error').join('; ').slice(0, 300)}` },
151
+ ],
152
+ }
153
+ }
154
+ if (json.data?.agentActivityCreate?.success === false) {
155
+ return { content: [{ type: 'text', text: 'linear_agent_activity failed: Linear reported success=false' }] }
156
+ }
157
+
158
+ log(`telegram gateway: linear_agent_activity: emitted type=${type} session=${sessionId} agent=${agent}\n`)
159
+ return { content: [{ type: 'text', text: `Linear ${type} emitted on session ${sessionId}` }] }
160
+ }