switchroom 0.15.12 → 0.15.14

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.
Files changed (28) hide show
  1. package/dist/agent-scheduler/index.js +324 -14
  2. package/dist/auth-broker/index.js +61 -4
  3. package/dist/cli/notion-write-pretool.mjs +61 -4
  4. package/dist/cli/switchroom.js +402 -113
  5. package/dist/host-control/main.js +61 -4
  6. package/dist/vault/approvals/kernel-server.js +62 -5
  7. package/dist/vault/broker/server.js +62 -5
  8. package/package.json +1 -1
  9. package/profiles/_base/cron-session.sh.hbs +30 -13
  10. package/profiles/_shared/agent-self-service.md.hbs +37 -0
  11. package/telegram-plugin/bridge/bridge.ts +38 -1
  12. package/telegram-plugin/dist/bridge/bridge.js +31 -1
  13. package/telegram-plugin/dist/gateway/gateway.js +536 -53
  14. package/telegram-plugin/dist/server.js +31 -1
  15. package/telegram-plugin/gateway/gateway.ts +169 -6
  16. package/telegram-plugin/gateway/ipc-protocol.ts +31 -1
  17. package/telegram-plugin/gateway/ipc-server.ts +29 -0
  18. package/telegram-plugin/gateway/linear-activity.ts +145 -0
  19. package/telegram-plugin/runtime-metrics.ts +14 -0
  20. package/telegram-plugin/scoped-approval.ts +253 -0
  21. package/telegram-plugin/tests/bridge-liveness-override.test.ts +21 -0
  22. package/telegram-plugin/tests/ipc-server-validate-send-outbound.test.ts +54 -0
  23. package/telegram-plugin/tests/linear-agent-activity.test.ts +1 -1
  24. package/telegram-plugin/tests/linear-create-issue.test.ts +211 -0
  25. package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +13 -0
  26. package/telegram-plugin/tests/runtime-metrics.test.ts +9 -0
  27. package/telegram-plugin/tests/scoped-approval.test.ts +254 -0
  28. package/telegram-plugin/tests/send-outbound-wiring.test.ts +63 -0
@@ -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, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, 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, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, 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({
@@ -13553,11 +13553,29 @@ var init_schema = __esm(() => {
13553
13553
  HttpDiffPollSchema,
13554
13554
  TelegramReactionsPollSchema
13555
13555
  ]);
13556
+ TelegramMessageActionSchema = exports_external.object({
13557
+ type: exports_external.literal("telegram-message"),
13558
+ text: exports_external.string().min(1).describe("Operator-authored message text. Posts to the AGENT'S OWN chat only " + "(the chat is not configurable \u2014 fenced by construction), into the " + "entry-level `topic` when set. Supports ONLY deterministic placeholders " + "{{date}} / {{time}} (UTC, from the fire clock) and {{agent}}. NO vault " + "secrets (use a webhook action for secret-bearing requests) and NO model " + "output \u2014 the text is fully determined by config."),
13559
+ parse_mode: exports_external.enum(["html", "text"]).default("html").describe("Telegram parse mode for the message body.")
13560
+ });
13561
+ WebhookActionSchema = exports_external.object({
13562
+ type: exports_external.literal("webhook"),
13563
+ url: exports_external.string().url().describe("Webhook target. SAME egress fence as an http-diff poll (\u00a76.1): https " + "only, host on the operator egress allowlist, loopback/private/link-local " + "rejected, resolved IP DNS-rebind-pinned. Not agent-writable without an " + "operator commit."),
13564
+ method: exports_external.enum(["GET", "POST"]).default("POST"),
13565
+ headers: exports_external.record(exports_external.string()).optional().describe("Static headers; {{secret}} substitution applies."),
13566
+ body: exports_external.string().optional().describe("Static request body; {{secret}} substitution applies."),
13567
+ secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault keys this webhook may inject into headers/body via {{name}}. Each " + "is HOST-PINNED (\u00a76.1): a secret may only be sent to the host it is bound " + "to in operator config, so an approved action cannot exfil it elsewhere.")
13568
+ });
13569
+ ActionSpecSchema = exports_external.discriminatedUnion("type", [
13570
+ TelegramMessageActionSchema,
13571
+ WebhookActionSchema
13572
+ ]);
13556
13573
  ScheduleEntrySchema = exports_external.object({
13557
13574
  cron: exports_external.string().describe("Cron expression (e.g., '0 8 * * *')"),
13558
- prompt: exports_external.string().describe("Prompt to send at the scheduled time (the escalation prompt when kind=poll; templated with {{diff}})."),
13559
- kind: exports_external.enum(["poll", "prompt"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. Honoured only when SWITCHROOM_CHEAP_CRON is on."),
13575
+ prompt: exports_external.string().optional().describe("Prompt to send at the scheduled time (the escalation prompt when " + "kind=poll; templated with {{diff}}). Required for kind prompt/poll; " + "absent for kind=action (an action has no model fire, so no prompt)."),
13576
+ kind: exports_external.enum(["poll", "prompt", "action"]).optional().describe("Tier-0 routing (docs/rfcs/cheap-cron-sessions.md). 'prompt' (default) " + "fires a model turn every tick (Tier 1/2 per `context`). 'poll' runs a " + "model-free deterministic check (requires `poll`) and only escalates to " + "a model fire on a hit. 'action' runs a model-free deterministic verb " + "(requires `action`) that COMPLETES the work and never escalates \u2014 zero " + "tokens, no session. poll/prompt are honoured only when " + "SWITCHROOM_CHEAP_CRON is on; an action is model-free regardless (the " + "kill-switch governs model tiering, not deterministic actions)."),
13560
13577
  poll: PollSpecSchema.optional().describe("Required iff kind=poll. The declarative poll spec."),
13578
+ action: ActionSpecSchema.optional().describe("Required iff kind=action. The declarative action spec (telegram-message or webhook)."),
13561
13579
  model: exports_external.string().optional().describe("Cron model hint. Reactivated by SWITCHROOM_CHEAP_CRON (was DEPRECATED/" + "IGNORED in v0.8). A known-cheap id (sonnet/haiku family) routes the " + "fire to a fresh cheap cron session (Tier 1, `context: fresh`); 'opus', " + "a custom id, or unset routes to the agent's live session (Tier 2, " + "`context: agent`) \u2014 the conservative default that preserves pre-v0.8 " + "behaviour. Note: a live session's model is fixed at launch, so on Tier " + "2 this is informational. See docs/scheduling.md."),
13562
13580
  context: exports_external.enum(["fresh", "agent"]).optional().describe("Does this cron need the agent, or just a model? 'fresh' \u2192 a minimal-" + "context cheap cron session (Tier 1). 'agent' \u2192 the agent's live " + "session with full persona/memory (Tier 2). Unset \u2192 inferred from " + "`model` (cheap\u2192fresh, else agent). Honoured only when " + "SWITCHROOM_CHEAP_CRON is on."),
13563
13581
  secrets: exports_external.array(exports_external.string().regex(/^[a-zA-Z0-9_\-/]+$/, "Secret key names must contain only alphanumeric characters, underscores, hyphens, and forward slashes")).default([]).describe("Vault key names this cron task may read via the vault-broker daemon. " + "Empty by default \u2014 broker requests for unlisted keys are denied. " + "Note: this is misconfiguration protection (a typo in cron-A doesn't " + "accidentally read cron-B's keys) rather than a security boundary \u2014 " + "anyone who can edit cron scripts can also edit switchroom.yaml, and " + "anyone with the vault passphrase can read the vault file directly. " + "See docs/configuration.md for the full framing."),
@@ -13574,13 +13592,41 @@ var init_schema = __esm(() => {
13574
13592
  message: "kind: poll requires a `poll` spec (http-diff or telegram-reactions)."
13575
13593
  });
13576
13594
  }
13577
- if (kind === "prompt" && entry.poll) {
13595
+ if (kind !== "poll" && entry.poll) {
13578
13596
  ctx.addIssue({
13579
13597
  code: exports_external.ZodIssueCode.custom,
13580
13598
  path: ["poll"],
13581
13599
  message: "`poll` is only valid when kind: poll."
13582
13600
  });
13583
13601
  }
13602
+ if (kind === "action" && !entry.action) {
13603
+ ctx.addIssue({
13604
+ code: exports_external.ZodIssueCode.custom,
13605
+ path: ["action"],
13606
+ message: "kind: action requires an `action` spec (telegram-message or webhook)."
13607
+ });
13608
+ }
13609
+ if (kind !== "action" && entry.action) {
13610
+ ctx.addIssue({
13611
+ code: exports_external.ZodIssueCode.custom,
13612
+ path: ["action"],
13613
+ message: "`action` is only valid when kind: action."
13614
+ });
13615
+ }
13616
+ if (kind !== "action" && (entry.prompt === undefined || entry.prompt.trim() === "")) {
13617
+ ctx.addIssue({
13618
+ code: exports_external.ZodIssueCode.custom,
13619
+ path: ["prompt"],
13620
+ message: `kind: ${kind} requires a non-empty \`prompt\`.`
13621
+ });
13622
+ }
13623
+ if (kind === "action" && entry.prompt !== undefined) {
13624
+ ctx.addIssue({
13625
+ code: exports_external.ZodIssueCode.custom,
13626
+ path: ["prompt"],
13627
+ message: "`prompt` is not valid for kind: action (an action never fires a model)."
13628
+ });
13629
+ }
13584
13630
  });
13585
13631
  AgentSoulSchema = exports_external.object({
13586
13632
  name: exports_external.string().describe("Agent persona name (e.g., 'Coach', 'Sage')"),
@@ -13714,7 +13760,8 @@ var init_schema = __esm(() => {
13714
13760
  linear_agent: exports_external.object({
13715
13761
  enabled: exports_external.boolean(),
13716
13762
  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."),
13717
- 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.")
13763
+ 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."),
13764
+ default_team_id: exports_external.string().optional().describe("Optional Linear team id new captured issues file into when the " + "agent doesn't pass an explicit team_id. Unnecessary for a " + "single-team workspace (auto-resolved); set it only when the " + "workspace has multiple teams. Manage via " + "`switchroom linear-agent set-team <agent> <team>`.")
13718
13765
  }).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."),
13719
13766
  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."),
13720
13767
  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`."),
@@ -14640,6 +14687,16 @@ function mergeAgentConfig(defaultsIn, agentIn) {
14640
14687
  }
14641
14688
  merged.reaction_dispatch = combined;
14642
14689
  }
14690
+ const linearEnabled = merged.channels?.telegram?.linear_agent?.enabled === true;
14691
+ if (linearEnabled) {
14692
+ const rd = merged.reaction_dispatch;
14693
+ if (!rd || rd.emojis === undefined) {
14694
+ merged.reaction_dispatch = {
14695
+ enabled: rd?.enabled ?? true,
14696
+ emojis: ["\uD83D\uDC68\u200d\uD83D\uDCBB", "\uD83D\uDCCC"]
14697
+ };
14698
+ }
14699
+ }
14643
14700
  if (defaults.resources || merged.resources) {
14644
14701
  const d = defaults.resources ?? {};
14645
14702
  const a = merged.resources ?? {};
@@ -15396,6 +15453,9 @@ function resolveCronModel(model) {
15396
15453
  return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
15397
15454
  }
15398
15455
  function resolveCronRouting(input, opts) {
15456
+ if ((input.kind ?? "prompt") === "action") {
15457
+ return { tier: "action", session: null, customModelDowngrade: false };
15458
+ }
15399
15459
  if (!opts.cheapCronEnabled) {
15400
15460
  return { tier: "main", session: "main", customModelDowngrade: false };
15401
15461
  }
@@ -15440,9 +15500,96 @@ var init_cron_routing = __esm(() => {
15440
15500
  OPUS_MODEL_RE = /opus/i;
15441
15501
  });
15442
15502
 
15503
+ // src/scheduler/cron-cadence.ts
15504
+ function csvSmallestGap(field) {
15505
+ if (!field.includes(","))
15506
+ return null;
15507
+ const parts = field.split(",").map((s) => Number(s)).filter((n) => Number.isInteger(n) && n >= 0);
15508
+ if (parts.length < 2)
15509
+ return null;
15510
+ const sorted = [...parts].sort((a, b) => a - b);
15511
+ let smallest = Infinity;
15512
+ for (let i = 1;i < sorted.length; i++) {
15513
+ const gap = sorted[i] - sorted[i - 1];
15514
+ if (gap > 0 && gap < smallest)
15515
+ smallest = gap;
15516
+ }
15517
+ return Number.isFinite(smallest) ? smallest : null;
15518
+ }
15519
+ function estimateCronGapMin(expr) {
15520
+ const fields = expr.trim().split(/\s+/);
15521
+ if (fields.length < 5)
15522
+ return Infinity;
15523
+ const [min, hour] = fields;
15524
+ if (min === "*")
15525
+ return 1;
15526
+ const minStep = min.match(/^\*\/(\d+)$/);
15527
+ if (minStep) {
15528
+ const n = Number(minStep[1]);
15529
+ return n > 0 ? n : Infinity;
15530
+ }
15531
+ const minCsv = csvSmallestGap(min);
15532
+ if (minCsv !== null)
15533
+ return minCsv;
15534
+ if (!/^\d+$/.test(min))
15535
+ return Infinity;
15536
+ if (hour === "*")
15537
+ return 60;
15538
+ const hourStep = hour.match(/^\*\/(\d+)$/);
15539
+ if (hourStep) {
15540
+ const n = Number(hourStep[1]);
15541
+ return n > 0 ? n * 60 : Infinity;
15542
+ }
15543
+ const hourCsv = csvSmallestGap(hour);
15544
+ if (hourCsv !== null)
15545
+ return hourCsv * 60;
15546
+ if (/^\d+$/.test(hour))
15547
+ return 1440;
15548
+ return Infinity;
15549
+ }
15550
+
15443
15551
  // src/scheduler/tier-selector.ts
15444
- function applyDefaultTier(entry, _frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
15445
- return entry;
15552
+ function resolveCronAutoTier(env2 = process.env) {
15553
+ const v = (env2.SWITCHROOM_CRON_AUTO_TIER ?? "").toLowerCase();
15554
+ return v === "1" || v === "true" || v === "on";
15555
+ }
15556
+ function recommendCronTier(input, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
15557
+ if (input.kind === "action") {
15558
+ return { tier: "action", source: "explicit", reason: "declared kind: action (model-free verb)" };
15559
+ }
15560
+ if (input.kind === "poll") {
15561
+ return { tier: "poll", source: "explicit", reason: "declared kind: poll (model-free check)" };
15562
+ }
15563
+ if (input.context === "fresh") {
15564
+ return { tier: "cheap", source: "explicit", reason: "declared context: fresh (cheap cron session)" };
15565
+ }
15566
+ if (input.context === "agent") {
15567
+ return { tier: "main", source: "explicit", reason: "declared context: agent (full live session)" };
15568
+ }
15569
+ if (input.model !== undefined) {
15570
+ return isKnownCheapModel(input.model) ? { tier: "cheap", source: "explicit", reason: `cheap model '${input.model}' \u2192 cheap cron session` } : { tier: "main", source: "explicit", reason: `model '${input.model}' is not a known-cheap id \u2192 full live session` };
15571
+ }
15572
+ if (input.smallestGapMin <= frequentGapMin) {
15573
+ return {
15574
+ tier: "cheap",
15575
+ source: "cadence-default",
15576
+ reason: `fires every ~${input.smallestGapMin}min (\u2264 ${frequentGapMin}min) \u2014 defaulting to a cheap ` + `session; set context: agent (or an Opus/custom model) if this needs the agent's full context`
15577
+ };
15578
+ }
15579
+ return {
15580
+ tier: "main",
15581
+ source: "cadence-default",
15582
+ reason: `fires every ~${input.smallestGapMin}min (> ${frequentGapMin}min) \u2014 defaulting to the agent's ` + `full session; set model: sonnet (or context: fresh) to run it cheaply`
15583
+ };
15584
+ }
15585
+ function applyDefaultTier(entry, frequentGapMin = DEFAULT_FREQUENT_GAP_MIN, autoTierEnabled = resolveCronAutoTier()) {
15586
+ if (!autoTierEnabled)
15587
+ return entry;
15588
+ if (entry.kind !== undefined || entry.context !== undefined || entry.model !== undefined) {
15589
+ return entry;
15590
+ }
15591
+ const rec = recommendCronTier({ smallestGapMin: estimateCronGapMin(entry.cron) }, frequentGapMin);
15592
+ return rec.tier === "cheap" ? { ...entry, context: "fresh" } : entry;
15446
15593
  }
15447
15594
  var DEFAULT_FREQUENT_GAP_MIN = 60;
15448
15595
  var init_tier_selector = __esm(() => {
@@ -30758,8 +30905,74 @@ var init_doctor_webkite = __esm(() => {
30758
30905
  init_loader();
30759
30906
  });
30760
30907
 
30908
+ // src/cli/doctor-cron-session.ts
30909
+ import { statSync as realStatSync } from "node:fs";
30910
+ import { resolve as resolve31 } from "node:path";
30911
+ function agentRunsCronSession(config, agent) {
30912
+ const raw = config.agents[agent];
30913
+ if (!raw)
30914
+ return false;
30915
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, raw);
30916
+ const entries = (resolved.schedule ?? []).map((e) => applyDefaultTier({ cron: e.cron, kind: e.kind, model: e.model, context: e.context }));
30917
+ return scheduleNeedsCronSession(entries, { cheapCronEnabled: true });
30918
+ }
30919
+ function runCronSessionChecks(config, deps = defaultDeps2) {
30920
+ const agentsDir = resolveAgentsDir(config);
30921
+ const results = [];
30922
+ for (const agent of Object.keys(config.agents).sort()) {
30923
+ if (!agentRunsCronSession(config, agent))
30924
+ continue;
30925
+ const name = `cron-session: ${agent}`;
30926
+ const alivePath = resolve31(agentsDir, agent, "telegram", ".bridge-alive-cron");
30927
+ let mtimeMs;
30928
+ try {
30929
+ mtimeMs = deps.statMtimeMs(alivePath);
30930
+ } catch (err) {
30931
+ const code = err.code;
30932
+ if (code === "EACCES" || code === "EPERM") {
30933
+ results.push({
30934
+ name,
30935
+ status: "skip",
30936
+ detail: "cron-bridge liveness file present but unreadable by the operator \u2014 re-run doctor without sudo"
30937
+ });
30938
+ continue;
30939
+ }
30940
+ results.push({
30941
+ name,
30942
+ status: "fail",
30943
+ detail: `<${agent}-cron> bridge not registered \u2014 no liveness file at ${alivePath}. Tier-1 cron fires are falling back to the (expensive) main session.`,
30944
+ fix: `Restart the agent (\`switchroom agent restart ${agent} --wait --force\`) and check /var/log/switchroom/cron-session.log for a wizard wedge.`
30945
+ });
30946
+ continue;
30947
+ }
30948
+ const ageMs = deps.now() - mtimeMs;
30949
+ if (ageMs <= MAX_AGE_MS) {
30950
+ results.push({ name, status: "ok", detail: `<${agent}-cron> bridge live (heartbeat ${Math.round(ageMs / 1000)}s ago)` });
30951
+ } else {
30952
+ results.push({
30953
+ name,
30954
+ status: "fail",
30955
+ detail: `<${agent}-cron> bridge heartbeat is stale (${Math.round(ageMs / 1000)}s > ${MAX_AGE_MS / 1000}s) \u2014 the cron session registered then died. Tier-1 fires are falling back to main.`,
30956
+ fix: `Restart the agent (\`switchroom agent restart ${agent} --wait --force\`) and check /var/log/switchroom/cron-session.log.`
30957
+ });
30958
+ }
30959
+ }
30960
+ return results;
30961
+ }
30962
+ var defaultDeps2, MAX_AGE_MS = 30000;
30963
+ var init_doctor_cron_session = __esm(() => {
30964
+ init_loader();
30965
+ init_merge();
30966
+ init_cron_routing();
30967
+ init_tier_selector();
30968
+ defaultDeps2 = {
30969
+ statMtimeMs: (p) => realStatSync(p).mtimeMs,
30970
+ now: () => Date.now()
30971
+ };
30972
+ });
30973
+
30761
30974
  // src/cli/doctor-scaffold-wiring.ts
30762
- import { join as join52, resolve as resolve31 } from "node:path";
30975
+ import { join as join52, resolve as resolve32 } from "node:path";
30763
30976
  function readJson2(d, path4) {
30764
30977
  if (!d.existsSync(path4))
30765
30978
  return { kind: "absent" };
@@ -30793,7 +31006,7 @@ function checkIntegrationScaffoldWiring(args) {
30793
31006
  ];
30794
31007
  }
30795
31008
  for (const name of agents) {
30796
- const agentDir = resolve31(agentsDir, name);
31009
+ const agentDir = resolve32(agentsDir, name);
30797
31010
  if (!deps.existsSync(agentDir)) {
30798
31011
  results.push({
30799
31012
  name: `${label}: ${name} scaffold`,
@@ -30833,7 +31046,7 @@ function checkIntegrationScaffoldWiring(args) {
30833
31046
  let trustDetail = "no .claude/.claude.json";
30834
31047
  if (trustRead.kind === "ok") {
30835
31048
  const projects = trustRead.data?.projects ?? {};
30836
- const proj = projects[resolve31(agentDir)];
31049
+ const proj = projects[resolve32(agentDir)];
30837
31050
  const enabled = proj?.enabledMcpjsonServers;
30838
31051
  if (Array.isArray(enabled) && enabled.includes(mcpKey)) {
30839
31052
  trustOk = true;
@@ -31052,7 +31265,7 @@ var init_doctor_microsoft = __esm(() => {
31052
31265
  import {
31053
31266
  existsSync as realExistsSync4,
31054
31267
  readFileSync as realReadFileSync4,
31055
- statSync as realStatSync
31268
+ statSync as realStatSync2
31056
31269
  } from "node:fs";
31057
31270
  import { join as join54 } from "node:path";
31058
31271
  import { homedir as homedir31 } from "node:os";
@@ -31061,7 +31274,7 @@ function resolveDeps4(deps) {
31061
31274
  return {
31062
31275
  existsSync: deps.existsSync ?? realExistsSync4,
31063
31276
  readFileSync: deps.readFileSync ?? realReadFileSync4,
31064
- statSync: deps.statSync ?? realStatSync,
31277
+ statSync: deps.statSync ?? realStatSync2,
31065
31278
  agentsDir: join54(home2, ".switchroom", "agents"),
31066
31279
  now: deps.now ?? Date.now,
31067
31280
  vaultAclReader: deps.vaultAclReader ?? (async () => ({ kind: "unreachable", msg: "no default reader wired" }))
@@ -31260,7 +31473,7 @@ var init_doctor_notion = __esm(() => {
31260
31473
  import {
31261
31474
  existsSync as realExistsSync5,
31262
31475
  readdirSync as realReaddirSync2,
31263
- statSync as realStatSync2
31476
+ statSync as realStatSync3
31264
31477
  } from "node:fs";
31265
31478
  import { homedir as homedir32 } from "node:os";
31266
31479
  import { join as join55 } from "node:path";
@@ -31270,7 +31483,7 @@ function runCredentialsMigrationChecks(config, deps = {}) {
31270
31483
  const readdirSync21 = deps.readdirSync ?? ((p) => realReaddirSync2(p));
31271
31484
  const isDirectory = deps.isDirectory ?? ((p) => {
31272
31485
  try {
31273
- return realStatSync2(p).isDirectory();
31486
+ return realStatSync3(p).isDirectory();
31274
31487
  } catch {
31275
31488
  return false;
31276
31489
  }
@@ -31912,7 +32125,7 @@ import {
31912
32125
  readdirSync as readdirSync21,
31913
32126
  statSync as statSync25
31914
32127
  } from "node:fs";
31915
- import { dirname as dirname13, join as join59, resolve as resolve32 } from "node:path";
32128
+ import { dirname as dirname13, join as join59, resolve as resolve33 } from "node:path";
31916
32129
  import { createPublicKey, createPrivateKey } from "node:crypto";
31917
32130
  function findInNvm(bin) {
31918
32131
  const nvmRoot = join59(process.env.HOME ?? "", ".nvm", "versions", "node");
@@ -32926,7 +33139,7 @@ function checkAgents(config, configPath) {
32926
33139
  const statuses = getAllAgentStatuses(config);
32927
33140
  const authStatuses = getAllAuthStatuses(config);
32928
33141
  for (const [name, agentConfig] of Object.entries(config.agents)) {
32929
- const agentDir = resolve32(agentsDir, name);
33142
+ const agentDir = resolve33(agentsDir, name);
32930
33143
  if (!existsSync57(agentDir)) {
32931
33144
  results.push({
32932
33145
  name: `${name}: scaffold`,
@@ -33109,7 +33322,7 @@ function mffAgentName(config) {
33109
33322
  function mffEnvPath(config) {
33110
33323
  const home2 = process.env.HOME ?? "/root";
33111
33324
  const agent = mffAgentName(config);
33112
- return agent ? resolve32(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve32(home2, ".switchroom/credentials/my-family-finance/.env");
33325
+ return agent ? resolve33(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve33(home2, ".switchroom/credentials/my-family-finance/.env");
33113
33326
  }
33114
33327
  function mffEnvState(envPath) {
33115
33328
  if (!existsSync57(envPath))
@@ -33525,14 +33738,14 @@ async function checkManifestDrift(probers) {
33525
33738
  return results;
33526
33739
  }
33527
33740
  function runDockerSection(config) {
33528
- const composePath = resolve32(process.env.HOME ?? "", ".switchroom", "compose", "docker-compose.yml");
33741
+ const composePath = resolve33(process.env.HOME ?? "", ".switchroom", "compose", "docker-compose.yml");
33529
33742
  const active = isDockerMode({ composePath });
33530
33743
  let composeYaml;
33531
33744
  let dockerfileAgent;
33532
33745
  try {
33533
33746
  composeYaml = readFileSync51(composePath, "utf8");
33534
33747
  } catch {}
33535
- const dockerfilePath = resolve32(process.env.HOME ?? "", ".switchroom", "docker", "Dockerfile.agent");
33748
+ const dockerfilePath = resolve33(process.env.HOME ?? "", ".switchroom", "docker", "Dockerfile.agent");
33536
33749
  try {
33537
33750
  dockerfileAgent = readFileSync51(dockerfilePath, "utf8");
33538
33751
  } catch {}
@@ -33657,7 +33870,8 @@ function registerDoctorCommand(program3) {
33657
33870
  })
33658
33871
  },
33659
33872
  { title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) },
33660
- { title: "Webkite", results: runWebkiteChecks(config) }
33873
+ { title: "Webkite", results: runWebkiteChecks(config) },
33874
+ { title: "Cron Session", results: runCronSessionChecks(config) }
33661
33875
  ];
33662
33876
  const cwd = process.cwd();
33663
33877
  if (isSwitchroomCheckout(cwd)) {
@@ -33719,6 +33933,7 @@ var init_doctor = __esm(() => {
33719
33933
  init_doctor_hostd();
33720
33934
  init_doctor_drive();
33721
33935
  init_doctor_webkite();
33936
+ init_doctor_cron_session();
33722
33937
  init_doctor_microsoft();
33723
33938
  init_doctor_notion();
33724
33939
  init_doctor_credentials_migration();
@@ -41903,7 +42118,7 @@ class Protocol {
41903
42118
  return;
41904
42119
  }
41905
42120
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
41906
- await new Promise((resolve49) => setTimeout(resolve49, pollInterval));
42121
+ await new Promise((resolve50) => setTimeout(resolve50, pollInterval));
41907
42122
  options?.signal?.throwIfAborted();
41908
42123
  }
41909
42124
  } catch (error2) {
@@ -41915,7 +42130,7 @@ class Protocol {
41915
42130
  }
41916
42131
  request(request, resultSchema, options) {
41917
42132
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
41918
- return new Promise((resolve49, reject) => {
42133
+ return new Promise((resolve50, reject) => {
41919
42134
  const earlyReject = (error2) => {
41920
42135
  reject(error2);
41921
42136
  };
@@ -41993,7 +42208,7 @@ class Protocol {
41993
42208
  if (!parseResult.success) {
41994
42209
  reject(parseResult.error);
41995
42210
  } else {
41996
- resolve49(parseResult.data);
42211
+ resolve50(parseResult.data);
41997
42212
  }
41998
42213
  } catch (error2) {
41999
42214
  reject(error2);
@@ -42184,12 +42399,12 @@ class Protocol {
42184
42399
  interval = task.pollInterval;
42185
42400
  }
42186
42401
  } catch {}
42187
- return new Promise((resolve49, reject) => {
42402
+ return new Promise((resolve50, reject) => {
42188
42403
  if (signal.aborted) {
42189
42404
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
42190
42405
  return;
42191
42406
  }
42192
- const timeoutId = setTimeout(resolve49, interval);
42407
+ const timeoutId = setTimeout(resolve50, interval);
42193
42408
  signal.addEventListener("abort", () => {
42194
42409
  clearTimeout(timeoutId);
42195
42410
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
@@ -45174,7 +45389,7 @@ var require_compile = __commonJS((exports2) => {
45174
45389
  const schOrFunc = root.refs[ref];
45175
45390
  if (schOrFunc)
45176
45391
  return schOrFunc;
45177
- let _sch = resolve49.call(this, root, ref);
45392
+ let _sch = resolve50.call(this, root, ref);
45178
45393
  if (_sch === undefined) {
45179
45394
  const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref];
45180
45395
  const { schemaId } = this.opts;
@@ -45201,7 +45416,7 @@ var require_compile = __commonJS((exports2) => {
45201
45416
  function sameSchemaEnv(s1, s2) {
45202
45417
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
45203
45418
  }
45204
- function resolve49(root, ref) {
45419
+ function resolve50(root, ref) {
45205
45420
  let sch;
45206
45421
  while (typeof (sch = this.refs[ref]) == "string")
45207
45422
  ref = sch;
@@ -45731,7 +45946,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
45731
45946
  }
45732
45947
  return uri;
45733
45948
  }
45734
- function resolve49(baseURI, relativeURI, options) {
45949
+ function resolve50(baseURI, relativeURI, options) {
45735
45950
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
45736
45951
  const resolved = resolveComponent(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
45737
45952
  schemelessOptions.skipEscape = true;
@@ -45959,7 +46174,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
45959
46174
  var fastUri = {
45960
46175
  SCHEMES,
45961
46176
  normalize,
45962
- resolve: resolve49,
46177
+ resolve: resolve50,
45963
46178
  resolveComponent,
45964
46179
  equal,
45965
46180
  serialize,
@@ -49342,12 +49557,12 @@ class StdioServerTransport {
49342
49557
  this.onclose?.();
49343
49558
  }
49344
49559
  send(message) {
49345
- return new Promise((resolve49) => {
49560
+ return new Promise((resolve50) => {
49346
49561
  const json = serializeMessage(message);
49347
49562
  if (this._stdout.write(json)) {
49348
- resolve49();
49563
+ resolve50();
49349
49564
  } else {
49350
- this._stdout.once("drain", resolve49);
49565
+ this._stdout.once("drain", resolve50);
49351
49566
  }
49352
49567
  });
49353
49568
  }
@@ -50252,8 +50467,8 @@ var {
50252
50467
  } = import__.default;
50253
50468
 
50254
50469
  // src/build-info.ts
50255
- var VERSION = "0.15.12";
50256
- var COMMIT_SHA = "18b7b6e6";
50470
+ var VERSION = "0.15.14";
50471
+ var COMMIT_SHA = "91ae15d3";
50257
50472
 
50258
50473
  // src/cli/agent.ts
50259
50474
  init_source();
@@ -50353,7 +50568,7 @@ import { createHash as createHash2 } from "node:crypto";
50353
50568
  // src/agents/cron-unit-name.ts
50354
50569
  import { createHash } from "node:crypto";
50355
50570
  function cronUnitHash(cron, prompt) {
50356
- return createHash("sha256").update(cron).update("\x00").update(prompt).digest("hex").slice(0, 12);
50571
+ return createHash("sha256").update(cron).update("\x00").update(prompt ?? "").digest("hex").slice(0, 12);
50357
50572
  }
50358
50573
  function cronUnitName(cron, prompt) {
50359
50574
  return `cron-${cronUnitHash(cron, prompt)}`;
@@ -50902,8 +51117,8 @@ function findExistingClaudeJson() {
50902
51117
  console.warn(" Alternatively, agents can be onboarded individually via `switchroom agent attach <name>`.");
50903
51118
  return null;
50904
51119
  }
50905
- function copyOnboardingState(sourcePath, agentDir) {
50906
- const claudeDir = join7(agentDir, ".claude");
51120
+ function copyOnboardingState(sourcePath, agentDir, configDirName = ".claude") {
51121
+ const claudeDir = join7(agentDir, configDirName);
50907
51122
  mkdirSync7(claudeDir, { recursive: true });
50908
51123
  const destPath = join7(claudeDir, ".claude.json");
50909
51124
  if (!existsSync11(destPath)) {
@@ -50979,8 +51194,8 @@ function loadUserConfig() {
50979
51194
  return null;
50980
51195
  }
50981
51196
  }
50982
- function preTrustWorkspace(agentDir) {
50983
- const configPath = join7(agentDir, ".claude", ".claude.json");
51197
+ function preTrustWorkspace(agentDir, configDirName = ".claude") {
51198
+ const configPath = join7(agentDir, configDirName, ".claude.json");
50984
51199
  if (!existsSync11(configPath)) {
50985
51200
  return;
50986
51201
  }
@@ -51004,8 +51219,8 @@ function preTrustWorkspace(agentDir) {
51004
51219
  });
51005
51220
  } catch {}
51006
51221
  }
51007
- function ensureMcpServersTrusted(agentDir, serverKeys) {
51008
- const configPath = join7(agentDir, ".claude", ".claude.json");
51222
+ function ensureMcpServersTrusted(agentDir, serverKeys, configDirName = ".claude") {
51223
+ const configPath = join7(agentDir, configDirName, ".claude.json");
51009
51224
  if (!existsSync11(configPath)) {
51010
51225
  return;
51011
51226
  }
@@ -51033,8 +51248,8 @@ function ensureMcpServersTrusted(agentDir, serverKeys) {
51033
51248
  console.warn(` WARNING: could not update MCP trust allowlist in ${configPath} ` + `(${err instanceof Error ? err.message : String(err)}). ` + `Scaffolded MCP servers (gdrive, agent-config, hostd) may be ` + `silently ignored by Claude Code for this agent.`);
51034
51249
  }
51035
51250
  }
51036
- function createMinimalClaudeConfig(agentDir) {
51037
- const claudeDir = join7(agentDir, ".claude");
51251
+ function createMinimalClaudeConfig(agentDir, configDirName = ".claude") {
51252
+ const claudeDir = join7(agentDir, configDirName);
51038
51253
  mkdirSync7(claudeDir, { recursive: true });
51039
51254
  const configPath = join7(claudeDir, ".claude.json");
51040
51255
  if (!existsSync11(configPath)) {
@@ -51049,6 +51264,11 @@ function createMinimalClaudeConfig(agentDir) {
51049
51264
  });
51050
51265
  }
51051
51266
  }
51267
+ function seedCronConfigDir(agentDir, serverKeys) {
51268
+ createMinimalClaudeConfig(agentDir, ".claude-cron");
51269
+ preTrustWorkspace(agentDir, ".claude-cron");
51270
+ ensureMcpServersTrusted(agentDir, serverKeys, ".claude-cron");
51271
+ }
51052
51272
 
51053
51273
  // src/repos/bare-clone.ts
51054
51274
  init_paths();
@@ -51513,20 +51733,33 @@ function buildCronSessionContext(agentConfig) {
51513
51733
  cronModelQ: shellSingleQuote(DEFAULT_CRON_MODEL)
51514
51734
  };
51515
51735
  }
51516
- function maybeWriteTrimmedCronMcp(agentDir, mcpServers, cronSessionEnabled) {
51736
+ function maybeWriteCronMcp(agentDir, mcpServers, cronSessionEnabled) {
51517
51737
  if (!cronSessionEnabled)
51518
51738
  return null;
51519
51739
  const telegram = mcpServers["switchroom-telegram"];
51520
51740
  if (!telegram)
51521
51741
  return null;
51742
+ const baseStateDir = telegram.env?.TELEGRAM_STATE_DIR;
51743
+ const cronTelegram = {
51744
+ ...telegram,
51745
+ env: {
51746
+ ...telegram.env,
51747
+ ...baseStateDir ? { SWITCHROOM_BRIDGE_ALIVE_PATH: `${baseStateDir}/.bridge-alive-cron` } : {}
51748
+ }
51749
+ };
51750
+ const cronServers = {
51751
+ ...mcpServers,
51752
+ "switchroom-telegram": cronTelegram
51753
+ };
51522
51754
  const cronDir = join9(agentDir, ".claude-cron");
51523
51755
  mkdirSync10(cronDir, { recursive: true });
51524
51756
  const path = join9(cronDir, ".mcp.json");
51525
- const content = JSON.stringify({ mcpServers: { "switchroom-telegram": telegram } }, null, 2) + `
51757
+ const content = JSON.stringify({ mcpServers: cronServers }, null, 2) + `
51526
51758
  `;
51527
51759
  if (!existsSync14(path) || readFileSync12(path, "utf-8") !== content) {
51528
51760
  writeFileSync5(path, content, { encoding: "utf-8", mode: 384 });
51529
51761
  }
51762
+ seedCronConfigDir(agentDir, Object.keys(cronServers));
51530
51763
  return path;
51531
51764
  }
51532
51765
  function alignAgentUid(name, agentDir, uid, opts = {}) {
@@ -51719,6 +51952,11 @@ function webkiteDenyForAgent(agentConfig) {
51719
51952
  }
51720
51953
  var SWITCHROOM_DEFAULT_MAIN_MODEL = "claude-sonnet-4-6";
51721
51954
  var SWITCHROOM_DEFAULT_THINKING_EFFORT = "low";
51955
+ function resolveMainModel(model) {
51956
+ if (model === undefined || model === "default")
51957
+ return SWITCHROOM_DEFAULT_MAIN_MODEL;
51958
+ return model;
51959
+ }
51722
51960
  function dedupe2(items) {
51723
51961
  const seen = new Set;
51724
51962
  const out = [];
@@ -51988,6 +52226,10 @@ function channelsToEnv(agent) {
51988
52226
  if (tg.clear_status_on_completion !== undefined) {
51989
52227
  out.SWITCHROOM_TG_CLEAR_STATUS_ON_COMPLETION = tg.clear_status_on_completion ? "1" : "0";
51990
52228
  }
52229
+ const linearDefaultTeam = tg?.linear_agent?.default_team_id;
52230
+ if (linearDefaultTeam) {
52231
+ out.SWITCHROOM_LINEAR_DEFAULT_TEAM_ID = linearDefaultTeam;
52232
+ }
51991
52233
  return out;
51992
52234
  }
51993
52235
  function buildRepoEnvVars(_agentName, agentDir, agent) {
@@ -52327,6 +52569,7 @@ function buildWorkspaceContext(args) {
52327
52569
  useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
52328
52570
  useHotReloadStable: agentConfig.channels?.telegram?.hotReloadStable === true,
52329
52571
  telegramEnabledFlag: agentConfig.channels?.telegram?.enabled === false ? "false" : "true",
52572
+ linearAgentEnabled: agentConfig.channels?.telegram?.linear_agent?.enabled === true,
52330
52573
  securityPluginDir: DOCKER_SECURITY_PLUGIN_PATH,
52331
52574
  hindsightEnabled: hindsightAutoRecallEnabled,
52332
52575
  hindsightBankIdQ: shellSingleQuote(hindsightBankId),
@@ -52338,7 +52581,7 @@ function buildWorkspaceContext(args) {
52338
52581
  hindsightTopicFilterMode,
52339
52582
  switchroomConfigPathQ: switchroomConfigPath ? shellSingleQuote(resolve11(switchroomConfigPath)) : undefined,
52340
52583
  hostHomeQ: process.env.HOME ? shellSingleQuote(process.env.HOME) : undefined,
52341
- modelQ: shellSingleQuote(agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL),
52584
+ modelQ: shellSingleQuote(resolveMainModel(agentConfig.model)),
52342
52585
  ...buildCronSessionContext(agentConfig),
52343
52586
  thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
52344
52587
  permissionMode: agentConfig.permission_mode,
@@ -52638,7 +52881,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
52638
52881
  useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
52639
52882
  configPath: switchroomConfigPath
52640
52883
  });
52641
- settings.model = agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL;
52884
+ settings.model = resolveMainModel(agentConfig.model);
52642
52885
  const mergedSettings = agentConfig.settings_raw ? deepMergeJson(settings, agentConfig.settings_raw) : settings;
52643
52886
  if (agentConfig.settings_raw && Object.keys(agentConfig.settings_raw).length > 0) {
52644
52887
  mergedSettings._switchroomManagedRawKeys = Object.keys(agentConfig.settings_raw);
@@ -52709,7 +52952,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
52709
52952
  }
52710
52953
  writeIfChanged(mcpJsonPath, () => JSON.stringify({ mcpServers }, null, 2) + `
52711
52954
  `, created, skipped, 384);
52712
- maybeWriteTrimmedCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
52955
+ maybeWriteCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
52713
52956
  mcpServerKeysToTrust = Object.keys(mcpServers);
52714
52957
  ensureMcpServersTrusted(agentDir, mcpServerKeysToTrust);
52715
52958
  }
@@ -53429,7 +53672,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
53429
53672
  hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
53430
53673
  hindsightTopicFilterMode,
53431
53674
  hostHomeQ: process.env.HOME ? shellSingleQuote(process.env.HOME) : undefined,
53432
- modelQ: shellSingleQuote(agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL),
53675
+ modelQ: shellSingleQuote(resolveMainModel(agentConfig.model)),
53433
53676
  ...buildCronSessionContext(agentConfig),
53434
53677
  thinkingEffort: agentConfig.thinking_effort ?? SWITCHROOM_DEFAULT_THINKING_EFFORT,
53435
53678
  permissionMode: agentConfig.permission_mode,
@@ -53514,7 +53757,8 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
53514
53757
  schedule: agentConfig.schedule,
53515
53758
  useSwitchroomPlugin: usesSwitchroomTelegramPlugin(agentConfig),
53516
53759
  admin: agentConfig.admin === true || agentConfig.root === true,
53517
- root: agentConfig.root === true
53760
+ root: agentConfig.root === true,
53761
+ linearAgentEnabled: agentConfig.channels?.telegram?.linear_agent?.enabled === true
53518
53762
  };
53519
53763
  let rendered = renderTemplate(claudeMdSrc, claudeContext);
53520
53764
  const vaultProtocol = renderVaultProtocolFragment(claudeContext);
@@ -53680,7 +53924,7 @@ ${body}
53680
53924
  }
53681
53925
  }
53682
53926
  }
53683
- settings.model = agentConfig.model ?? SWITCHROOM_DEFAULT_MAIN_MODEL;
53927
+ settings.model = resolveMainModel(agentConfig.model);
53684
53928
  const mergedSettings = agentConfig.settings_raw ? deepMergeJson(settings, agentConfig.settings_raw) : settings;
53685
53929
  if (agentConfig.settings_raw && Object.keys(agentConfig.settings_raw).length > 0) {
53686
53930
  mergedSettings[META_KEY] = Object.keys(agentConfig.settings_raw);
@@ -53783,9 +54027,9 @@ ${body}
53783
54027
  writeFileSync5(mcpJsonPath, after, { encoding: "utf-8", mode: 384 });
53784
54028
  changes.push(mcpJsonPath);
53785
54029
  }
53786
- const trimmedCronMcp = maybeWriteTrimmedCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
53787
- if (trimmedCronMcp)
53788
- changes.push(trimmedCronMcp);
54030
+ const cronMcp = maybeWriteCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
54031
+ if (cronMcp)
54032
+ changes.push(cronMcp);
53789
54033
  ensureMcpServersTrusted(agentDir, Object.keys(mcpServers));
53790
54034
  }
53791
54035
  const reconcileWorkspaceDir = join9(agentDir, "workspace");
@@ -67041,6 +67285,22 @@ function setLinearAgent(yamlText, agentName, opts) {
67041
67285
  if (opts.workspaceId)
67042
67286
  block.workspace_id = opts.workspaceId;
67043
67287
  doc.setIn(["agents", agentName, "channels", "telegram", "linear_agent"], block);
67288
+ doc.setIn(["agents", agentName, "channels", "telegram", "webhook_via_gateway"], true);
67289
+ return String(doc);
67290
+ }
67291
+ function setLinearDefaultTeam(yamlText, agentName, teamId) {
67292
+ const doc = import_yaml11.parseDocument(yamlText);
67293
+ ensureAgent(doc, agentName);
67294
+ if (!doc.hasIn(["agents", agentName, "channels", "telegram", "linear_agent"])) {
67295
+ throw new Error(`agent '${agentName}' has no linear_agent block. Run 'switchroom linear-agent setup --agent ${agentName} --token <token>' first.`);
67296
+ }
67297
+ const path4 = ["agents", agentName, "channels", "telegram", "linear_agent", "default_team_id"];
67298
+ if (teamId === null) {
67299
+ if (doc.hasIn(path4))
67300
+ doc.deleteIn(path4);
67301
+ } else {
67302
+ doc.setIn(path4, teamId);
67303
+ }
67044
67304
  return String(doc);
67045
67305
  }
67046
67306
  function setTelegramFeature(yamlText, agentName, feature, value) {
@@ -67642,6 +67902,29 @@ function registerLinearAgentCommand(program3) {
67642
67902
  }
67643
67903
  printLinearInstructions(opts, vaultKey);
67644
67904
  }));
67905
+ linear.command("set-team").description("Set (or clear) the default Linear team captured issues file into for <agent>. Only needed when the workspace has multiple teams \u2014 a single-team workspace auto-resolves. Pass --clear to remove the default.").requiredOption("--agent <name>", "Agent name (must have a linear_agent block)").option("--team <id>", "Linear team id new captured issues default to.").option("--clear", "Remove the configured default team (revert to auto-resolve).").action(withConfigError(async (opts) => {
67906
+ if (!/^[a-z][a-z0-9_-]{0,63}$/.test(opts.agent)) {
67907
+ fail2(`--agent must be a lowercase agent slug (got '${opts.agent}').`);
67908
+ }
67909
+ if (!opts.clear && (!opts.team || opts.team.trim().length === 0)) {
67910
+ fail2("pass either --team <id> or --clear.");
67911
+ }
67912
+ const path4 = getConfigPath(program3);
67913
+ const before = readFileSync39(path4, "utf-8");
67914
+ let after;
67915
+ try {
67916
+ after = setLinearDefaultTeam(before, opts.agent, opts.clear ? null : opts.team.trim());
67917
+ } catch (err) {
67918
+ fail2(err.message);
67919
+ }
67920
+ writeFileSync22(path4, after, "utf-8");
67921
+ if (opts.clear) {
67922
+ console.log(source_default.green(`\u2713 Cleared default Linear team for '${opts.agent}' (auto-resolve).`));
67923
+ } else {
67924
+ console.log(source_default.green(`\u2713 Default Linear team for '${opts.agent}' set to ${opts.team.trim()}.`));
67925
+ }
67926
+ console.log(source_default.gray(` Run 'switchroom agent restart ${opts.agent}' to pick up the change.`));
67927
+ }));
67645
67928
  }
67646
67929
  function printLinearInstructions(opts, vaultKey) {
67647
67930
  const base = opts.webhookBase ?? "https://<your-switchroom-web-host>";
@@ -68313,17 +68596,19 @@ function collectScheduleEntries(config) {
68313
68596
  const schedule = resolved.schedule ?? [];
68314
68597
  for (let i = 0;i < schedule.length; i++) {
68315
68598
  const entry = schedule[i];
68599
+ const auditMaterial = entry.prompt ?? `action:${JSON.stringify(entry.action ?? {})}`;
68316
68600
  out.push({
68317
68601
  agent,
68318
68602
  scheduleIndex: i,
68319
68603
  cron: entry.cron,
68320
- prompt: entry.prompt,
68321
- promptKey: createHash9("sha256").update(entry.prompt).digest("hex").slice(0, 12),
68604
+ ...entry.prompt !== undefined ? { prompt: entry.prompt } : {},
68605
+ promptKey: createHash9("sha256").update(auditMaterial).digest("hex").slice(0, 12),
68322
68606
  ...entry.topic !== undefined ? { topic: entry.topic } : {},
68323
68607
  ...entry.kind !== undefined ? { kind: entry.kind } : {},
68324
68608
  ...entry.model !== undefined ? { model: entry.model } : {},
68325
68609
  ...entry.context !== undefined ? { context: entry.context } : {},
68326
- ...entry.poll !== undefined ? { poll: entry.poll } : {}
68610
+ ...entry.poll !== undefined ? { poll: entry.poll } : {},
68611
+ ...entry.action !== undefined ? { action: entry.action } : {}
68327
68612
  });
68328
68613
  }
68329
68614
  }
@@ -76040,7 +76325,7 @@ init_loader();
76040
76325
  init_lifecycle();
76041
76326
  import { cpSync as cpSync2, existsSync as existsSync58, mkdirSync as mkdirSync32, readFileSync as readFileSync52, realpathSync as realpathSync6, rmSync as rmSync12, statSync as statSync26 } from "node:fs";
76042
76327
  import { spawnSync as spawnSync9 } from "node:child_process";
76043
- import { join as join60, dirname as dirname14, resolve as resolve33 } from "node:path";
76328
+ import { join as join60, dirname as dirname14, resolve as resolve34 } from "node:path";
76044
76329
  import { homedir as homedir36 } from "node:os";
76045
76330
  var DEFAULT_COMPOSE_PATH = join60(homedir36(), ".switchroom", "compose", "docker-compose.yml");
76046
76331
  function runningFromSwitchroomCheckout(scriptPath) {
@@ -76198,7 +76483,7 @@ function planUpdate(opts) {
76198
76483
  opts.syncBundledSkillsFn();
76199
76484
  return;
76200
76485
  }
76201
- const source = resolve33(import.meta.dirname, "../../skills");
76486
+ const source = resolve34(import.meta.dirname, "../../skills");
76202
76487
  const dest = join60(homedir36(), ".switchroom", "skills", "_bundled");
76203
76488
  if (!existsSync58(source)) {
76204
76489
  process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
@@ -76525,7 +76810,7 @@ init_source();
76525
76810
  init_helpers();
76526
76811
  init_loader();
76527
76812
  init_lifecycle();
76528
- import { resolve as resolve34 } from "node:path";
76813
+ import { resolve as resolve35 } from "node:path";
76529
76814
 
76530
76815
  // src/cli/version.ts
76531
76816
  init_source();
@@ -76680,7 +76965,7 @@ function registerRestartCommand(program3) {
76680
76965
  }
76681
76966
  const didRestart = res.restarted || !graceful;
76682
76967
  if (didRestart) {
76683
- const agentDir = resolve34(agentsDir, name);
76968
+ const agentDir = resolve35(agentsDir, name);
76684
76969
  const converged = waitForAuthConverge(name, agentDir);
76685
76970
  if (!converged) {
76686
76971
  console.log(source_default.yellow(` ${name}: agent is up but auth status didn't converge in 30s \u2014 check logs`));
@@ -76761,7 +77046,7 @@ Dependency manifest`));
76761
77046
  // src/cli/handoff.ts
76762
77047
  init_helpers();
76763
77048
  init_loader();
76764
- import { resolve as resolve35 } from "node:path";
77049
+ import { resolve as resolve36 } from "node:path";
76765
77050
  function registerHandoffCommand(program3) {
76766
77051
  program3.command("handoff <agent>", { hidden: true }).description("Build the agent's session handoff sidecars \u2014 a transcript-tail " + "briefing (.handoff.md) and topic line (.handoff-topic). " + "[internal \u2014 used by the Stop hook]").option("--max-turns <n>", "Max turns kept in the handoff transcript tail", String(DEFAULT_MAX_TURNS)).action(withConfigError(async (agentName, opts) => {
76767
77052
  let agentConfig;
@@ -76775,7 +77060,7 @@ function registerHandoffCommand(program3) {
76775
77060
  return;
76776
77061
  }
76777
77062
  const agentsDir = resolveAgentsDir(config);
76778
- agentDir = resolve35(agentsDir, agentName);
77063
+ agentDir = resolve36(agentsDir, agentName);
76779
77064
  } catch (err) {
76780
77065
  if (!(err instanceof ConfigError))
76781
77066
  throw err;
@@ -76790,7 +77075,7 @@ function registerHandoffCommand(program3) {
76790
77075
  `);
76791
77076
  return;
76792
77077
  }
76793
- const claudeConfigDir = resolve35(agentDir, ".claude");
77078
+ const claudeConfigDir = resolve36(agentDir, ".claude");
76794
77079
  const jsonl = findLatestSessionJsonl(claudeConfigDir);
76795
77080
  if (!jsonl) {
76796
77081
  process.stderr.write(`handoff: no session JSONL under ${claudeConfigDir}/projects; skipping
@@ -77299,7 +77584,7 @@ function record(stateDir, input, nowFn = Date.now) {
77299
77584
  return result;
77300
77585
  });
77301
77586
  }
77302
- function resolve36(stateDir, fingerprint, nowFn = Date.now) {
77587
+ function resolve37(stateDir, fingerprint, nowFn = Date.now) {
77303
77588
  if (!existsSync60(join62(stateDir, ISSUES_FILE)))
77304
77589
  return 0;
77305
77590
  return withLock(stateDir, () => {
@@ -77587,11 +77872,11 @@ function registerIssuesCommand(program3) {
77587
77872
  const stateDir = resolveStateDir2(opts);
77588
77873
  let flipped;
77589
77874
  if (opts.source && opts.code) {
77590
- flipped = resolve36(stateDir, computeFingerprint(opts.source, opts.code));
77875
+ flipped = resolve37(stateDir, computeFingerprint(opts.source, opts.code));
77591
77876
  } else if (opts.source && !fingerprint) {
77592
77877
  flipped = resolveAllBySource(stateDir, opts.source);
77593
77878
  } else if (fingerprint) {
77594
- flipped = resolve36(stateDir, fingerprint);
77879
+ flipped = resolve37(stateDir, fingerprint);
77595
77880
  } else {
77596
77881
  process.stderr.write(`issues resolve: need either <fingerprint>, --source, or --source + --code
77597
77882
  `);
@@ -77688,7 +77973,7 @@ function relTime(deltaMs) {
77688
77973
  init_source();
77689
77974
  import { existsSync as existsSync63 } from "node:fs";
77690
77975
  import { homedir as homedir39 } from "node:os";
77691
- import { join as join65, resolve as resolve37 } from "node:path";
77976
+ import { join as join65, resolve as resolve38 } from "node:path";
77692
77977
 
77693
77978
  // src/deps/python.ts
77694
77979
  import { createHash as createHash11 } from "node:crypto";
@@ -77895,7 +78180,7 @@ function ensureNodeEnv(opts) {
77895
78180
 
77896
78181
  // src/cli/deps.ts
77897
78182
  function builtinSkillsRoot() {
77898
- return resolve37(homedir39(), ".switchroom/skills/_bundled");
78183
+ return resolve38(homedir39(), ".switchroom/skills/_bundled");
77899
78184
  }
77900
78185
  function registerDepsCommand(program3) {
77901
78186
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
@@ -77974,7 +78259,7 @@ function registerDepsCommand(program3) {
77974
78259
  init_helpers();
77975
78260
  init_loader();
77976
78261
  import { existsSync as existsSync64 } from "node:fs";
77977
- import { resolve as resolve38, sep as sep3 } from "node:path";
78262
+ import { resolve as resolve39, sep as sep3 } from "node:path";
77978
78263
  import { spawnSync as spawnSync10 } from "node:child_process";
77979
78264
 
77980
78265
  // src/agents/workspace.ts
@@ -78680,8 +78965,8 @@ function registerWorkspaceCommand(program3) {
78680
78965
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
78681
78966
  if (!dir)
78682
78967
  return;
78683
- const resolvedWorkspace = resolve38(dir);
78684
- const target = resolve38(resolvedWorkspace, file ?? "AGENTS.md");
78968
+ const resolvedWorkspace = resolve39(dir);
78969
+ const target = resolve39(resolvedWorkspace, file ?? "AGENTS.md");
78685
78970
  if (!isInsideWorkspace(resolvedWorkspace, target)) {
78686
78971
  process.stderr.write(`workspace edit: refusing path traversal outside workspace dir (${target})
78687
78972
  `);
@@ -78749,7 +79034,7 @@ function registerWorkspaceCommand(program3) {
78749
79034
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
78750
79035
  if (!dir)
78751
79036
  return;
78752
- const gitDir = resolve38(dir, ".git");
79037
+ const gitDir = resolve39(dir, ".git");
78753
79038
  if (!existsSync64(gitDir)) {
78754
79039
  process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
78755
79040
  `);
@@ -78803,7 +79088,7 @@ function registerWorkspaceCommand(program3) {
78803
79088
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
78804
79089
  if (!dir)
78805
79090
  return;
78806
- const gitDir = resolve38(dir, ".git");
79091
+ const gitDir = resolve39(dir, ".git");
78807
79092
  if (!existsSync64(gitDir)) {
78808
79093
  process.stdout.write(`Workspace is not a git repository.
78809
79094
  `);
@@ -78827,7 +79112,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
78827
79112
  return;
78828
79113
  }
78829
79114
  const agentsDir = resolveAgentsDir(config);
78830
- const agentDir = resolve38(agentsDir, agentName);
79115
+ const agentDir = resolve39(agentsDir, agentName);
78831
79116
  const dir = resolveAgentWorkspaceDir(agentDir);
78832
79117
  if (!existsSync64(dir)) {
78833
79118
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
@@ -78866,7 +79151,7 @@ init_helpers();
78866
79151
  init_loader();
78867
79152
  init_merge();
78868
79153
  import { copyFileSync as copyFileSync10, existsSync as existsSync65, readFileSync as readFileSync57, writeFileSync as writeFileSync31 } from "node:fs";
78869
- import { join as join66, resolve as resolve39 } from "node:path";
79154
+ import { join as join66, resolve as resolve40 } from "node:path";
78870
79155
  init_schema();
78871
79156
  function resolveSoulTargetOrExit(program3, agentName) {
78872
79157
  const config = getConfig(program3);
@@ -78879,7 +79164,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
78879
79164
  const profileName = merged.extends ?? DEFAULT_PROFILE;
78880
79165
  const profilePath = getProfilePath(profileName);
78881
79166
  const agentsDir = resolveAgentsDir(config);
78882
- const agentDir = resolve39(agentsDir, agentName);
79167
+ const agentDir = resolve40(agentsDir, agentName);
78883
79168
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
78884
79169
  if (!existsSync65(workspaceDir)) {
78885
79170
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
@@ -78957,7 +79242,7 @@ function registerSoulCommand(program3) {
78957
79242
  init_helpers();
78958
79243
  init_loader();
78959
79244
  import { existsSync as existsSync66, readFileSync as readFileSync58, readdirSync as readdirSync23, statSync as statSync28 } from "node:fs";
78960
- import { resolve as resolve40, join as join67 } from "node:path";
79245
+ import { resolve as resolve41, join as join67 } from "node:path";
78961
79246
  import { createHash as createHash13 } from "node:crypto";
78962
79247
  init_merge();
78963
79248
  init_hindsight();
@@ -79055,7 +79340,7 @@ function registerDebugCommand(program3) {
79055
79340
  process.exit(1);
79056
79341
  }
79057
79342
  const agentsDir = resolveAgentsDir(config);
79058
- const agentDir = resolve40(agentsDir, agentName);
79343
+ const agentDir = resolve41(agentsDir, agentName);
79059
79344
  if (!existsSync66(agentDir)) {
79060
79345
  console.error(`Agent directory not found: ${agentDir}`);
79061
79346
  process.exit(1);
@@ -79232,7 +79517,7 @@ init_source();
79232
79517
  // src/worktree/claim.ts
79233
79518
  import { execFileSync as execFileSync21 } from "node:child_process";
79234
79519
  import { closeSync as closeSync12, mkdirSync as mkdirSync37, openSync as openSync12, existsSync as existsSync68, unlinkSync as unlinkSync13 } from "node:fs";
79235
- import { join as join69, resolve as resolve42 } from "node:path";
79520
+ import { join as join69, resolve as resolve43 } from "node:path";
79236
79521
  import { homedir as homedir41 } from "node:os";
79237
79522
  import { randomBytes as randomBytes13 } from "node:crypto";
79238
79523
 
@@ -79246,10 +79531,10 @@ import {
79246
79531
  existsSync as existsSync67,
79247
79532
  renameSync as renameSync13
79248
79533
  } from "node:fs";
79249
- import { join as join68, resolve as resolve41 } from "node:path";
79534
+ import { join as join68, resolve as resolve42 } from "node:path";
79250
79535
  import { homedir as homedir40 } from "node:os";
79251
79536
  function registryDir() {
79252
- return resolve41(process.env.SWITCHROOM_WORKTREE_DIR ?? join68(homedir40(), ".switchroom", "worktrees"));
79537
+ return resolve42(process.env.SWITCHROOM_WORKTREE_DIR ?? join68(homedir40(), ".switchroom", "worktrees"));
79253
79538
  }
79254
79539
  function recordPath(id) {
79255
79540
  return join68(registryDir(), `${id}.json`);
@@ -79330,7 +79615,7 @@ function acquireRepoLock(repoPath) {
79330
79615
  }
79331
79616
  var DEFAULT_CONCURRENCY = 5;
79332
79617
  function worktreesBaseDir() {
79333
- return resolve42(process.env.SWITCHROOM_WORKTREE_BASE ?? join69(homedir41(), ".switchroom", "worktree-checkouts"));
79618
+ return resolve43(process.env.SWITCHROOM_WORKTREE_BASE ?? join69(homedir41(), ".switchroom", "worktree-checkouts"));
79334
79619
  }
79335
79620
  function shortId() {
79336
79621
  return randomBytes13(4).toString("hex");
@@ -80050,20 +80335,20 @@ function wireStdio(child) {
80050
80335
  async function killChild(child, gracefulMs = 3000) {
80051
80336
  if (child.exitCode !== null || child.signalCode !== null)
80052
80337
  return;
80053
- return new Promise((resolve43) => {
80338
+ return new Promise((resolve44) => {
80054
80339
  let killTimer = null;
80055
80340
  const onExit = () => {
80056
80341
  if (killTimer) {
80057
80342
  clearTimeout(killTimer);
80058
80343
  killTimer = null;
80059
80344
  }
80060
- resolve43();
80345
+ resolve44();
80061
80346
  };
80062
80347
  child.once("exit", onExit);
80063
80348
  try {
80064
80349
  child.kill("SIGTERM");
80065
80350
  } catch {
80066
- resolve43();
80351
+ resolve44();
80067
80352
  return;
80068
80353
  }
80069
80354
  killTimer = setTimeout(() => {
@@ -80177,8 +80462,8 @@ async function runMs365McpLauncher(opts, rt) {
80177
80462
  };
80178
80463
  process.on("SIGINT", onSignal);
80179
80464
  process.on("SIGTERM", onSignal);
80180
- return new Promise((resolve43) => {
80181
- resolveLauncher = resolve43;
80465
+ return new Promise((resolve44) => {
80466
+ resolveLauncher = resolve44;
80182
80467
  });
80183
80468
  }
80184
80469
  function registerM365McpLauncherCommand(program3) {
@@ -80280,23 +80565,23 @@ async function runNotionMcpLauncher(opts, runtime) {
80280
80565
  };
80281
80566
  process.on("SIGTERM", () => forward("SIGTERM"));
80282
80567
  process.on("SIGINT", () => forward("SIGINT"));
80283
- const exitCode = await new Promise((resolve43) => {
80568
+ const exitCode = await new Promise((resolve44) => {
80284
80569
  child.once("exit", (code, signal) => {
80285
80570
  clearTimer(heartbeatHandle);
80286
80571
  if (typeof code === "number") {
80287
- resolve43(code);
80572
+ resolve44(code);
80288
80573
  } else if (signal) {
80289
80574
  const sigCode = { SIGTERM: 15, SIGINT: 2, SIGKILL: 9 }[signal] ?? 1;
80290
- resolve43(128 + sigCode);
80575
+ resolve44(128 + sigCode);
80291
80576
  } else {
80292
- resolve43(1);
80577
+ resolve44(1);
80293
80578
  }
80294
80579
  });
80295
80580
  child.once("error", (err) => {
80296
80581
  clearTimer(heartbeatHandle);
80297
80582
  process.stderr.write(`notion-mcp-launcher: child spawn error: ${err.message}
80298
80583
  `);
80299
- resolve43(1);
80584
+ resolve44(1);
80300
80585
  });
80301
80586
  });
80302
80587
  return exitCode;
@@ -81354,7 +81639,7 @@ agents:
81354
81639
 
81355
81640
  // src/cli/apply.ts
81356
81641
  init_resolver();
81357
- import { dirname as dirname22, join as join74, resolve as resolve44 } from "node:path";
81642
+ import { dirname as dirname22, join as join74, resolve as resolve45 } from "node:path";
81358
81643
  import { homedir as homedir43 } from "node:os";
81359
81644
  import { execFileSync as execFileSync24 } from "node:child_process";
81360
81645
  init_vault();
@@ -81955,7 +82240,7 @@ function copyExampleConfig2(name) {
81955
82240
  if (!/^[a-z0-9_-]+$/.test(name)) {
81956
82241
  throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
81957
82242
  }
81958
- const dest = resolve44(process.cwd(), "switchroom.yaml");
82243
+ const dest = resolve45(process.cwd(), "switchroom.yaml");
81959
82244
  if (existsSync74(dest)) {
81960
82245
  console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
81961
82246
  return;
@@ -81966,7 +82251,7 @@ function copyExampleConfig2(name) {
81966
82251
  console.log(source_default.green(`Copied ${name}.yaml -> switchroom.yaml`));
81967
82252
  return;
81968
82253
  }
81969
- const exampleFile = resolve44(import.meta.dirname, `../../examples/${name}.yaml`);
82254
+ const exampleFile = resolve45(import.meta.dirname, `../../examples/${name}.yaml`);
81970
82255
  if (!existsSync74(exampleFile)) {
81971
82256
  throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
81972
82257
  }
@@ -82544,10 +82829,10 @@ import {
82544
82829
  unlinkSync as unlinkSync14,
82545
82830
  writeSync as writeSync8
82546
82831
  } from "node:fs";
82547
- import { join as join76, resolve as resolve45 } from "node:path";
82832
+ import { join as join76, resolve as resolve46 } from "node:path";
82548
82833
  var STAGING_SUBDIR = ".staging";
82549
82834
  function overlayPathsFor(agent, opts = {}) {
82550
- const base = opts.root ? resolve45(opts.root, agent) : resolve45(resolveDualPath(`~/.switchroom/agents/${agent}`));
82835
+ const base = opts.root ? resolve46(opts.root, agent) : resolve46(resolveDualPath(`~/.switchroom/agents/${agent}`));
82551
82836
  const scheduleDir = join76(base, "schedule.d");
82552
82837
  const scheduleStagingDir = join76(scheduleDir, STAGING_SUBDIR);
82553
82838
  const skillsDir = join76(base, "skills.d");
@@ -82943,11 +83228,12 @@ import { existsSync as existsSync78, readFileSync as readFileSync66 } from "node
82943
83228
  import { execFileSync as execFileSync25 } from "node:child_process";
82944
83229
 
82945
83230
  // src/scheduler/schedule-report.ts
82946
- var TIER_WEIGHT = { poll: 0, cheap: 1, main: 5 };
83231
+ var TIER_WEIGHT = { poll: 0, action: 0, cheap: 1, main: 5 };
82947
83232
  function summarizeScheduleReport(rows, opts = {}) {
82948
83233
  const s = {
82949
83234
  total: 0,
82950
83235
  pollFires: 0,
83236
+ actionFires: 0,
82951
83237
  cheapFires: 0,
82952
83238
  mainFires: 0,
82953
83239
  errors: 0,
@@ -82962,6 +83248,8 @@ function summarizeScheduleReport(rows, opts = {}) {
82962
83248
  const tier = r.tier ?? "main";
82963
83249
  if (tier === "poll")
82964
83250
  s.pollFires += 1;
83251
+ else if (tier === "action")
83252
+ s.actionFires += 1;
82965
83253
  else if (tier === "cheap")
82966
83254
  s.cheapFires += 1;
82967
83255
  else
@@ -82993,11 +83281,12 @@ function parseScheduleJsonl(blob) {
82993
83281
  }
82994
83282
  function formatScheduleReport(agent, s) {
82995
83283
  const modelFires = s.cheapFires + s.mainFires;
82996
- const savedPct = s.total > 0 ? Math.round(s.pollFires / s.total * 100) : 0;
83284
+ const modelFreePct = s.total > 0 ? Math.round((s.pollFires + s.actionFires) / s.total * 100) : 0;
82997
83285
  const lines = [
82998
83286
  `cron report \u2014 ${agent}`,
82999
83287
  ` total fires ${s.total}`,
83000
- ` Tier 0 poll (free) ${s.pollFires} (${savedPct}% model-free)`,
83288
+ ` Tier 0 poll (free) ${s.pollFires} (${modelFreePct}% model-free incl. actions)`,
83289
+ ` Tier 0 action(free)${s.actionFires}`,
83001
83290
  ` Tier 1 cheap ${s.cheapFires}`,
83002
83291
  ` Tier 2 main ${s.mainFires}`,
83003
83292
  ` model fires ${modelFires}`,
@@ -83799,7 +84088,7 @@ import {
83799
84088
  writeFileSync as writeFileSync39
83800
84089
  } from "node:fs";
83801
84090
  import { tmpdir as tmpdir5, homedir as homedir45 } from "node:os";
83802
- import { dirname as dirname23, join as join79, relative as relative2, resolve as resolve46 } from "node:path";
84091
+ import { dirname as dirname23, join as join79, relative as relative2, resolve as resolve47 } from "node:path";
83803
84092
  import { spawnSync as spawnSync11 } from "node:child_process";
83804
84093
 
83805
84094
  // src/cli/skill-common.ts
@@ -83997,7 +84286,7 @@ function resolveSkillsPoolDir2(override) {
83997
84286
  }
83998
84287
  if (raw === "~")
83999
84288
  return homedir45();
84000
- return resolve46(raw);
84289
+ return resolve47(raw);
84001
84290
  }
84002
84291
  function readStdinSync() {
84003
84292
  const chunks = [];
@@ -84281,7 +84570,7 @@ function registerSkillCommand(program3) {
84281
84570
  if (opts.from === undefined) {
84282
84571
  files = loadFromStdin();
84283
84572
  } else {
84284
- const fromPath = resolve46(opts.from);
84573
+ const fromPath = resolve47(opts.from);
84285
84574
  if (!existsSync80(fromPath)) {
84286
84575
  fail3(`--from path does not exist: ${opts.from}`);
84287
84576
  }
@@ -84350,7 +84639,7 @@ import {
84350
84639
  utimesSync,
84351
84640
  writeFileSync as writeFileSync40
84352
84641
  } from "node:fs";
84353
- import { dirname as dirname24, join as join80, relative as relative3, resolve as resolve47 } from "node:path";
84642
+ import { dirname as dirname24, join as join80, relative as relative3, resolve as resolve48 } from "node:path";
84354
84643
  import { homedir as homedir46, tmpdir as tmpdir6 } from "node:os";
84355
84644
  import { spawnSync as spawnSync12 } from "node:child_process";
84356
84645
  init_helpers();
@@ -84361,7 +84650,7 @@ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
84361
84650
  var PERSONAL_SKILLS_SUBPATH = "personal-skills";
84362
84651
  function resolveConfigSkillsDir(agent) {
84363
84652
  const override = process.env.SWITCHROOM_CONFIG_DIR;
84364
- const candidate = override ? resolve47(override) : join80(homedir46(), ".switchroom-config");
84653
+ const candidate = override ? resolve48(override) : join80(homedir46(), ".switchroom-config");
84365
84654
  if (!existsSync81(candidate))
84366
84655
  return null;
84367
84656
  return join80(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
@@ -84456,7 +84745,7 @@ function resolveAgent(opts) {
84456
84745
  }
84457
84746
  function resolveAgentsRoot(opts) {
84458
84747
  if (opts.root)
84459
- return resolve47(opts.root);
84748
+ return resolve48(opts.root);
84460
84749
  return join80(homedir46(), ".switchroom", "agents");
84461
84750
  }
84462
84751
  function personalSkillDir(agentsRoot, agent, name) {
@@ -84486,7 +84775,7 @@ function readStdinSync2() {
84486
84775
  return Buffer.concat(chunks).toString("utf-8");
84487
84776
  }
84488
84777
  function loadFromDir2(dir) {
84489
- const abs = resolve47(dir);
84778
+ const abs = resolve48(dir);
84490
84779
  if (!statSync32(abs).isDirectory()) {
84491
84780
  fail4(`--from path is not a directory: ${dir}`);
84492
84781
  }
@@ -84680,7 +84969,7 @@ function loadFiles(opts) {
84680
84969
  if (opts.from === undefined) {
84681
84970
  return loadFromStdin2();
84682
84971
  }
84683
- const p = resolve47(opts.from);
84972
+ const p = resolve48(opts.from);
84684
84973
  if (!existsSync81(p)) {
84685
84974
  fail4(`--from path does not exist: ${opts.from}`);
84686
84975
  }
@@ -84949,18 +85238,18 @@ init_helpers();
84949
85238
  var import_yaml23 = __toESM(require_dist(), 1);
84950
85239
  import { existsSync as existsSync82, readdirSync as readdirSync32, readFileSync as readFileSync69, statSync as statSync33 } from "node:fs";
84951
85240
  import { homedir as homedir47 } from "node:os";
84952
- import { join as join81, resolve as resolve48 } from "node:path";
85241
+ import { join as join81, resolve as resolve49 } from "node:path";
84953
85242
  var PERSONAL_PREFIX2 = "personal-";
84954
85243
  var BUNDLED_SUBDIR = "_bundled";
84955
85244
  var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
84956
85245
  function defaultAgentsRoot() {
84957
- return resolve48(homedir47(), ".switchroom/agents");
85246
+ return resolve49(homedir47(), ".switchroom/agents");
84958
85247
  }
84959
85248
  function defaultSharedRoot2() {
84960
- return resolve48(homedir47(), ".switchroom/skills");
85249
+ return resolve49(homedir47(), ".switchroom/skills");
84961
85250
  }
84962
85251
  function defaultBundledRoot2() {
84963
- return resolve48(homedir47(), ".switchroom/skills/_bundled");
85252
+ return resolve49(homedir47(), ".switchroom/skills/_bundled");
84964
85253
  }
84965
85254
  function readSkillFrontmatter(skillDir) {
84966
85255
  const mdPath = join81(skillDir, "SKILL.md");