switchroom 0.15.13 → 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.
@@ -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')"),
@@ -15407,6 +15453,9 @@ function resolveCronModel(model) {
15407
15453
  return isKnownCheapModel(model) ? model : DEFAULT_CRON_MODEL;
15408
15454
  }
15409
15455
  function resolveCronRouting(input, opts) {
15456
+ if ((input.kind ?? "prompt") === "action") {
15457
+ return { tier: "action", session: null, customModelDowngrade: false };
15458
+ }
15410
15459
  if (!opts.cheapCronEnabled) {
15411
15460
  return { tier: "main", session: "main", customModelDowngrade: false };
15412
15461
  }
@@ -15451,9 +15500,96 @@ var init_cron_routing = __esm(() => {
15451
15500
  OPUS_MODEL_RE = /opus/i;
15452
15501
  });
15453
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
+
15454
15551
  // src/scheduler/tier-selector.ts
15455
- function applyDefaultTier(entry, _frequentGapMin = DEFAULT_FREQUENT_GAP_MIN) {
15456
- 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;
15457
15593
  }
15458
15594
  var DEFAULT_FREQUENT_GAP_MIN = 60;
15459
15595
  var init_tier_selector = __esm(() => {
@@ -30769,8 +30905,74 @@ var init_doctor_webkite = __esm(() => {
30769
30905
  init_loader();
30770
30906
  });
30771
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
+
30772
30974
  // src/cli/doctor-scaffold-wiring.ts
30773
- import { join as join52, resolve as resolve31 } from "node:path";
30975
+ import { join as join52, resolve as resolve32 } from "node:path";
30774
30976
  function readJson2(d, path4) {
30775
30977
  if (!d.existsSync(path4))
30776
30978
  return { kind: "absent" };
@@ -30804,7 +31006,7 @@ function checkIntegrationScaffoldWiring(args) {
30804
31006
  ];
30805
31007
  }
30806
31008
  for (const name of agents) {
30807
- const agentDir = resolve31(agentsDir, name);
31009
+ const agentDir = resolve32(agentsDir, name);
30808
31010
  if (!deps.existsSync(agentDir)) {
30809
31011
  results.push({
30810
31012
  name: `${label}: ${name} scaffold`,
@@ -30844,7 +31046,7 @@ function checkIntegrationScaffoldWiring(args) {
30844
31046
  let trustDetail = "no .claude/.claude.json";
30845
31047
  if (trustRead.kind === "ok") {
30846
31048
  const projects = trustRead.data?.projects ?? {};
30847
- const proj = projects[resolve31(agentDir)];
31049
+ const proj = projects[resolve32(agentDir)];
30848
31050
  const enabled = proj?.enabledMcpjsonServers;
30849
31051
  if (Array.isArray(enabled) && enabled.includes(mcpKey)) {
30850
31052
  trustOk = true;
@@ -31063,7 +31265,7 @@ var init_doctor_microsoft = __esm(() => {
31063
31265
  import {
31064
31266
  existsSync as realExistsSync4,
31065
31267
  readFileSync as realReadFileSync4,
31066
- statSync as realStatSync
31268
+ statSync as realStatSync2
31067
31269
  } from "node:fs";
31068
31270
  import { join as join54 } from "node:path";
31069
31271
  import { homedir as homedir31 } from "node:os";
@@ -31072,7 +31274,7 @@ function resolveDeps4(deps) {
31072
31274
  return {
31073
31275
  existsSync: deps.existsSync ?? realExistsSync4,
31074
31276
  readFileSync: deps.readFileSync ?? realReadFileSync4,
31075
- statSync: deps.statSync ?? realStatSync,
31277
+ statSync: deps.statSync ?? realStatSync2,
31076
31278
  agentsDir: join54(home2, ".switchroom", "agents"),
31077
31279
  now: deps.now ?? Date.now,
31078
31280
  vaultAclReader: deps.vaultAclReader ?? (async () => ({ kind: "unreachable", msg: "no default reader wired" }))
@@ -31271,7 +31473,7 @@ var init_doctor_notion = __esm(() => {
31271
31473
  import {
31272
31474
  existsSync as realExistsSync5,
31273
31475
  readdirSync as realReaddirSync2,
31274
- statSync as realStatSync2
31476
+ statSync as realStatSync3
31275
31477
  } from "node:fs";
31276
31478
  import { homedir as homedir32 } from "node:os";
31277
31479
  import { join as join55 } from "node:path";
@@ -31281,7 +31483,7 @@ function runCredentialsMigrationChecks(config, deps = {}) {
31281
31483
  const readdirSync21 = deps.readdirSync ?? ((p) => realReaddirSync2(p));
31282
31484
  const isDirectory = deps.isDirectory ?? ((p) => {
31283
31485
  try {
31284
- return realStatSync2(p).isDirectory();
31486
+ return realStatSync3(p).isDirectory();
31285
31487
  } catch {
31286
31488
  return false;
31287
31489
  }
@@ -31923,7 +32125,7 @@ import {
31923
32125
  readdirSync as readdirSync21,
31924
32126
  statSync as statSync25
31925
32127
  } from "node:fs";
31926
- 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";
31927
32129
  import { createPublicKey, createPrivateKey } from "node:crypto";
31928
32130
  function findInNvm(bin) {
31929
32131
  const nvmRoot = join59(process.env.HOME ?? "", ".nvm", "versions", "node");
@@ -32937,7 +33139,7 @@ function checkAgents(config, configPath) {
32937
33139
  const statuses = getAllAgentStatuses(config);
32938
33140
  const authStatuses = getAllAuthStatuses(config);
32939
33141
  for (const [name, agentConfig] of Object.entries(config.agents)) {
32940
- const agentDir = resolve32(agentsDir, name);
33142
+ const agentDir = resolve33(agentsDir, name);
32941
33143
  if (!existsSync57(agentDir)) {
32942
33144
  results.push({
32943
33145
  name: `${name}: scaffold`,
@@ -33120,7 +33322,7 @@ function mffAgentName(config) {
33120
33322
  function mffEnvPath(config) {
33121
33323
  const home2 = process.env.HOME ?? "/root";
33122
33324
  const agent = mffAgentName(config);
33123
- 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");
33124
33326
  }
33125
33327
  function mffEnvState(envPath) {
33126
33328
  if (!existsSync57(envPath))
@@ -33536,14 +33738,14 @@ async function checkManifestDrift(probers) {
33536
33738
  return results;
33537
33739
  }
33538
33740
  function runDockerSection(config) {
33539
- const composePath = resolve32(process.env.HOME ?? "", ".switchroom", "compose", "docker-compose.yml");
33741
+ const composePath = resolve33(process.env.HOME ?? "", ".switchroom", "compose", "docker-compose.yml");
33540
33742
  const active = isDockerMode({ composePath });
33541
33743
  let composeYaml;
33542
33744
  let dockerfileAgent;
33543
33745
  try {
33544
33746
  composeYaml = readFileSync51(composePath, "utf8");
33545
33747
  } catch {}
33546
- const dockerfilePath = resolve32(process.env.HOME ?? "", ".switchroom", "docker", "Dockerfile.agent");
33748
+ const dockerfilePath = resolve33(process.env.HOME ?? "", ".switchroom", "docker", "Dockerfile.agent");
33547
33749
  try {
33548
33750
  dockerfileAgent = readFileSync51(dockerfilePath, "utf8");
33549
33751
  } catch {}
@@ -33668,7 +33870,8 @@ function registerDoctorCommand(program3) {
33668
33870
  })
33669
33871
  },
33670
33872
  { title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) },
33671
- { title: "Webkite", results: runWebkiteChecks(config) }
33873
+ { title: "Webkite", results: runWebkiteChecks(config) },
33874
+ { title: "Cron Session", results: runCronSessionChecks(config) }
33672
33875
  ];
33673
33876
  const cwd = process.cwd();
33674
33877
  if (isSwitchroomCheckout(cwd)) {
@@ -33730,6 +33933,7 @@ var init_doctor = __esm(() => {
33730
33933
  init_doctor_hostd();
33731
33934
  init_doctor_drive();
33732
33935
  init_doctor_webkite();
33936
+ init_doctor_cron_session();
33733
33937
  init_doctor_microsoft();
33734
33938
  init_doctor_notion();
33735
33939
  init_doctor_credentials_migration();
@@ -41914,7 +42118,7 @@ class Protocol {
41914
42118
  return;
41915
42119
  }
41916
42120
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
41917
- await new Promise((resolve49) => setTimeout(resolve49, pollInterval));
42121
+ await new Promise((resolve50) => setTimeout(resolve50, pollInterval));
41918
42122
  options?.signal?.throwIfAborted();
41919
42123
  }
41920
42124
  } catch (error2) {
@@ -41926,7 +42130,7 @@ class Protocol {
41926
42130
  }
41927
42131
  request(request, resultSchema, options) {
41928
42132
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
41929
- return new Promise((resolve49, reject) => {
42133
+ return new Promise((resolve50, reject) => {
41930
42134
  const earlyReject = (error2) => {
41931
42135
  reject(error2);
41932
42136
  };
@@ -42004,7 +42208,7 @@ class Protocol {
42004
42208
  if (!parseResult.success) {
42005
42209
  reject(parseResult.error);
42006
42210
  } else {
42007
- resolve49(parseResult.data);
42211
+ resolve50(parseResult.data);
42008
42212
  }
42009
42213
  } catch (error2) {
42010
42214
  reject(error2);
@@ -42195,12 +42399,12 @@ class Protocol {
42195
42399
  interval = task.pollInterval;
42196
42400
  }
42197
42401
  } catch {}
42198
- return new Promise((resolve49, reject) => {
42402
+ return new Promise((resolve50, reject) => {
42199
42403
  if (signal.aborted) {
42200
42404
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
42201
42405
  return;
42202
42406
  }
42203
- const timeoutId = setTimeout(resolve49, interval);
42407
+ const timeoutId = setTimeout(resolve50, interval);
42204
42408
  signal.addEventListener("abort", () => {
42205
42409
  clearTimeout(timeoutId);
42206
42410
  reject(new McpError(ErrorCode2.InvalidRequest, "Request cancelled"));
@@ -45185,7 +45389,7 @@ var require_compile = __commonJS((exports2) => {
45185
45389
  const schOrFunc = root.refs[ref];
45186
45390
  if (schOrFunc)
45187
45391
  return schOrFunc;
45188
- let _sch = resolve49.call(this, root, ref);
45392
+ let _sch = resolve50.call(this, root, ref);
45189
45393
  if (_sch === undefined) {
45190
45394
  const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref];
45191
45395
  const { schemaId } = this.opts;
@@ -45212,7 +45416,7 @@ var require_compile = __commonJS((exports2) => {
45212
45416
  function sameSchemaEnv(s1, s2) {
45213
45417
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
45214
45418
  }
45215
- function resolve49(root, ref) {
45419
+ function resolve50(root, ref) {
45216
45420
  let sch;
45217
45421
  while (typeof (sch = this.refs[ref]) == "string")
45218
45422
  ref = sch;
@@ -45742,7 +45946,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
45742
45946
  }
45743
45947
  return uri;
45744
45948
  }
45745
- function resolve49(baseURI, relativeURI, options) {
45949
+ function resolve50(baseURI, relativeURI, options) {
45746
45950
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
45747
45951
  const resolved = resolveComponent(parse6(baseURI, schemelessOptions), parse6(relativeURI, schemelessOptions), schemelessOptions, true);
45748
45952
  schemelessOptions.skipEscape = true;
@@ -45970,7 +46174,7 @@ var require_fast_uri = __commonJS((exports2, module) => {
45970
46174
  var fastUri = {
45971
46175
  SCHEMES,
45972
46176
  normalize,
45973
- resolve: resolve49,
46177
+ resolve: resolve50,
45974
46178
  resolveComponent,
45975
46179
  equal,
45976
46180
  serialize,
@@ -49353,12 +49557,12 @@ class StdioServerTransport {
49353
49557
  this.onclose?.();
49354
49558
  }
49355
49559
  send(message) {
49356
- return new Promise((resolve49) => {
49560
+ return new Promise((resolve50) => {
49357
49561
  const json = serializeMessage(message);
49358
49562
  if (this._stdout.write(json)) {
49359
- resolve49();
49563
+ resolve50();
49360
49564
  } else {
49361
- this._stdout.once("drain", resolve49);
49565
+ this._stdout.once("drain", resolve50);
49362
49566
  }
49363
49567
  });
49364
49568
  }
@@ -50263,8 +50467,8 @@ var {
50263
50467
  } = import__.default;
50264
50468
 
50265
50469
  // src/build-info.ts
50266
- var VERSION = "0.15.13";
50267
- var COMMIT_SHA = "36ba2682";
50470
+ var VERSION = "0.15.14";
50471
+ var COMMIT_SHA = "91ae15d3";
50268
50472
 
50269
50473
  // src/cli/agent.ts
50270
50474
  init_source();
@@ -50364,7 +50568,7 @@ import { createHash as createHash2 } from "node:crypto";
50364
50568
  // src/agents/cron-unit-name.ts
50365
50569
  import { createHash } from "node:crypto";
50366
50570
  function cronUnitHash(cron, prompt) {
50367
- 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);
50368
50572
  }
50369
50573
  function cronUnitName(cron, prompt) {
50370
50574
  return `cron-${cronUnitHash(cron, prompt)}`;
@@ -50913,8 +51117,8 @@ function findExistingClaudeJson() {
50913
51117
  console.warn(" Alternatively, agents can be onboarded individually via `switchroom agent attach <name>`.");
50914
51118
  return null;
50915
51119
  }
50916
- function copyOnboardingState(sourcePath, agentDir) {
50917
- const claudeDir = join7(agentDir, ".claude");
51120
+ function copyOnboardingState(sourcePath, agentDir, configDirName = ".claude") {
51121
+ const claudeDir = join7(agentDir, configDirName);
50918
51122
  mkdirSync7(claudeDir, { recursive: true });
50919
51123
  const destPath = join7(claudeDir, ".claude.json");
50920
51124
  if (!existsSync11(destPath)) {
@@ -50990,8 +51194,8 @@ function loadUserConfig() {
50990
51194
  return null;
50991
51195
  }
50992
51196
  }
50993
- function preTrustWorkspace(agentDir) {
50994
- const configPath = join7(agentDir, ".claude", ".claude.json");
51197
+ function preTrustWorkspace(agentDir, configDirName = ".claude") {
51198
+ const configPath = join7(agentDir, configDirName, ".claude.json");
50995
51199
  if (!existsSync11(configPath)) {
50996
51200
  return;
50997
51201
  }
@@ -51015,8 +51219,8 @@ function preTrustWorkspace(agentDir) {
51015
51219
  });
51016
51220
  } catch {}
51017
51221
  }
51018
- function ensureMcpServersTrusted(agentDir, serverKeys) {
51019
- const configPath = join7(agentDir, ".claude", ".claude.json");
51222
+ function ensureMcpServersTrusted(agentDir, serverKeys, configDirName = ".claude") {
51223
+ const configPath = join7(agentDir, configDirName, ".claude.json");
51020
51224
  if (!existsSync11(configPath)) {
51021
51225
  return;
51022
51226
  }
@@ -51044,8 +51248,8 @@ function ensureMcpServersTrusted(agentDir, serverKeys) {
51044
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.`);
51045
51249
  }
51046
51250
  }
51047
- function createMinimalClaudeConfig(agentDir) {
51048
- const claudeDir = join7(agentDir, ".claude");
51251
+ function createMinimalClaudeConfig(agentDir, configDirName = ".claude") {
51252
+ const claudeDir = join7(agentDir, configDirName);
51049
51253
  mkdirSync7(claudeDir, { recursive: true });
51050
51254
  const configPath = join7(claudeDir, ".claude.json");
51051
51255
  if (!existsSync11(configPath)) {
@@ -51060,6 +51264,11 @@ function createMinimalClaudeConfig(agentDir) {
51060
51264
  });
51061
51265
  }
51062
51266
  }
51267
+ function seedCronConfigDir(agentDir, serverKeys) {
51268
+ createMinimalClaudeConfig(agentDir, ".claude-cron");
51269
+ preTrustWorkspace(agentDir, ".claude-cron");
51270
+ ensureMcpServersTrusted(agentDir, serverKeys, ".claude-cron");
51271
+ }
51063
51272
 
51064
51273
  // src/repos/bare-clone.ts
51065
51274
  init_paths();
@@ -51524,20 +51733,33 @@ function buildCronSessionContext(agentConfig) {
51524
51733
  cronModelQ: shellSingleQuote(DEFAULT_CRON_MODEL)
51525
51734
  };
51526
51735
  }
51527
- function maybeWriteTrimmedCronMcp(agentDir, mcpServers, cronSessionEnabled) {
51736
+ function maybeWriteCronMcp(agentDir, mcpServers, cronSessionEnabled) {
51528
51737
  if (!cronSessionEnabled)
51529
51738
  return null;
51530
51739
  const telegram = mcpServers["switchroom-telegram"];
51531
51740
  if (!telegram)
51532
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
+ };
51533
51754
  const cronDir = join9(agentDir, ".claude-cron");
51534
51755
  mkdirSync10(cronDir, { recursive: true });
51535
51756
  const path = join9(cronDir, ".mcp.json");
51536
- const content = JSON.stringify({ mcpServers: { "switchroom-telegram": telegram } }, null, 2) + `
51757
+ const content = JSON.stringify({ mcpServers: cronServers }, null, 2) + `
51537
51758
  `;
51538
51759
  if (!existsSync14(path) || readFileSync12(path, "utf-8") !== content) {
51539
51760
  writeFileSync5(path, content, { encoding: "utf-8", mode: 384 });
51540
51761
  }
51762
+ seedCronConfigDir(agentDir, Object.keys(cronServers));
51541
51763
  return path;
51542
51764
  }
51543
51765
  function alignAgentUid(name, agentDir, uid, opts = {}) {
@@ -52730,7 +52952,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
52730
52952
  }
52731
52953
  writeIfChanged(mcpJsonPath, () => JSON.stringify({ mcpServers }, null, 2) + `
52732
52954
  `, created, skipped, 384);
52733
- maybeWriteTrimmedCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
52955
+ maybeWriteCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
52734
52956
  mcpServerKeysToTrust = Object.keys(mcpServers);
52735
52957
  ensureMcpServersTrusted(agentDir, mcpServerKeysToTrust);
52736
52958
  }
@@ -53805,9 +54027,9 @@ ${body}
53805
54027
  writeFileSync5(mcpJsonPath, after, { encoding: "utf-8", mode: 384 });
53806
54028
  changes.push(mcpJsonPath);
53807
54029
  }
53808
- const trimmedCronMcp = maybeWriteTrimmedCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
53809
- if (trimmedCronMcp)
53810
- changes.push(trimmedCronMcp);
54030
+ const cronMcp = maybeWriteCronMcp(agentDir, mcpServers, buildCronSessionContext(agentConfig).cronSessionEnabled);
54031
+ if (cronMcp)
54032
+ changes.push(cronMcp);
53811
54033
  ensureMcpServersTrusted(agentDir, Object.keys(mcpServers));
53812
54034
  }
53813
54035
  const reconcileWorkspaceDir = join9(agentDir, "workspace");
@@ -68374,17 +68596,19 @@ function collectScheduleEntries(config) {
68374
68596
  const schedule = resolved.schedule ?? [];
68375
68597
  for (let i = 0;i < schedule.length; i++) {
68376
68598
  const entry = schedule[i];
68599
+ const auditMaterial = entry.prompt ?? `action:${JSON.stringify(entry.action ?? {})}`;
68377
68600
  out.push({
68378
68601
  agent,
68379
68602
  scheduleIndex: i,
68380
68603
  cron: entry.cron,
68381
- prompt: entry.prompt,
68382
- 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),
68383
68606
  ...entry.topic !== undefined ? { topic: entry.topic } : {},
68384
68607
  ...entry.kind !== undefined ? { kind: entry.kind } : {},
68385
68608
  ...entry.model !== undefined ? { model: entry.model } : {},
68386
68609
  ...entry.context !== undefined ? { context: entry.context } : {},
68387
- ...entry.poll !== undefined ? { poll: entry.poll } : {}
68610
+ ...entry.poll !== undefined ? { poll: entry.poll } : {},
68611
+ ...entry.action !== undefined ? { action: entry.action } : {}
68388
68612
  });
68389
68613
  }
68390
68614
  }
@@ -76101,7 +76325,7 @@ init_loader();
76101
76325
  init_lifecycle();
76102
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";
76103
76327
  import { spawnSync as spawnSync9 } from "node:child_process";
76104
- 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";
76105
76329
  import { homedir as homedir36 } from "node:os";
76106
76330
  var DEFAULT_COMPOSE_PATH = join60(homedir36(), ".switchroom", "compose", "docker-compose.yml");
76107
76331
  function runningFromSwitchroomCheckout(scriptPath) {
@@ -76259,7 +76483,7 @@ function planUpdate(opts) {
76259
76483
  opts.syncBundledSkillsFn();
76260
76484
  return;
76261
76485
  }
76262
- const source = resolve33(import.meta.dirname, "../../skills");
76486
+ const source = resolve34(import.meta.dirname, "../../skills");
76263
76487
  const dest = join60(homedir36(), ".switchroom", "skills", "_bundled");
76264
76488
  if (!existsSync58(source)) {
76265
76489
  process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
@@ -76586,7 +76810,7 @@ init_source();
76586
76810
  init_helpers();
76587
76811
  init_loader();
76588
76812
  init_lifecycle();
76589
- import { resolve as resolve34 } from "node:path";
76813
+ import { resolve as resolve35 } from "node:path";
76590
76814
 
76591
76815
  // src/cli/version.ts
76592
76816
  init_source();
@@ -76741,7 +76965,7 @@ function registerRestartCommand(program3) {
76741
76965
  }
76742
76966
  const didRestart = res.restarted || !graceful;
76743
76967
  if (didRestart) {
76744
- const agentDir = resolve34(agentsDir, name);
76968
+ const agentDir = resolve35(agentsDir, name);
76745
76969
  const converged = waitForAuthConverge(name, agentDir);
76746
76970
  if (!converged) {
76747
76971
  console.log(source_default.yellow(` ${name}: agent is up but auth status didn't converge in 30s \u2014 check logs`));
@@ -76822,7 +77046,7 @@ Dependency manifest`));
76822
77046
  // src/cli/handoff.ts
76823
77047
  init_helpers();
76824
77048
  init_loader();
76825
- import { resolve as resolve35 } from "node:path";
77049
+ import { resolve as resolve36 } from "node:path";
76826
77050
  function registerHandoffCommand(program3) {
76827
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) => {
76828
77052
  let agentConfig;
@@ -76836,7 +77060,7 @@ function registerHandoffCommand(program3) {
76836
77060
  return;
76837
77061
  }
76838
77062
  const agentsDir = resolveAgentsDir(config);
76839
- agentDir = resolve35(agentsDir, agentName);
77063
+ agentDir = resolve36(agentsDir, agentName);
76840
77064
  } catch (err) {
76841
77065
  if (!(err instanceof ConfigError))
76842
77066
  throw err;
@@ -76851,7 +77075,7 @@ function registerHandoffCommand(program3) {
76851
77075
  `);
76852
77076
  return;
76853
77077
  }
76854
- const claudeConfigDir = resolve35(agentDir, ".claude");
77078
+ const claudeConfigDir = resolve36(agentDir, ".claude");
76855
77079
  const jsonl = findLatestSessionJsonl(claudeConfigDir);
76856
77080
  if (!jsonl) {
76857
77081
  process.stderr.write(`handoff: no session JSONL under ${claudeConfigDir}/projects; skipping
@@ -77360,7 +77584,7 @@ function record(stateDir, input, nowFn = Date.now) {
77360
77584
  return result;
77361
77585
  });
77362
77586
  }
77363
- function resolve36(stateDir, fingerprint, nowFn = Date.now) {
77587
+ function resolve37(stateDir, fingerprint, nowFn = Date.now) {
77364
77588
  if (!existsSync60(join62(stateDir, ISSUES_FILE)))
77365
77589
  return 0;
77366
77590
  return withLock(stateDir, () => {
@@ -77648,11 +77872,11 @@ function registerIssuesCommand(program3) {
77648
77872
  const stateDir = resolveStateDir2(opts);
77649
77873
  let flipped;
77650
77874
  if (opts.source && opts.code) {
77651
- flipped = resolve36(stateDir, computeFingerprint(opts.source, opts.code));
77875
+ flipped = resolve37(stateDir, computeFingerprint(opts.source, opts.code));
77652
77876
  } else if (opts.source && !fingerprint) {
77653
77877
  flipped = resolveAllBySource(stateDir, opts.source);
77654
77878
  } else if (fingerprint) {
77655
- flipped = resolve36(stateDir, fingerprint);
77879
+ flipped = resolve37(stateDir, fingerprint);
77656
77880
  } else {
77657
77881
  process.stderr.write(`issues resolve: need either <fingerprint>, --source, or --source + --code
77658
77882
  `);
@@ -77749,7 +77973,7 @@ function relTime(deltaMs) {
77749
77973
  init_source();
77750
77974
  import { existsSync as existsSync63 } from "node:fs";
77751
77975
  import { homedir as homedir39 } from "node:os";
77752
- import { join as join65, resolve as resolve37 } from "node:path";
77976
+ import { join as join65, resolve as resolve38 } from "node:path";
77753
77977
 
77754
77978
  // src/deps/python.ts
77755
77979
  import { createHash as createHash11 } from "node:crypto";
@@ -77956,7 +78180,7 @@ function ensureNodeEnv(opts) {
77956
78180
 
77957
78181
  // src/cli/deps.ts
77958
78182
  function builtinSkillsRoot() {
77959
- return resolve37(homedir39(), ".switchroom/skills/_bundled");
78183
+ return resolve38(homedir39(), ".switchroom/skills/_bundled");
77960
78184
  }
77961
78185
  function registerDepsCommand(program3) {
77962
78186
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
@@ -78035,7 +78259,7 @@ function registerDepsCommand(program3) {
78035
78259
  init_helpers();
78036
78260
  init_loader();
78037
78261
  import { existsSync as existsSync64 } from "node:fs";
78038
- import { resolve as resolve38, sep as sep3 } from "node:path";
78262
+ import { resolve as resolve39, sep as sep3 } from "node:path";
78039
78263
  import { spawnSync as spawnSync10 } from "node:child_process";
78040
78264
 
78041
78265
  // src/agents/workspace.ts
@@ -78741,8 +78965,8 @@ function registerWorkspaceCommand(program3) {
78741
78965
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
78742
78966
  if (!dir)
78743
78967
  return;
78744
- const resolvedWorkspace = resolve38(dir);
78745
- const target = resolve38(resolvedWorkspace, file ?? "AGENTS.md");
78968
+ const resolvedWorkspace = resolve39(dir);
78969
+ const target = resolve39(resolvedWorkspace, file ?? "AGENTS.md");
78746
78970
  if (!isInsideWorkspace(resolvedWorkspace, target)) {
78747
78971
  process.stderr.write(`workspace edit: refusing path traversal outside workspace dir (${target})
78748
78972
  `);
@@ -78810,7 +79034,7 @@ function registerWorkspaceCommand(program3) {
78810
79034
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
78811
79035
  if (!dir)
78812
79036
  return;
78813
- const gitDir = resolve38(dir, ".git");
79037
+ const gitDir = resolve39(dir, ".git");
78814
79038
  if (!existsSync64(gitDir)) {
78815
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.
78816
79040
  `);
@@ -78864,7 +79088,7 @@ function registerWorkspaceCommand(program3) {
78864
79088
  const dir = resolveAgentWorkspaceDirOrExit(program3, agentName);
78865
79089
  if (!dir)
78866
79090
  return;
78867
- const gitDir = resolve38(dir, ".git");
79091
+ const gitDir = resolve39(dir, ".git");
78868
79092
  if (!existsSync64(gitDir)) {
78869
79093
  process.stdout.write(`Workspace is not a git repository.
78870
79094
  `);
@@ -78888,7 +79112,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
78888
79112
  return;
78889
79113
  }
78890
79114
  const agentsDir = resolveAgentsDir(config);
78891
- const agentDir = resolve38(agentsDir, agentName);
79115
+ const agentDir = resolve39(agentsDir, agentName);
78892
79116
  const dir = resolveAgentWorkspaceDir(agentDir);
78893
79117
  if (!existsSync64(dir)) {
78894
79118
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
@@ -78927,7 +79151,7 @@ init_helpers();
78927
79151
  init_loader();
78928
79152
  init_merge();
78929
79153
  import { copyFileSync as copyFileSync10, existsSync as existsSync65, readFileSync as readFileSync57, writeFileSync as writeFileSync31 } from "node:fs";
78930
- import { join as join66, resolve as resolve39 } from "node:path";
79154
+ import { join as join66, resolve as resolve40 } from "node:path";
78931
79155
  init_schema();
78932
79156
  function resolveSoulTargetOrExit(program3, agentName) {
78933
79157
  const config = getConfig(program3);
@@ -78940,7 +79164,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
78940
79164
  const profileName = merged.extends ?? DEFAULT_PROFILE;
78941
79165
  const profilePath = getProfilePath(profileName);
78942
79166
  const agentsDir = resolveAgentsDir(config);
78943
- const agentDir = resolve39(agentsDir, agentName);
79167
+ const agentDir = resolve40(agentsDir, agentName);
78944
79168
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
78945
79169
  if (!existsSync65(workspaceDir)) {
78946
79170
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
@@ -79018,7 +79242,7 @@ function registerSoulCommand(program3) {
79018
79242
  init_helpers();
79019
79243
  init_loader();
79020
79244
  import { existsSync as existsSync66, readFileSync as readFileSync58, readdirSync as readdirSync23, statSync as statSync28 } from "node:fs";
79021
- import { resolve as resolve40, join as join67 } from "node:path";
79245
+ import { resolve as resolve41, join as join67 } from "node:path";
79022
79246
  import { createHash as createHash13 } from "node:crypto";
79023
79247
  init_merge();
79024
79248
  init_hindsight();
@@ -79116,7 +79340,7 @@ function registerDebugCommand(program3) {
79116
79340
  process.exit(1);
79117
79341
  }
79118
79342
  const agentsDir = resolveAgentsDir(config);
79119
- const agentDir = resolve40(agentsDir, agentName);
79343
+ const agentDir = resolve41(agentsDir, agentName);
79120
79344
  if (!existsSync66(agentDir)) {
79121
79345
  console.error(`Agent directory not found: ${agentDir}`);
79122
79346
  process.exit(1);
@@ -79293,7 +79517,7 @@ init_source();
79293
79517
  // src/worktree/claim.ts
79294
79518
  import { execFileSync as execFileSync21 } from "node:child_process";
79295
79519
  import { closeSync as closeSync12, mkdirSync as mkdirSync37, openSync as openSync12, existsSync as existsSync68, unlinkSync as unlinkSync13 } from "node:fs";
79296
- import { join as join69, resolve as resolve42 } from "node:path";
79520
+ import { join as join69, resolve as resolve43 } from "node:path";
79297
79521
  import { homedir as homedir41 } from "node:os";
79298
79522
  import { randomBytes as randomBytes13 } from "node:crypto";
79299
79523
 
@@ -79307,10 +79531,10 @@ import {
79307
79531
  existsSync as existsSync67,
79308
79532
  renameSync as renameSync13
79309
79533
  } from "node:fs";
79310
- import { join as join68, resolve as resolve41 } from "node:path";
79534
+ import { join as join68, resolve as resolve42 } from "node:path";
79311
79535
  import { homedir as homedir40 } from "node:os";
79312
79536
  function registryDir() {
79313
- return resolve41(process.env.SWITCHROOM_WORKTREE_DIR ?? join68(homedir40(), ".switchroom", "worktrees"));
79537
+ return resolve42(process.env.SWITCHROOM_WORKTREE_DIR ?? join68(homedir40(), ".switchroom", "worktrees"));
79314
79538
  }
79315
79539
  function recordPath(id) {
79316
79540
  return join68(registryDir(), `${id}.json`);
@@ -79391,7 +79615,7 @@ function acquireRepoLock(repoPath) {
79391
79615
  }
79392
79616
  var DEFAULT_CONCURRENCY = 5;
79393
79617
  function worktreesBaseDir() {
79394
- 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"));
79395
79619
  }
79396
79620
  function shortId() {
79397
79621
  return randomBytes13(4).toString("hex");
@@ -80111,20 +80335,20 @@ function wireStdio(child) {
80111
80335
  async function killChild(child, gracefulMs = 3000) {
80112
80336
  if (child.exitCode !== null || child.signalCode !== null)
80113
80337
  return;
80114
- return new Promise((resolve43) => {
80338
+ return new Promise((resolve44) => {
80115
80339
  let killTimer = null;
80116
80340
  const onExit = () => {
80117
80341
  if (killTimer) {
80118
80342
  clearTimeout(killTimer);
80119
80343
  killTimer = null;
80120
80344
  }
80121
- resolve43();
80345
+ resolve44();
80122
80346
  };
80123
80347
  child.once("exit", onExit);
80124
80348
  try {
80125
80349
  child.kill("SIGTERM");
80126
80350
  } catch {
80127
- resolve43();
80351
+ resolve44();
80128
80352
  return;
80129
80353
  }
80130
80354
  killTimer = setTimeout(() => {
@@ -80238,8 +80462,8 @@ async function runMs365McpLauncher(opts, rt) {
80238
80462
  };
80239
80463
  process.on("SIGINT", onSignal);
80240
80464
  process.on("SIGTERM", onSignal);
80241
- return new Promise((resolve43) => {
80242
- resolveLauncher = resolve43;
80465
+ return new Promise((resolve44) => {
80466
+ resolveLauncher = resolve44;
80243
80467
  });
80244
80468
  }
80245
80469
  function registerM365McpLauncherCommand(program3) {
@@ -80341,23 +80565,23 @@ async function runNotionMcpLauncher(opts, runtime) {
80341
80565
  };
80342
80566
  process.on("SIGTERM", () => forward("SIGTERM"));
80343
80567
  process.on("SIGINT", () => forward("SIGINT"));
80344
- const exitCode = await new Promise((resolve43) => {
80568
+ const exitCode = await new Promise((resolve44) => {
80345
80569
  child.once("exit", (code, signal) => {
80346
80570
  clearTimer(heartbeatHandle);
80347
80571
  if (typeof code === "number") {
80348
- resolve43(code);
80572
+ resolve44(code);
80349
80573
  } else if (signal) {
80350
80574
  const sigCode = { SIGTERM: 15, SIGINT: 2, SIGKILL: 9 }[signal] ?? 1;
80351
- resolve43(128 + sigCode);
80575
+ resolve44(128 + sigCode);
80352
80576
  } else {
80353
- resolve43(1);
80577
+ resolve44(1);
80354
80578
  }
80355
80579
  });
80356
80580
  child.once("error", (err) => {
80357
80581
  clearTimer(heartbeatHandle);
80358
80582
  process.stderr.write(`notion-mcp-launcher: child spawn error: ${err.message}
80359
80583
  `);
80360
- resolve43(1);
80584
+ resolve44(1);
80361
80585
  });
80362
80586
  });
80363
80587
  return exitCode;
@@ -81415,7 +81639,7 @@ agents:
81415
81639
 
81416
81640
  // src/cli/apply.ts
81417
81641
  init_resolver();
81418
- 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";
81419
81643
  import { homedir as homedir43 } from "node:os";
81420
81644
  import { execFileSync as execFileSync24 } from "node:child_process";
81421
81645
  init_vault();
@@ -82016,7 +82240,7 @@ function copyExampleConfig2(name) {
82016
82240
  if (!/^[a-z0-9_-]+$/.test(name)) {
82017
82241
  throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
82018
82242
  }
82019
- const dest = resolve44(process.cwd(), "switchroom.yaml");
82243
+ const dest = resolve45(process.cwd(), "switchroom.yaml");
82020
82244
  if (existsSync74(dest)) {
82021
82245
  console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
82022
82246
  return;
@@ -82027,7 +82251,7 @@ function copyExampleConfig2(name) {
82027
82251
  console.log(source_default.green(`Copied ${name}.yaml -> switchroom.yaml`));
82028
82252
  return;
82029
82253
  }
82030
- const exampleFile = resolve44(import.meta.dirname, `../../examples/${name}.yaml`);
82254
+ const exampleFile = resolve45(import.meta.dirname, `../../examples/${name}.yaml`);
82031
82255
  if (!existsSync74(exampleFile)) {
82032
82256
  throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
82033
82257
  }
@@ -82605,10 +82829,10 @@ import {
82605
82829
  unlinkSync as unlinkSync14,
82606
82830
  writeSync as writeSync8
82607
82831
  } from "node:fs";
82608
- import { join as join76, resolve as resolve45 } from "node:path";
82832
+ import { join as join76, resolve as resolve46 } from "node:path";
82609
82833
  var STAGING_SUBDIR = ".staging";
82610
82834
  function overlayPathsFor(agent, opts = {}) {
82611
- 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}`));
82612
82836
  const scheduleDir = join76(base, "schedule.d");
82613
82837
  const scheduleStagingDir = join76(scheduleDir, STAGING_SUBDIR);
82614
82838
  const skillsDir = join76(base, "skills.d");
@@ -83004,11 +83228,12 @@ import { existsSync as existsSync78, readFileSync as readFileSync66 } from "node
83004
83228
  import { execFileSync as execFileSync25 } from "node:child_process";
83005
83229
 
83006
83230
  // src/scheduler/schedule-report.ts
83007
- var TIER_WEIGHT = { poll: 0, cheap: 1, main: 5 };
83231
+ var TIER_WEIGHT = { poll: 0, action: 0, cheap: 1, main: 5 };
83008
83232
  function summarizeScheduleReport(rows, opts = {}) {
83009
83233
  const s = {
83010
83234
  total: 0,
83011
83235
  pollFires: 0,
83236
+ actionFires: 0,
83012
83237
  cheapFires: 0,
83013
83238
  mainFires: 0,
83014
83239
  errors: 0,
@@ -83023,6 +83248,8 @@ function summarizeScheduleReport(rows, opts = {}) {
83023
83248
  const tier = r.tier ?? "main";
83024
83249
  if (tier === "poll")
83025
83250
  s.pollFires += 1;
83251
+ else if (tier === "action")
83252
+ s.actionFires += 1;
83026
83253
  else if (tier === "cheap")
83027
83254
  s.cheapFires += 1;
83028
83255
  else
@@ -83054,11 +83281,12 @@ function parseScheduleJsonl(blob) {
83054
83281
  }
83055
83282
  function formatScheduleReport(agent, s) {
83056
83283
  const modelFires = s.cheapFires + s.mainFires;
83057
- 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;
83058
83285
  const lines = [
83059
83286
  `cron report \u2014 ${agent}`,
83060
83287
  ` total fires ${s.total}`,
83061
- ` 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}`,
83062
83290
  ` Tier 1 cheap ${s.cheapFires}`,
83063
83291
  ` Tier 2 main ${s.mainFires}`,
83064
83292
  ` model fires ${modelFires}`,
@@ -83860,7 +84088,7 @@ import {
83860
84088
  writeFileSync as writeFileSync39
83861
84089
  } from "node:fs";
83862
84090
  import { tmpdir as tmpdir5, homedir as homedir45 } from "node:os";
83863
- 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";
83864
84092
  import { spawnSync as spawnSync11 } from "node:child_process";
83865
84093
 
83866
84094
  // src/cli/skill-common.ts
@@ -84058,7 +84286,7 @@ function resolveSkillsPoolDir2(override) {
84058
84286
  }
84059
84287
  if (raw === "~")
84060
84288
  return homedir45();
84061
- return resolve46(raw);
84289
+ return resolve47(raw);
84062
84290
  }
84063
84291
  function readStdinSync() {
84064
84292
  const chunks = [];
@@ -84342,7 +84570,7 @@ function registerSkillCommand(program3) {
84342
84570
  if (opts.from === undefined) {
84343
84571
  files = loadFromStdin();
84344
84572
  } else {
84345
- const fromPath = resolve46(opts.from);
84573
+ const fromPath = resolve47(opts.from);
84346
84574
  if (!existsSync80(fromPath)) {
84347
84575
  fail3(`--from path does not exist: ${opts.from}`);
84348
84576
  }
@@ -84411,7 +84639,7 @@ import {
84411
84639
  utimesSync,
84412
84640
  writeFileSync as writeFileSync40
84413
84641
  } from "node:fs";
84414
- 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";
84415
84643
  import { homedir as homedir46, tmpdir as tmpdir6 } from "node:os";
84416
84644
  import { spawnSync as spawnSync12 } from "node:child_process";
84417
84645
  init_helpers();
@@ -84422,7 +84650,7 @@ var TRASH_TTL_MS = 24 * 60 * 60 * 1000;
84422
84650
  var PERSONAL_SKILLS_SUBPATH = "personal-skills";
84423
84651
  function resolveConfigSkillsDir(agent) {
84424
84652
  const override = process.env.SWITCHROOM_CONFIG_DIR;
84425
- const candidate = override ? resolve47(override) : join80(homedir46(), ".switchroom-config");
84653
+ const candidate = override ? resolve48(override) : join80(homedir46(), ".switchroom-config");
84426
84654
  if (!existsSync81(candidate))
84427
84655
  return null;
84428
84656
  return join80(candidate, "agents", agent, PERSONAL_SKILLS_SUBPATH);
@@ -84517,7 +84745,7 @@ function resolveAgent(opts) {
84517
84745
  }
84518
84746
  function resolveAgentsRoot(opts) {
84519
84747
  if (opts.root)
84520
- return resolve47(opts.root);
84748
+ return resolve48(opts.root);
84521
84749
  return join80(homedir46(), ".switchroom", "agents");
84522
84750
  }
84523
84751
  function personalSkillDir(agentsRoot, agent, name) {
@@ -84547,7 +84775,7 @@ function readStdinSync2() {
84547
84775
  return Buffer.concat(chunks).toString("utf-8");
84548
84776
  }
84549
84777
  function loadFromDir2(dir) {
84550
- const abs = resolve47(dir);
84778
+ const abs = resolve48(dir);
84551
84779
  if (!statSync32(abs).isDirectory()) {
84552
84780
  fail4(`--from path is not a directory: ${dir}`);
84553
84781
  }
@@ -84741,7 +84969,7 @@ function loadFiles(opts) {
84741
84969
  if (opts.from === undefined) {
84742
84970
  return loadFromStdin2();
84743
84971
  }
84744
- const p = resolve47(opts.from);
84972
+ const p = resolve48(opts.from);
84745
84973
  if (!existsSync81(p)) {
84746
84974
  fail4(`--from path does not exist: ${opts.from}`);
84747
84975
  }
@@ -85010,18 +85238,18 @@ init_helpers();
85010
85238
  var import_yaml23 = __toESM(require_dist(), 1);
85011
85239
  import { existsSync as existsSync82, readdirSync as readdirSync32, readFileSync as readFileSync69, statSync as statSync33 } from "node:fs";
85012
85240
  import { homedir as homedir47 } from "node:os";
85013
- import { join as join81, resolve as resolve48 } from "node:path";
85241
+ import { join as join81, resolve as resolve49 } from "node:path";
85014
85242
  var PERSONAL_PREFIX2 = "personal-";
85015
85243
  var BUNDLED_SUBDIR = "_bundled";
85016
85244
  var AGENT_NAME_RE3 = /^[a-z][a-z0-9_-]{0,62}$/;
85017
85245
  function defaultAgentsRoot() {
85018
- return resolve48(homedir47(), ".switchroom/agents");
85246
+ return resolve49(homedir47(), ".switchroom/agents");
85019
85247
  }
85020
85248
  function defaultSharedRoot2() {
85021
- return resolve48(homedir47(), ".switchroom/skills");
85249
+ return resolve49(homedir47(), ".switchroom/skills");
85022
85250
  }
85023
85251
  function defaultBundledRoot2() {
85024
- return resolve48(homedir47(), ".switchroom/skills/_bundled");
85252
+ return resolve49(homedir47(), ".switchroom/skills/_bundled");
85025
85253
  }
85026
85254
  function readSkillFrontmatter(skillDir) {
85027
85255
  const mdPath = join81(skillDir, "SKILL.md");