switchroom 0.13.50 → 0.13.52

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.
@@ -23253,6 +23253,9 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23253
23253
  if (existsSync12(`${hostHomeForChecks}/.switchroom/mcp-launchers`)) {
23254
23254
  lines.push(` - ${homePrefix}/.switchroom/mcp-launchers:${homePrefix}/.switchroom/mcp-launchers:ro`);
23255
23255
  }
23256
+ if (existsSync12(`${hostHomeForChecks}/.switchroom/fleet`)) {
23257
+ lines.push(` - ${homePrefix}/.switchroom/fleet:${homePrefix}/.switchroom/fleet:ro`);
23258
+ }
23256
23259
  if (existsSync12(`${hostHomeForChecks}/.switchroom/credentials/${a.name}`)) {
23257
23260
  lines.push(` - ${homePrefix}/.switchroom/credentials/${a.name}:${homePrefix}/.switchroom/credentials:ro`);
23258
23261
  }
@@ -23263,6 +23266,12 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23263
23266
  mkdirSync8(`${hostHomeForChecks}/.switchroom/agents/${a.name}/schedule.d`, { recursive: true });
23264
23267
  } catch {}
23265
23268
  lines.push(` - ${homePrefix}/.switchroom/audit/${a.name}:${homePrefix}/.switchroom/audit/${a.name}:rw`);
23269
+ if (existsSync12(`${hostHomeForChecks}/.switchroom-config`)) {
23270
+ try {
23271
+ mkdirSync8(`${hostHomeForChecks}/.switchroom-config/agents/${a.name}/personal-skills`, { recursive: true });
23272
+ } catch {}
23273
+ lines.push(` - ${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:${homePrefix}/.switchroom-config/agents/${a.name}/personal-skills:rw`);
23274
+ }
23266
23275
  if (bundledSkillsPoolDir && existsSync12(bundledSkillsPoolDir) && !bundledSkillsPoolDir.startsWith(`${hostHomeForChecks}/.switchroom/skills`)) {
23267
23276
  lines.push(` - ${bundledSkillsPoolDir}:${bundledSkillsPoolDir}:ro`);
23268
23277
  }
@@ -47536,7 +47545,7 @@ __export(exports_server2, {
47536
47545
  TOOLS: () => TOOLS2
47537
47546
  });
47538
47547
  import { randomBytes as randomBytes14 } from "node:crypto";
47539
- import { existsSync as existsSync78, readFileSync as readFileSync63 } from "node:fs";
47548
+ import { existsSync as existsSync78, readFileSync as readFileSync64 } from "node:fs";
47540
47549
  function selfSocketPath() {
47541
47550
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
47542
47551
  }
@@ -47714,7 +47723,7 @@ function getLastUpdateApplyStatus() {
47714
47723
  }
47715
47724
  let raw;
47716
47725
  try {
47717
- raw = readFileSync63(path8, "utf-8");
47726
+ raw = readFileSync64(path8, "utf-8");
47718
47727
  } catch (err2) {
47719
47728
  return errorText2(`get_status: failed to read audit log at ${path8}: ${err2.message}`);
47720
47729
  }
@@ -47947,8 +47956,8 @@ var {
47947
47956
  } = import__.default;
47948
47957
 
47949
47958
  // src/build-info.ts
47950
- var VERSION = "0.13.50";
47951
- var COMMIT_SHA = "99e127b1";
47959
+ var VERSION = "0.13.52";
47960
+ var COMMIT_SHA = "3d68efa2";
47952
47961
 
47953
47962
  // src/cli/agent.ts
47954
47963
  init_source();
@@ -48516,10 +48525,90 @@ Example response shapes:
48516
48525
  \`docker/Dockerfile.agent\` and rebuild the agent image."
48517
48526
  - "I tried to clone into \`/workspace\` \u2014 that path doesn't exist in my
48518
48527
  sandbox. Cloning into \`$HOME/workspace\` instead."`;
48528
+ var TELEGRAM_GUIDANCE = `## Talking to a human on Telegram
48529
+
48530
+ There is a real person on the other end. Every turn should feel like
48531
+ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
48532
+
48533
+ 1. **Acknowledge first.** Unless your whole reply is a single short
48534
+ sentence you can send right now, your FIRST action this turn is a
48535
+ brief \`reply\` in your own voice \u2014 "on it", "good question, one
48536
+ sec", "let me dig in" \u2014 before any tool call and before you
48537
+ compose the full answer. This holds even for a pure-thinking
48538
+ answer: if it will run to a paragraph, ack first. It is the line
48539
+ between a colleague and a black box.
48540
+ 2. **Then go quiet and work.** Heads-down is correct \u2014 do NOT narrate
48541
+ every tool call. A typing indicator runs automatically while you
48542
+ work; you do not maintain it.
48543
+ 3. **Surface meaningful progress** at genuine inflection points \u2014 a
48544
+ hard step finished, a blocker, a pivot, dispatching a sub-agent, a
48545
+ notably slow wait, a finding worth knowing now. One short \`reply\`,
48546
+ \`disable_notification: true\`.
48547
+ 4. **Hand back delegations with synthesis.** When a sub-agent / worker
48548
+ returns, re-enter in YOUR voice \u2014 what it found, and what you are
48549
+ doing next. Never let its raw report stand as your reply. A
48550
+ *background* worker finishes after your turn ends; its result
48551
+ arrives as a fresh \`<channel source="subagent_handback">\` turn \u2014
48552
+ treat that turn as the cue to do exactly this.
48553
+ 5. **Deliver the answer** as a final \`reply\`.
48554
+
48555
+ The one thing to avoid is *spam*: a reply on every tool call, on a
48556
+ timer, or repeating what you already said. Responsive and human, never
48557
+ a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
48558
+ acknowledging, or *instead* of an update at a real milestone, is the
48559
+ black box this exists to prevent.
48560
+
48561
+ Every turn that answers a user message ends with a user-visible
48562
+ \`reply\` (or \`stream_reply\` done=true) \u2014 Telegram is all the user
48563
+ sees; your terminal output never reaches them.`;
48564
+ var MEMORY_GUIDANCE = `## Memory \u2014 proactive, conversational
48565
+
48566
+ You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
48567
+
48568
+ ### Retain proactively
48569
+ When the user shares a fact, preference, decision, or plan worth keeping across sessions, call \`sync_retain\` in the same turn. Briefly acknowledge in your reply ("got it, April 2nd anniversary"). Don't narrate the tool call. Skip small talk and transient tool output, the auto-retain hook handles conversation-level signal.
48570
+
48571
+ ### Correct proactively
48572
+ When the user corrects you or contradicts a prior memory, call \`delete_memory\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
48573
+
48574
+ ### Forget proactively
48575
+ When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_memory\` for matching entries and confirm what was removed.
48576
+
48577
+ ### Inspect proactively
48578
+ When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
48579
+
48580
+ Don't wait for a slash command. Don't ask permission. Memory work is table stakes, like a colleague who takes notes and remembers.`;
48581
+ function renderFleetInvariants() {
48582
+ return [
48583
+ "<!--",
48584
+ " ~/.switchroom/fleet/switchroom-invariants.md",
48585
+ "",
48586
+ " Release-controlled fleet invariants. Every Switchroom agent reads",
48587
+ " this via Claude Code native CLAUDE.md discovery, since the agent's",
48588
+ " `claude` process boots with `--add-dir ~/.switchroom/fleet`.",
48589
+ "",
48590
+ " DO NOT EDIT. `switchroom apply` regenerates this file on every run",
48591
+ " and restores it if it drifts from the release canonical. To extend",
48592
+ " the agent's behaviour fleet-wide, edit ~/.switchroom/fleet/CLAUDE.md",
48593
+ " instead (operator-owned, additive, preserved across applies).",
48594
+ "-->",
48595
+ "",
48596
+ "# Switchroom fleet invariants",
48597
+ "",
48598
+ SANDBOX_GUIDANCE,
48599
+ "",
48600
+ TELEGRAM_GUIDANCE,
48601
+ "",
48602
+ MEMORY_GUIDANCE,
48603
+ ""
48604
+ ].join(`
48605
+ `);
48606
+ }
48519
48607
  function alignAgentUid(name, agentDir, uid, opts = {}) {
48520
48608
  const writeOut = opts.writeOut ?? ((s) => process.stdout.write(s));
48521
48609
  const logsDir = join8(homedir4(), ".switchroom", "logs", name);
48522
48610
  const auditDir = join8(homedir4(), ".switchroom", "audit", name);
48611
+ const configMirrorDir = join8(homedir4(), ".switchroom-config", "agents", name, "personal-skills");
48523
48612
  const paths = [];
48524
48613
  if (existsSync11(agentDir))
48525
48614
  paths.push(agentDir);
@@ -48527,6 +48616,8 @@ function alignAgentUid(name, agentDir, uid, opts = {}) {
48527
48616
  paths.push(logsDir);
48528
48617
  if (existsSync11(auditDir))
48529
48618
  paths.push(auditDir);
48619
+ if (existsSync11(configMirrorDir))
48620
+ paths.push(configMirrorDir);
48530
48621
  if (paths.length === 0)
48531
48622
  return { chowned: false, paths: [] };
48532
48623
  const priors = [];
@@ -49288,70 +49379,7 @@ function buildWorkspaceContext(args) {
49288
49379
  return out;
49289
49380
  })(),
49290
49381
  systemPromptAppendShellQuoted: (() => {
49291
- const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(agentConfig);
49292
49382
  const baseAppend = agentConfig.system_prompt_append ?? "";
49293
- const telegramGuidance = `## Talking to a human on Telegram
49294
-
49295
- There is a real person on the other end. Every turn should feel like
49296
- messaging a capable colleague \u2014 not a tool emitting output. Five beats:
49297
-
49298
- 1. **Acknowledge first.** Unless your whole reply is a single short
49299
- sentence you can send right now, your FIRST action this turn is a
49300
- brief \`reply\` in your own voice \u2014 "on it", "good question, one
49301
- sec", "let me dig in" \u2014 before any tool call and before you
49302
- compose the full answer. This holds even for a pure-thinking
49303
- answer: if it will run to a paragraph, ack first. It is the line
49304
- between a colleague and a black box.
49305
- 2. **Then go quiet and work.** Heads-down is correct \u2014 do NOT narrate
49306
- every tool call. A typing indicator runs automatically while you
49307
- work; you do not maintain it.
49308
- 3. **Surface meaningful progress** at genuine inflection points \u2014 a
49309
- hard step finished, a blocker, a pivot, dispatching a sub-agent, a
49310
- notably slow wait, a finding worth knowing now. One short \`reply\`,
49311
- \`disable_notification: true\`.
49312
- 4. **Hand back delegations with synthesis.** When a sub-agent / worker
49313
- returns, re-enter in YOUR voice \u2014 what it found, and what you are
49314
- doing next. Never let its raw report stand as your reply. A
49315
- *background* worker finishes after your turn ends; its result
49316
- arrives as a fresh \`<channel source="subagent_handback">\` turn \u2014
49317
- treat that turn as the cue to do exactly this.
49318
- 5. **Deliver the answer** as a final \`reply\`.
49319
-
49320
- The one thing to avoid is *spam*: a reply on every tool call, on a
49321
- timer, or repeating what you already said. Responsive and human, never
49322
- a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
49323
- acknowledging, or *instead* of an update at a real milestone, is the
49324
- black box this exists to prevent.
49325
-
49326
- Every turn that answers a user message ends with a user-visible
49327
- \`reply\` (or \`stream_reply\` done=true) \u2014 Telegram is all the user
49328
- sees; your terminal output never reaches them.`;
49329
- const memoryGuidance = `## Memory \u2014 proactive, conversational
49330
-
49331
- You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
49332
-
49333
- ### Retain proactively
49334
- When the user shares a fact, preference, decision, or plan worth keeping across sessions, call \`sync_retain\` in the same turn. Briefly acknowledge in your reply ("got it, April 2nd anniversary"). Don't narrate the tool call. Skip small talk and transient tool output, the auto-retain hook handles conversation-level signal.
49335
-
49336
- ### Correct proactively
49337
- When the user corrects you or contradicts a prior memory, call \`delete_memory\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
49338
-
49339
- ### Forget proactively
49340
- When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_memory\` for matching entries and confirm what was removed.
49341
-
49342
- ### Inspect proactively
49343
- When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
49344
-
49345
- Don't wait for a slash command. Don't ask permission. Memory work is table stakes, like a colleague who takes notes and remembers.`;
49346
- if (useSwitchroomPlugin) {
49347
- const parts = [baseAppend, telegramGuidance, memoryGuidance, SANDBOX_GUIDANCE].filter((s) => s.length > 0);
49348
- const combined = parts.join(`
49349
-
49350
- ---
49351
-
49352
- `);
49353
- return shellSingleQuote(combined);
49354
- }
49355
49383
  return baseAppend.length > 0 ? shellSingleQuote(baseAppend) : undefined;
49356
49384
  })(),
49357
49385
  extraCliArgs: (() => {
@@ -50252,70 +50280,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
50252
50280
  })(),
50253
50281
  model: agentConfig.model,
50254
50282
  systemPromptAppendShellQuoted: (() => {
50255
- const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(agentConfig);
50256
50283
  const baseAppend = agentConfig.system_prompt_append ?? "";
50257
- const telegramGuidance = `## Talking to a human on Telegram
50258
-
50259
- There is a real person on the other end. Every turn should feel like
50260
- messaging a capable colleague \u2014 not a tool emitting output. Five beats:
50261
-
50262
- 1. **Acknowledge first.** Unless your whole reply is a single short
50263
- sentence you can send right now, your FIRST action this turn is a
50264
- brief \`reply\` in your own voice \u2014 "on it", "good question, one
50265
- sec", "let me dig in" \u2014 before any tool call and before you
50266
- compose the full answer. This holds even for a pure-thinking
50267
- answer: if it will run to a paragraph, ack first. It is the line
50268
- between a colleague and a black box.
50269
- 2. **Then go quiet and work.** Heads-down is correct \u2014 do NOT narrate
50270
- every tool call. A typing indicator runs automatically while you
50271
- work; you do not maintain it.
50272
- 3. **Surface meaningful progress** at genuine inflection points \u2014 a
50273
- hard step finished, a blocker, a pivot, dispatching a sub-agent, a
50274
- notably slow wait, a finding worth knowing now. One short \`reply\`,
50275
- \`disable_notification: true\`.
50276
- 4. **Hand back delegations with synthesis.** When a sub-agent / worker
50277
- returns, re-enter in YOUR voice \u2014 what it found, and what you are
50278
- doing next. Never let its raw report stand as your reply. A
50279
- *background* worker finishes after your turn ends; its result
50280
- arrives as a fresh \`<channel source="subagent_handback">\` turn \u2014
50281
- treat that turn as the cue to do exactly this.
50282
- 5. **Deliver the answer** as a final \`reply\`.
50283
-
50284
- The one thing to avoid is *spam*: a reply on every tool call, on a
50285
- timer, or repeating what you already said. Responsive and human, never
50286
- a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
50287
- acknowledging, or *instead* of an update at a real milestone, is the
50288
- black box this exists to prevent.
50289
-
50290
- Every turn that answers a user message ends with a user-visible
50291
- \`reply\` (or \`stream_reply\` done=true) \u2014 Telegram is all the user
50292
- sees; your terminal output never reaches them.`;
50293
- const memoryGuidance = `## Memory \u2014 proactive, conversational
50294
-
50295
- You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
50296
-
50297
- ### Retain proactively
50298
- When the user shares a fact, preference, decision, or plan worth keeping across sessions, call \`sync_retain\` in the same turn. Briefly acknowledge in your reply ("got it, April 2nd anniversary"). Don't narrate the tool call. Skip small talk and transient tool output, the auto-retain hook handles conversation-level signal.
50299
-
50300
- ### Correct proactively
50301
- When the user corrects you or contradicts a prior memory, call \`delete_memory\` on the wrong entry, then \`sync_retain\` the correction. Acknowledge the correction in one line ("noted, Alice not Bob").
50302
-
50303
- ### Forget proactively
50304
- When the user asks you to forget something ("forget that", "delete X", "drop what I said about Y"), call \`delete_memory\` for matching entries and confirm what was removed.
50305
-
50306
- ### Inspect proactively
50307
- When the user asks "what do you know about X / me", "what do you remember about Y", or any memory audit, use \`reflect\` to synthesize an answer across the bank. Return it as honest prose, not a raw dump. If the bank has little on the topic, say so.
50308
-
50309
- Don't wait for a slash command. Don't ask permission. Memory work is table stakes, like a colleague who takes notes and remembers.`;
50310
- if (useSwitchroomPlugin) {
50311
- const parts = [baseAppend, telegramGuidance, memoryGuidance, SANDBOX_GUIDANCE].filter((s) => s.length > 0);
50312
- const combined = parts.join(`
50313
-
50314
- ---
50315
-
50316
- `);
50317
- return shellSingleQuote(combined);
50318
- }
50319
50284
  return baseAppend.length > 0 ? shellSingleQuote(baseAppend) : undefined;
50320
50285
  })(),
50321
50286
  extraCliArgs: (() => {
@@ -74048,7 +74013,7 @@ function registerDriveMcpLauncherCommand(program3) {
74048
74013
 
74049
74014
  // src/cli/apply.ts
74050
74015
  init_source();
74051
- import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync68, mkdirSync as mkdirSync36, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
74016
+ import { accessSync as accessSync3, chownSync as chownSync4, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync68, mkdirSync as mkdirSync36, readFileSync as readFileSync55, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
74052
74017
  import { mkdir, writeFile } from "node:fs/promises";
74053
74018
  import { spawnSync as childSpawnSync } from "node:child_process";
74054
74019
  import readline from "node:readline";
@@ -74762,6 +74727,9 @@ async function ensureHostMountSources(config) {
74762
74727
  dirs.push(join62(home2, ".switchroom", "logs", name));
74763
74728
  dirs.push(join62(home2, ".claude", "projects", name));
74764
74729
  dirs.push(join62(home2, ".switchroom", "audit", name));
74730
+ if (existsSync68(join62(home2, ".switchroom-config"))) {
74731
+ dirs.push(join62(home2, ".switchroom-config", "agents", name, "personal-skills"));
74732
+ }
74765
74733
  }
74766
74734
  for (const dir of dirs) {
74767
74735
  await mkdir(dir, { recursive: true });
@@ -74792,6 +74760,29 @@ async function ensureHostMountSources(config) {
74792
74760
  chownSync4(tokenPath, uid, uid);
74793
74761
  } catch {}
74794
74762
  }
74763
+ const fleetDir = join62(home2, ".switchroom", "fleet");
74764
+ await mkdir(fleetDir, { recursive: true });
74765
+ const invariantsPath = join62(fleetDir, "switchroom-invariants.md");
74766
+ const invariantsCanonical = renderFleetInvariants();
74767
+ const invariantsCurrent = existsSync68(invariantsPath) ? readFileSync55(invariantsPath, "utf-8") : null;
74768
+ if (invariantsCurrent !== invariantsCanonical) {
74769
+ writeFileSync32(invariantsPath, invariantsCanonical, { mode: 420 });
74770
+ }
74771
+ const fleetClaudePath = join62(fleetDir, "CLAUDE.md");
74772
+ if (!existsSync68(fleetClaudePath)) {
74773
+ writeFileSync32(fleetClaudePath, [
74774
+ "# Switchroom fleet defaults",
74775
+ "",
74776
+ "Operator-owned fleet brain. Every agent reads this via",
74777
+ "`--add-dir ~/.switchroom/fleet` (Claude Code native CLAUDE.md",
74778
+ "discovery). Additions stack across the fleet; `switchroom apply`",
74779
+ "never clobbers your edits here.",
74780
+ "",
74781
+ "<!-- L2 fleet defaults content lands in switchroom #1855 -->",
74782
+ ""
74783
+ ].join(`
74784
+ `), { mode: 420 });
74785
+ }
74795
74786
  }
74796
74787
  function detectComposeV2() {
74797
74788
  try {
@@ -75310,7 +75301,7 @@ function runRedactStdin() {
75310
75301
  }
75311
75302
 
75312
75303
  // src/cli/status-ask.ts
75313
- import { readFileSync as readFileSync55, existsSync as existsSync69, readdirSync as readdirSync26 } from "node:fs";
75304
+ import { readFileSync as readFileSync56, existsSync as existsSync69, readdirSync as readdirSync26 } from "node:fs";
75314
75305
  import { join as join63 } from "node:path";
75315
75306
  import { homedir as homedir36 } from "node:os";
75316
75307
 
@@ -75586,7 +75577,7 @@ function runReport(opts) {
75586
75577
  for (const src of sources) {
75587
75578
  let content;
75588
75579
  try {
75589
- content = readFileSync55(src.path, "utf-8");
75580
+ content = readFileSync56(src.path, "utf-8");
75590
75581
  } catch (err) {
75591
75582
  process.stderr.write(`status-ask report: cannot read ${src.path}: ${err instanceof Error ? err.message : String(err)}
75592
75583
  `);
@@ -75687,7 +75678,7 @@ import {
75687
75678
  existsSync as existsSync70,
75688
75679
  mkdirSync as mkdirSync37,
75689
75680
  appendFileSync as appendFileSync4,
75690
- readFileSync as readFileSync56
75681
+ readFileSync as readFileSync57
75691
75682
  } from "node:fs";
75692
75683
  var AUDIT_ROOT = join64(homedir37(), ".switchroom", "audit");
75693
75684
  function auditPathFor(agent) {
@@ -75782,7 +75773,7 @@ function readAuditTail(agent, limit, opts = {}) {
75782
75773
  return [];
75783
75774
  let raw;
75784
75775
  try {
75785
- raw = readFileSync56(path8, "utf-8");
75776
+ raw = readFileSync57(path8, "utf-8");
75786
75777
  } catch {
75787
75778
  return [];
75788
75779
  }
@@ -75943,7 +75934,7 @@ import {
75943
75934
  mkdirSync as mkdirSync38,
75944
75935
  openSync as openSync13,
75945
75936
  readdirSync as readdirSync27,
75946
- readFileSync as readFileSync57,
75937
+ readFileSync as readFileSync58,
75947
75938
  renameSync as renameSync14,
75948
75939
  statSync as statSync28,
75949
75940
  unlinkSync as unlinkSync14,
@@ -76067,7 +76058,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
76067
76058
  continue;
76068
76059
  const full = join65(paths.skillsDir, name);
76069
76060
  try {
76070
- const raw = readFileSync57(full, "utf-8");
76061
+ const raw = readFileSync58(full, "utf-8");
76071
76062
  const slug = name.replace(/\.ya?ml$/i, "");
76072
76063
  out.push({ slug, path: full, raw });
76073
76064
  } catch {}
@@ -76094,7 +76085,7 @@ function listOverlayEntries(agent, opts = {}) {
76094
76085
  continue;
76095
76086
  const full = join65(paths.scheduleDir, name);
76096
76087
  try {
76097
- const raw = readFileSync57(full, "utf-8");
76088
+ const raw = readFileSync58(full, "utf-8");
76098
76089
  const slug = name.replace(/\.ya?ml$/i, "");
76099
76090
  out.push({ slug, path: full, raw });
76100
76091
  } catch {}
@@ -76242,7 +76233,7 @@ import {
76242
76233
  mkdirSync as mkdirSync39,
76243
76234
  openSync as openSync14,
76244
76235
  readdirSync as readdirSync28,
76245
- readFileSync as readFileSync58,
76236
+ readFileSync as readFileSync59,
76246
76237
  renameSync as renameSync15,
76247
76238
  unlinkSync as unlinkSync15,
76248
76239
  writeFileSync as writeFileSync33,
@@ -76306,7 +76297,7 @@ function listPendingScheduleEntries(agent, opts = {}) {
76306
76297
  if (!existsSync72(yamlPath))
76307
76298
  continue;
76308
76299
  try {
76309
- const meta = JSON.parse(readFileSync58(metaPath, "utf-8"));
76300
+ const meta = JSON.parse(readFileSync59(metaPath, "utf-8"));
76310
76301
  if (meta?.v !== 1 || typeof meta.stage_id !== "string")
76311
76302
  continue;
76312
76303
  out.push({ stageId: meta.stage_id, agent: meta.agent, yamlPath, metaPath, meta });
@@ -76345,7 +76336,7 @@ function denyPendingScheduleEntry(opts) {
76345
76336
 
76346
76337
  // src/cli/agent-config-write.ts
76347
76338
  init_protocol3();
76348
- import { existsSync as existsSync73, readFileSync as readFileSync59 } from "node:fs";
76339
+ import { existsSync as existsSync73, readFileSync as readFileSync60 } from "node:fs";
76349
76340
  var MAX_ENTRIES_PER_AGENT = 20;
76350
76341
  var MIN_CRON_INTERVAL_MIN = 5;
76351
76342
  function extractCronSmallestGapMin(expr) {
@@ -76632,7 +76623,7 @@ function scheduleRemove(opts) {
76632
76623
  let priorContent = null;
76633
76624
  try {
76634
76625
  if (existsSync73(match.path))
76635
- priorContent = readFileSync59(match.path, "utf-8");
76626
+ priorContent = readFileSync60(match.path, "utf-8");
76636
76627
  } catch {}
76637
76628
  deleteOverlayEntry(agent, match.slug, { root: opts.root });
76638
76629
  const reconcileFn = opts.reconcile === undefined ? opts.root ? null : reconcileAgentCronOnly : opts.reconcile;
@@ -77072,7 +77063,7 @@ import {
77072
77063
  mkdirSync as mkdirSync40,
77073
77064
  mkdtempSync as mkdtempSync5,
77074
77065
  openSync as openSync15,
77075
- readFileSync as readFileSync60,
77066
+ readFileSync as readFileSync61,
77076
77067
  readdirSync as readdirSync29,
77077
77068
  realpathSync as realpathSync7,
77078
77069
  renameSync as renameSync16,
@@ -77324,7 +77315,7 @@ function loadFromDir(dir) {
77324
77315
  continue;
77325
77316
  }
77326
77317
  if (ent.isFile()) {
77327
- const buf = readFileSync60(full);
77318
+ const buf = readFileSync61(full);
77328
77319
  files[rel.replace(/\\/g, "/")] = buf.toString("utf-8");
77329
77320
  }
77330
77321
  }
@@ -77373,7 +77364,7 @@ function loadFromTarball(tarPath) {
77373
77364
  }
77374
77365
  }
77375
77366
  function loadSingleFile(filePath) {
77376
- const content = readFileSync60(filePath, "utf-8");
77367
+ const content = readFileSync61(filePath, "utf-8");
77377
77368
  return { "SKILL.md": content };
77378
77369
  }
77379
77370
  function loadFromStdin() {
@@ -77464,7 +77455,7 @@ function diffSummary(currentDir, files) {
77464
77455
  if (ent.isDirectory()) {
77465
77456
  walk2(full);
77466
77457
  } else if (ent.isFile()) {
77467
- currentFiles[rel.replace(/\\/g, "/")] = readFileSync60(full, "utf-8");
77458
+ currentFiles[rel.replace(/\\/g, "/")] = readFileSync61(full, "utf-8");
77468
77459
  }
77469
77460
  }
77470
77461
  };
@@ -77624,7 +77615,7 @@ import {
77624
77615
  mkdirSync as mkdirSync41,
77625
77616
  mkdtempSync as mkdtempSync6,
77626
77617
  openSync as openSync16,
77627
- readFileSync as readFileSync61,
77618
+ readFileSync as readFileSync62,
77628
77619
  readdirSync as readdirSync30,
77629
77620
  renameSync as renameSync17,
77630
77621
  rmSync as rmSync17,
@@ -77706,7 +77697,7 @@ function mirrorToConfigRepo(agent, name, liveSkillDir) {
77706
77697
  if (ent.isDirectory())
77707
77698
  walk2(s, d);
77708
77699
  else if (ent.isFile()) {
77709
- writeFileSync35(d, readFileSync61(s));
77700
+ writeFileSync35(d, readFileSync62(s));
77710
77701
  }
77711
77702
  }
77712
77703
  };
@@ -77785,7 +77776,7 @@ function loadFromDir2(dir) {
77785
77776
  }
77786
77777
  if (ent.isFile()) {
77787
77778
  const rel = relative3(abs, full).replace(/\\/g, "/");
77788
- files[rel] = readFileSync61(full, "utf-8");
77779
+ files[rel] = readFileSync62(full, "utf-8");
77789
77780
  }
77790
77781
  }
77791
77782
  };
@@ -77971,7 +77962,7 @@ function loadFiles(opts) {
77971
77962
  return loadFromDir2(p);
77972
77963
  }
77973
77964
  if (p.endsWith(".md")) {
77974
- return { "SKILL.md": readFileSync61(p, "utf-8") };
77965
+ return { "SKILL.md": readFileSync62(p, "utf-8") };
77975
77966
  }
77976
77967
  fail3(`--from must be a directory or a .md file. Got: ${opts.from}`);
77977
77968
  }
@@ -78037,6 +78028,7 @@ function resolveCloneSource(source, opts) {
78037
78028
  var CLONE_MAX_FILE_BYTES = 1024 * 1024;
78038
78029
  function readSourceFiles(dir) {
78039
78030
  const files = {};
78031
+ const skipped = [];
78040
78032
  const walk2 = (sub) => {
78041
78033
  for (const ent of readdirSync30(sub, { withFileTypes: true })) {
78042
78034
  const full = join69(sub, ent.name);
@@ -78049,18 +78041,22 @@ function readSourceFiles(dir) {
78049
78041
  }
78050
78042
  if (ent.isFile()) {
78051
78043
  const rel = relative3(dir, full).replace(/\\/g, "/");
78044
+ if (!validateRelPath(rel)) {
78045
+ skipped.push(rel);
78046
+ continue;
78047
+ }
78052
78048
  try {
78053
78049
  const st = lstatSync9(full);
78054
78050
  if (st.size > CLONE_MAX_FILE_BYTES) {
78055
78051
  fail3(`clone source has oversized file ${rel} (${st.size} bytes > ${CLONE_MAX_FILE_BYTES}); ` + `refuse to read`, 3);
78056
78052
  }
78057
78053
  } catch {}
78058
- files[rel] = readFileSync61(full, "utf-8");
78054
+ files[rel] = readFileSync62(full, "utf-8");
78059
78055
  }
78060
78056
  }
78061
78057
  };
78062
78058
  walk2(dir);
78063
- return files;
78059
+ return { files, skipped };
78064
78060
  }
78065
78061
  function rewriteSkillMdName(content, newName) {
78066
78062
  if (!content.startsWith(`---
@@ -78088,13 +78084,17 @@ function clonePersonalAction(source, opts) {
78088
78084
  if (!SKILL_SLUG_RE.test(newName)) {
78089
78085
  fail3(`destination name must match ${SKILL_SLUG_RE.source}: got ${JSON.stringify(newName)}`);
78090
78086
  }
78091
- const files = readSourceFiles(src.dir);
78087
+ const { files, skipped } = readSourceFiles(src.dir);
78092
78088
  if (!files["SKILL.md"]) {
78093
78089
  fail3(`source ${JSON.stringify(source)} has no SKILL.md at ${src.dir}`);
78094
78090
  }
78095
78091
  if (newName !== src.slug) {
78096
78092
  files["SKILL.md"] = rewriteSkillMdName(files["SKILL.md"], newName);
78097
78093
  }
78094
+ if (skipped.length > 0) {
78095
+ process.stderr.write(source_default.yellow(`note: skipped ${skipped.length} non-allowlisted path${skipped.length === 1 ? "" : "s"} from source: ${skipped.join(", ")}
78096
+ `));
78097
+ }
78098
78098
  loadValidateWrite(agentsRoot, agent, newName, files, true);
78099
78099
  const skillDir = personalSkillDir(agentsRoot, agent, newName);
78100
78100
  mirrorToConfigRepo(agent, newName, skillDir);
@@ -78107,14 +78107,16 @@ function clonePersonalAction(source, opts) {
78107
78107
  source_slug: src.slug,
78108
78108
  name: newName,
78109
78109
  path: skillDir,
78110
- files: Object.keys(files).length
78110
+ files: Object.keys(files).length,
78111
+ skipped
78111
78112
  }));
78112
78113
  appendAudit(agent, "skill.clone_to_personal", {
78113
78114
  source,
78114
78115
  source_tier: src.tier,
78115
78116
  source_slug: src.slug,
78116
78117
  name: newName,
78117
- files: Object.keys(files).length
78118
+ files: Object.keys(files).length,
78119
+ skipped_count: skipped.length
78118
78120
  }, 0);
78119
78121
  }
78120
78122
  function removePersonalAction(name, opts) {
@@ -78218,7 +78220,7 @@ function registerSkillPersonalCommands(program3) {
78218
78220
  // src/cli/skill-search.ts
78219
78221
  init_helpers();
78220
78222
  var import_yaml18 = __toESM(require_dist(), 1);
78221
- import { existsSync as existsSync77, readdirSync as readdirSync31, readFileSync as readFileSync62, statSync as statSync31 } from "node:fs";
78223
+ import { existsSync as existsSync77, readdirSync as readdirSync31, readFileSync as readFileSync63, statSync as statSync31 } from "node:fs";
78222
78224
  import { homedir as homedir40 } from "node:os";
78223
78225
  import { join as join70, resolve as resolve46 } from "node:path";
78224
78226
  var PERSONAL_PREFIX2 = "personal-";
@@ -78239,7 +78241,7 @@ function readSkillFrontmatter(skillDir) {
78239
78241
  return null;
78240
78242
  let content;
78241
78243
  try {
78242
- content = readFileSync62(mdPath, "utf-8");
78244
+ content = readFileSync63(mdPath, "utf-8");
78243
78245
  } catch {
78244
78246
  return null;
78245
78247
  }
@@ -78511,7 +78513,7 @@ function registerHostdMcpCommand(program3) {
78511
78513
  // src/cli/hostd.ts
78512
78514
  init_source();
78513
78515
  init_helpers();
78514
- import { existsSync as existsSync79, mkdirSync as mkdirSync42, readdirSync as readdirSync32, readFileSync as readFileSync64, writeFileSync as writeFileSync36, statSync as statSync32, copyFileSync as copyFileSync12 } from "node:fs";
78516
+ import { existsSync as existsSync79, mkdirSync as mkdirSync42, readdirSync as readdirSync32, readFileSync as readFileSync65, writeFileSync as writeFileSync36, statSync as statSync32, copyFileSync as copyFileSync12 } from "node:fs";
78515
78517
  import { homedir as homedir41 } from "node:os";
78516
78518
  import { join as join71 } from "node:path";
78517
78519
  import { spawnSync as spawnSync13 } from "node:child_process";
@@ -78764,7 +78766,7 @@ function registerHostdCommand(program3) {
78764
78766
  The log is created when hostd handles its first privileged-verb request.`));
78765
78767
  return;
78766
78768
  }
78767
- const raw = readFileSync64(logPath, "utf-8");
78769
+ const raw = readFileSync65(logPath, "utf-8");
78768
78770
  const limit = Math.max(1, parseInt(opts.tail ?? "50", 10) || 50);
78769
78771
  const filters = {
78770
78772
  agent: opts.agent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.50",
3
+ "version": "0.13.52",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -159,6 +159,19 @@ fi
159
159
  if [ ! -e "$HOME/.switchroom" ] || [ -L "$HOME/.switchroom" ]; then
160
160
  ln -sfn {{{hostHomeQ}}}/.switchroom "$HOME/.switchroom" 2>/dev/null || true
161
161
  fi
162
+ # Host ~/.switchroom-config symlink (#1846). Same shape as the
163
+ # ~/.switchroom link above, for the operator's git-tracked config repo.
164
+ # The compose generator bind-mounts a per-agent slice at
165
+ # <host-home>/.switchroom-config/agents/<self>/personal-skills/ (when
166
+ # the operator has opted in). Without this symlink, the CLI's
167
+ # resolveConfigSkillsDir would call existsSync("$HOME/.switchroom-config")
168
+ # and miss the bind-mounted path entirely → mirror silently no-ops.
169
+ # The link lands ONLY when the host slice is actually mounted (the dir
170
+ # the bind-mount targets) — leaving a dangling link when the operator
171
+ # hasn't opted in is fine, existsSync resolves false through it.
172
+ if [ ! -e "$HOME/.switchroom-config" ] || [ -L "$HOME/.switchroom-config" ]; then
173
+ ln -sfn {{{hostHomeQ}}}/.switchroom-config "$HOME/.switchroom-config" 2>/dev/null || true
174
+ fi
162
175
  {{/if}}
163
176
 
164
177
  export NVM_DIR="$HOME/.nvm"
@@ -555,16 +568,29 @@ for f in \
555
568
  fi
556
569
  done
557
570
 
571
+ # Fleet directory — epic #1850 / issue #1852. Extends Claude Code's
572
+ # native CLAUDE.md auto-discovery to `~/.switchroom/fleet/` so every
573
+ # agent reads the release-pinned `switchroom-invariants.md` (lane 1)
574
+ # and operator-owned `CLAUDE.md` (lane 2) from there. Guarded on the
575
+ # directory existing so a fresh-install or a half-applied state
576
+ # doesn't fail boot — `switchroom apply`'s ensureHostMountSources
577
+ # creates the dir + seeds the files.
578
+ SR_FLEET_DIR="$HOME/.switchroom/fleet"
579
+ SR_FLEET_ARG=""
580
+ if [ -d "$SR_FLEET_DIR" ]; then
581
+ SR_FLEET_ARG="--add-dir $SR_FLEET_DIR"
582
+ fi
583
+
558
584
  {{#if useSwitchroomPlugin}}
559
585
  if [ -n "$APPEND_PROMPT" ]; then
560
- exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
586
+ exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
561
587
  else
562
- exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
588
+ exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
563
589
  fi
564
590
  {{else}}
565
591
  if [ -n "$APPEND_PROMPT" ]; then
566
- exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
592
+ exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
567
593
  else
568
- exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
594
+ exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
569
595
  fi
570
596
  {{/if}}
@@ -22,6 +22,9 @@ tools is to let you do the edit yourself.
22
22
  | "what other agents are running here?" / "is there an agent that does X?" / "who handles Y?" | `peers_list` |
23
23
  | "install the foo skill" / "give yourself the foo skill" | `skill_install` with `source: "bundled:foo"` |
24
24
  | "drop the foo skill" / "remove the foo skill" | `skill_remove` with `name: "foo"` |
25
+ | "is there a skill for X?" | `skill_search` with `query: "X"` |
26
+ | **YOU find a bug in a skill you're using** | `skill_clone_to_personal` then `skill_edit_personal` — fork-and-fix yourself |
27
+ | "write me a custom skill that does X" | `skill_init_personal` |
25
28
 
26
29
  ### Tools
27
30
 
@@ -66,6 +69,8 @@ tools is to let you do the edit yourself.
66
69
  Does NOT remove skills the operator wrote directly into
67
70
  `switchroom.yaml` — those are removed by the operator only.
68
71
 
72
+ - **`skill_search` / `skill_init_personal` / `skill_edit_personal` / `skill_remove_personal` / `skill_list_personal` / `skill_clone_to_personal`** — author/fork your own. `files` is `{path: content}` JSON; allowlist `SKILL.md`, `scripts/*.{sh,py}`, `references/*.md`.
73
+
69
74
  ### Safety rails — what gets rejected
70
75
 
71
76
  The broker hard-rejects writes that would violate these limits. Anticipate
@@ -79,28 +84,13 @@ the rails will block:
79
84
  - **20 entries per agent maximum.** `E_QUOTA_EXCEEDED`. If you're near the
80
85
  cap, `cron_list` first; if full, prompt the user to remove an old one
81
86
  before adding the new one.
82
- - **No `secrets:` on agent-authored entries.** `E_OVERLAY_SECRETS_REQUIRES_APPROVAL`.
83
- If the user's task needs a credential (e.g. "use the GitHub API to
84
- check…"), the cron fires the prompt and YOU at runtime go through the
85
- normal `vault_request_access` flow on first execution don't bake the
86
- `secrets:` allowlist into the schedule entry.
87
- - **Cross-agent writes.** You can only manage your OWN schedule. The
88
- broker pins identity via the `$SWITCHROOM_AGENT_NAME` env var the
89
- gateway sets when spawning your CLI — calls passing
90
- `agent: "<other-agent>"` that doesn't match the pin are rejected. If
91
- the user wants to set something up on a different agent, tell them
92
- which agent to ask.
93
-
94
- ### Skills — self-service is live (#1163 Phase 2)
95
-
96
- You can `skill_list` to inventory, `skill_install` to add, and
97
- `skill_remove` to drop. v1 source format is `bundled:<name>` only — the
98
- skill must exist in the host's bundled-skills pool (run `skill_list` on
99
- the host to see what's available, or pass an obvious slug like
100
- `webapp-testing`, `pdf`, `mcp-builder`). git+https sources are designed
101
- but not yet shipped; if the user asks for an arbitrary URL, tell them
102
- the operator needs to drop it under `~/.switchroom/skills/<name>/` and
103
- run `switchroom apply`.
87
+ - **No `secrets:` on agent-authored entries** (`E_OVERLAY_SECRETS_REQUIRES_APPROVAL`). Cron fires the prompt; you go through `vault_request_access` at runtime.
88
+ - **Cross-agent writes rejected** — you manage only your own schedule. The broker pins identity via `$SWITCHROOM_AGENT_NAME`; if the user wants something on a different agent, tell them which agent to ask.
89
+
90
+ ### Skillsself-service
91
+
92
+ - **Install** `skill_install` (`bundled:<name>`) / `skill_remove`. git+https deferred.
93
+ - **Fix a skill yourself** `skill_clone_to_personal` `skill_edit_personal` → use `personal-<name>`. Fork is durable + auto-mirrors to `~/.switchroom-config/` when present.
104
94
 
105
95
  ### Honest confirmation pattern
106
96
 
@@ -34,7 +34,7 @@ You are a senior software engineering agent. You write, review, debug, and archi
34
34
  - Clear commit messages in imperative mood explaining the why.
35
35
  - Atomic commits. PR descriptions explain what, why, and how to test.
36
36
 
37
- {{> telegram-style}}
37
+ {{!-- telegram-style now in ~/.switchroom/fleet/switchroom-invariants.md --}}
38
38
 
39
39
  ## Memory — Hindsight
40
40
 
@@ -0,0 +1,122 @@
1
+ # Agent:
2
+
3
+ ## What you are
4
+
5
+ You are a **switchroom agent** — an instance of **Claude Code** (Anthropic's official `claude` CLI, unmodified) running in a Linux container, managed by switchroom. Your `$SWITCHROOM_AGENT_NAME` is ``. Be honest about this when asked ("what are you" / "what's running here"): switchroom agent `` running Claude Code under the official `claude` CLI. Not a custom model, not a wrapper, not "an AI assistant" in the abstract.
6
+
7
+ You are one of several agents here. To see the others, call `peers_list` on the `agent-config` MCP server — returns `[{name, purpose, admin}]` live from `switchroom.yaml`. **Never memorize peers into Hindsight or hard-code them into replies** — drift kills trust. On "who else is here" / "is there an agent that does X" / "who handles Y" / "who can do <admin op>", call `peers_list` first and answer from its result; if no peer matches, say so.
8
+
9
+ ## Who you are
10
+
11
+ See `SOUL.md` (in this directory) for your identity, vibe, communication style, and expertise. That file is your persona source of truth.
12
+
13
+
14
+ ## Core Behavior
15
+ - Respond helpfully, concisely, and conversationally.
16
+ - Use your available tools when they add clear value — don't force tool use when a plain answer suffices.
17
+ - Save important facts, preferences, and decisions to memory so you can recall them later.
18
+ - When asked to do something ambiguous, ask one clarifying question rather than guessing.
19
+ - If a task has multiple steps, outline your plan before executing.
20
+
21
+ ## Safety
22
+ - Don't exfiltrate private data. Ever.
23
+ - Don't run destructive commands without asking.
24
+ - Prefer `trash` over `rm` when available (recoverable beats gone forever).
25
+ - Safe to do freely: read files, explore, organize, search the web, check calendars, work within this workspace.
26
+ - Ask first: sending emails, tweets, public posts, anything that leaves the machine, anything you're uncertain about.
27
+
28
+ ## Execution Bias
29
+
30
+ How you should decide what to do next. These are procedural rules, not vibe.
31
+
32
+ - **Act in-turn.** If the request is actionable, do it this turn. Don't finish with a plan or promise when tools can move it forward.
33
+ - **Verify mutable facts before claiming them.** Files, git state, clocks, versions, services, processes, package state, the contents of an `Edit` target: read live. Memory and prior context are not verification sources. "I think the function is at line 200" is not an answer; `Grep`/`Read` is.
34
+ - **Final answer needs evidence.** Test/build/lint output, screenshot, inspection, tool output, or a named blocker. "It should work" is not a finalization.
35
+ - **Weak or empty tool result is not a conclusion.** Vary the query, path, command, or source before deciding the thing isn't there.
36
+ - **Non-final turn:** use tools to advance, or ask the one clarifying question that unblocks safe progress. One question, not five.
37
+
38
+
39
+ ## Memory — Hindsight is your single backend
40
+
41
+ **Claude Code's built-in file-based auto-memory is disabled for this agent.** Don't try to write `.md` files under `.claude/projects/.../memory/` or maintain a `MEMORY.md` index — that whole system is off. There's exactly one memory backend: **Hindsight**.
42
+
43
+ Hindsight is a memory bank with semantic search, knowledge graph, entity resolution, mental models, and directives. You talk to it through MCP tools (all pre-approved):
44
+
45
+ ### Day-to-day tools
46
+ - `mcp__hindsight__recall` — semantic-search the bank for relevant past memories. Auto-fires on every inbound user message via the plugin's UserPromptSubmit hook (you'll see "Relevant memories from past conversations" in your context). Call manually when you need a more specific query than the auto-fired one.
47
+ - `mcp__hindsight__retain` — store a new memory. The plugin automatically retains the conversation transcript every ~10 turns via the Stop hook, so you usually don't need this. Call manually for significant decisions, corrections, or facts you want immediately searchable.
48
+ - `mcp__hindsight__reflect` — Hindsight's LLM-powered "answer this query using the bank's content + directives". Use when the user asks a question that requires synthesis across multiple past memories.
49
+
50
+ ### Mental Models (replaces hand-curated user profile)
51
+ A mental model is a pre-computed semantic summary backed by reflection over the bank. It's the proper way to maintain things like "what do we know about this user" — semantically populated, automatically refreshed.
52
+
53
+ - `mcp__hindsight__create_mental_model(name, source_query)` — create one. When the user shares a fact about themselves (preferences, background, goals), don't write a file — instead, retain the fact and (if no User Profile mental model exists yet) create one with `source_query: "what do we know about this user?"`. Hindsight will populate it from the retained memories.
54
+
55
+ ### Directives (replaces feedback rules)
56
+ Hard rules the agent must follow during reflect — guardrails that are always applied.
57
+
58
+ - `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file.
59
+
60
+ (Inspection tools like `list_memories`, `list_mental_models`, `update_mental_model`, `refresh_mental_model`, `list_directives`, `delete_directive` are available under the `mcp__hindsight__*` namespace if you ever need them, but you rarely should — Hindsight's own auto-recall surfaces what matters and the operator handles bank curation out-of-band.)
61
+
62
+ ### What to retain — and what NOT to retain
63
+
64
+ Retain proactively when:
65
+ - The user shares a preference or fact about themselves
66
+ - The user gives you a correction or rule (these go to directives, not retain)
67
+ - A significant decision was made and the rationale matters for next time
68
+ - You did real work and the result + the path you took would be useful next session
69
+
70
+ Don't retain:
71
+ - Routine pleasantries, "thanks", "got it"
72
+ - Conversation chatter that doesn't carry forward
73
+ - Sensitive content the user explicitly asked you to not remember
74
+ - Things already in a mental model — they'll be re-derived from underlying memories
75
+
76
+ The plugin's auto-retain (Stop hook) handles transcript-level storage on a 10-turn cadence, so you don't need to manually retain everything. Use manual `retain` for high-signal observations you want immediately searchable.
77
+
78
+ ## Sub-Agent Delegation
79
+
80
+ The main session is for conversation. Execution belongs in sub-agents. Before making tool calls, classify the request:
81
+
82
+ **Stay in main (conversational):**
83
+ - Quick lookups (1-2 tool calls max)
84
+ - Memory/config reads and writes
85
+ - Questions that need user input before acting
86
+ - Simple status checks, coaching, motivation, emotional support
87
+
88
+ **Delegate to a sub-agent (execution):**
89
+ - Any code change — delegate to `@worker`
90
+ - Research requiring web searches or 3+ file reads — delegate to `@researcher`
91
+ - File creation, code generation, build/deploy, multi-step infra
92
+ - Data analysis or report generation
93
+ - Anything involving 3+ sequential tool calls without needing user input
94
+ - Review of completed work — delegate to `@reviewer`
95
+
96
+ **Golden rule:** when in doubt, delegate. Unnecessary delegation costs slightly more tokens. A blocked session costs the user's attention. Keep your own turns short — dispatch and acknowledge. The user should never wait more than 10 seconds for a response from you.
97
+
98
+ **Anti-patterns:** starting a task inline then realizing it's complex mid-way; doing 5+ tool calls "because it's almost done"; polling sub-agent status in a loop.
99
+
100
+ If no sub-agents are configured, do the work yourself.
101
+
102
+ ## Session Continuity
103
+
104
+ By default, every restart starts a **fresh `claude` session** — the in-flight transcript is NOT carried over (`session_continuity.resume_mode: handoff`, the default since switchroom #362). Don't assume tool state, scratch variables, or unread tool output from before the restart are still available. What does survive:
105
+
106
+ - **Handoff briefing** — on a clean shutdown, the Stop hook writes a bounded raw transcript tail of the prior session to `.handoff.md`. On boot, start.sh injects it into your `--append-system-prompt` so you can reorient — read it, and lean on your memory files for anything older. If `.handoff.md` is missing or stale (fresh agent, or pre-Stop-hook crash), `start.sh` runs `handoff-briefing.sh` to assemble `.handoff-briefing.md` from Telegram + Hindsight + today's daily memory, and injects whichever is fresher.
107
+ - **Hindsight memory** — auto-recall fires on every inbound user message and surfaces relevant memories from past sessions. Long-term facts, decisions, and mental models live here, not in the transcript.
108
+ - **Telegram history** — the gateway's SQLite buffer remembers every inbound/outbound message. Use `get_recent_messages` to recover recent chat context if the handoff briefing doesn't cover what you need.
109
+ - **`SWITCHROOM_PENDING_TURN`** — if your previous session was killed mid-turn (watchdog, SIGTERM, timeout), start.sh exports this env var plus the chat/thread/last-user-message context. Acknowledge the interruption and ask for direction rather than silently resuming.
110
+ - **`.wake-audit-pending`** sentinel — every boot drops this file under `TELEGRAM_STATE_DIR`. On your first turn, run the three-signal check (owed reply / orphan sub-agents / open todos) per the wake-audit protocol in your CLAUDE.md, then `rm -f` the sentinel.
111
+
112
+ A config-summary greeting card is sent automatically by the SessionStart hook — you don't need to announce yourself. If your context feels thin (after compaction or any fresh session), proactively recall from Hindsight before proceeding.
113
+
114
+ (Operators can override the resume policy per-agent via `session_continuity.resume_mode` in switchroom.yaml — `auto`, `continue`, `handoff`, or `none`. The default is `handoff`.)
115
+
116
+ ## Admin operations
117
+
118
+ You're NOT `admin: true`. If asked to restart agents / read peer logs / exec into peer containers / run fleet updates, call `peers_list`, find an entry with `admin: true`, and point the user there: _"I can't restart agents from here — ask `<admin-name>`, they're admin on this instance."_ No long apology; just hand off.
119
+
120
+ ## Tools
121
+ Use your available tools when appropriate. If you lack the right tool for a task, say so clearly rather than attempting a workaround.
122
+
@@ -39,7 +39,14 @@ How you should decide what to do next. These are procedural rules, not vibe.
39
39
  - **Weak or empty tool result is not a conclusion.** Vary the query, path, command, or source before deciding the thing isn't there.
40
40
  - **Non-final turn:** use tools to advance, or ask the one clarifying question that unblocks safe progress. One question, not five.
41
41
 
42
- {{> telegram-style}}
42
+ {{!--
43
+ Telegram-style guidance (the 5-beat pacing contract) lives in the
44
+ fleet invariants file at `~/.switchroom/fleet/switchroom-invariants.md`
45
+ and reaches the agent via Claude Code native CLAUDE.md discovery
46
+ (`--add-dir ~/.switchroom/fleet`). Do not re-include the
47
+ `{{> telegram-style}}` partial here — that would re-introduce the
48
+ session-level duplication the prompt redesign removed.
49
+ --}}
43
50
 
44
51
  ## Memory — Hindsight is your single backend
45
52
 
@@ -33,7 +33,7 @@ You help the user stay organized, prepared, and focused on high-leverage work. Y
33
33
  - **Anticipate, don't just react.** Flag gaps proactively.
34
34
  - **Be concise.** Lead with essentials. Details on request.
35
35
 
36
- {{> telegram-style}}
36
+ {{!-- telegram-style now in ~/.switchroom/fleet/switchroom-invariants.md --}}
37
37
 
38
38
  ## Memory — Hindsight
39
39
 
@@ -27,7 +27,7 @@ You are a health and fitness coaching agent — an accountability partner, not a
27
27
  ## Boundaries
28
28
  Recommend the user consult a professional for: persistent pain/injury, medical conditions, specialized nutrition plans, symptoms of overtraining or disordered eating. Say it plainly: "That's outside my lane — worth checking with your doctor."
29
29
 
30
- {{> telegram-style}}
30
+ {{!-- telegram-style now in ~/.switchroom/fleet/switchroom-invariants.md --}}
31
31
 
32
32
  ## Memory — Hindsight
33
33
 
@@ -48732,10 +48732,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
48732
48732
  }
48733
48733
 
48734
48734
  // ../src/build-info.ts
48735
- var VERSION = "0.13.50";
48736
- var COMMIT_SHA = "99e127b1";
48737
- var COMMIT_DATE = "2026-05-25T22:55:41Z";
48738
- var LATEST_PR = 1845;
48735
+ var VERSION = "0.13.52";
48736
+ var COMMIT_SHA = "3d68efa2";
48737
+ var COMMIT_DATE = "2026-05-26T00:48:06Z";
48738
+ var LATEST_PR = 1864;
48739
48739
  var COMMITS_AHEAD_OF_TAG = 0;
48740
48740
 
48741
48741
  // gateway/boot-version.ts