switchroom 0.21.11 → 0.21.13
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.
- package/dist/agent-scheduler/index.js +23 -0
- package/dist/auth-broker/index.js +24 -1
- package/dist/cli/notion-write-pretool.mjs +23 -0
- package/dist/cli/switchroom.js +3221 -1755
- package/dist/host-control/main.js +25 -2
- package/dist/vault/approvals/kernel-server.js +24 -1
- package/dist/vault/broker/server.js +24 -1
- package/package.json +3 -1
- package/skills/dev-protocol/SKILL.md +14 -10
- package/skills/switchroom-architecture/SKILL.md +11 -7
- package/skills/switchroom-architecture/cascade.md +29 -10
- package/skills/switchroom-architecture/sub-agents.md +27 -26
- package/skills/switchroom-architecture/telegram.md +44 -17
- package/skills/switchroom-release/SKILL.md +11 -5
- package/skills/switchroom-status/SKILL.md +17 -18
- package/skills/switchroom-status/scripts/status.sh +8 -17
- package/skills/telegram-test-harness/SKILL.md +6 -5
- package/telegram-plugin/dist/gateway/gateway.js +133 -5
- package/telegram-plugin/gateway/gateway.ts +2 -0
- package/telegram-plugin/shared/utf8-sanitize.ts +269 -0
- package/telegram-plugin/tests/utf8-sanitize-wire.test.ts +588 -0
|
@@ -10994,7 +10994,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
10994
10994
|
});
|
|
10995
10995
|
|
|
10996
10996
|
// src/config/schema.ts
|
|
10997
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
10997
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, ScratchConfigSchema, DiskConfigSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
10998
10998
|
var init_schema = __esm(() => {
|
|
10999
10999
|
init_zod();
|
|
11000
11000
|
init_observation_scopes();
|
|
@@ -11867,6 +11867,27 @@ var init_schema = __esm(() => {
|
|
|
11867
11867
|
notify_on_detect: exports_external.boolean().default(false).describe("KEN-129 — operator-in-the-loop update prompt. Only consulted " + "when apply_on_detect is false (auto-apply supersedes notify): " + "a newly detected release posts ONE operator approval card " + "('fleet is behind — tap to apply') via an admin agent's " + "gateway; Approve runs hostd's update_apply path (fleet-" + "mutation-locked, durable status rows, get_status-pollable). " + "Dedup on release id: the last-notified id persists in " + "~/.switchroom/release-notify-state.json, so a card that " + "reached the operator is never re-posted for the same release."),
|
|
11868
11868
|
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
11869
11869
|
});
|
|
11870
|
+
ScratchConfigSchema = exports_external.object({
|
|
11871
|
+
enabled: exports_external.boolean().default(true).describe("Whether agents get a scratch mount at /scratch with their package " + "caches redirected there. Default: true — but the feature only " + "engages when `volume` actually exists on the host, so a single-disk " + "machine is unaffected. Set false to opt out even where it does."),
|
|
11872
|
+
volume: exports_external.string().refine((v) => v.startsWith("/"), {
|
|
11873
|
+
message: "scratch.volume must be an absolute host path"
|
|
11874
|
+
}).default("/mnt/bulkdata").describe("Absolute host path of the bulk device's mountpoint. MUST already " + "exist — its presence is the single probe that turns the feature on, " + "so a typo degrades to the pre-existing behaviour rather than " + "quietly relocating caches somewhere that is still the root disk. " + "Default: /mnt/bulkdata."),
|
|
11875
|
+
subdir: exports_external.string().refine((v) => !v.startsWith("/") && !v.split("/").includes(".."), {
|
|
11876
|
+
message: "scratch.subdir must be a relative path without '..' segments"
|
|
11877
|
+
}).default("switchroom/scratch").describe("Relative path under `volume` holding the per-agent scratch " + "directories (`<volume>/<subdir>/<agent>`). Default: " + "switchroom/scratch.")
|
|
11878
|
+
});
|
|
11879
|
+
DiskConfigSchema = exports_external.object({
|
|
11880
|
+
warn_pct: exports_external.number().int().min(1).max(99).default(80).describe("Used-percentage at or above which `switchroom doctor` WARNs about " + "the filesystem holding the agents directory. Default 80 — the " + "reference fleet was at 85% when the condition was found by hand."),
|
|
11881
|
+
fail_pct: exports_external.number().int().min(2).max(100).default(90).describe("Used-percentage at or above which `switchroom doctor` FAILs. Must " + "be greater than `warn_pct`. Default 90."),
|
|
11882
|
+
reap_report: exports_external.object({
|
|
11883
|
+
enabled: exports_external.boolean().default(true).describe("Whether doctor checks that the report-only worktree sweep " + "(`switchroom worktree reap-report --append <file>`) is actually " + "running. Set false on a host that deliberately does not run it."),
|
|
11884
|
+
log: exports_external.string().min(1).default("/var/log/switchroom/reap-report.jsonl").describe("Path of the JSONL evidence log the scheduled `worktree " + "reap-report --append` writes. Doctor reads the newest record's " + "`generatedAt` — it detects the sweep by its OUTPUT, so it is " + "agnostic about whether an operator crontab, /etc/cron.d, or a " + "systemd timer drives it. Default " + "/var/log/switchroom/reap-report.jsonl (the path documented in " + "docs/operators/worktree-gc.md)."),
|
|
11885
|
+
max_age_hours: exports_external.number().int().min(1).max(720).default(48).describe("How old the newest record in `log` may be before doctor WARNs " + "that the sweep has stopped running. Default 48 — twice the " + "documented daily cadence, so a single missed run is not noise.")
|
|
11886
|
+
}).default({}).describe("Liveness check for the report-only worktree sweep. REPORT-ONLY by " + "construction: doctor reads an evidence log and never invokes any " + "reclaim path.")
|
|
11887
|
+
}).refine((v) => v.fail_pct > v.warn_pct, {
|
|
11888
|
+
message: "disk.fail_pct must be greater than disk.warn_pct",
|
|
11889
|
+
path: ["fail_pct"]
|
|
11890
|
+
});
|
|
11870
11891
|
HostControlConfigSchema = exports_external.object({
|
|
11871
11892
|
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
11872
11893
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
@@ -11941,6 +11962,8 @@ var init_schema = __esm(() => {
|
|
|
11941
11962
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
11942
11963
|
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
11943
11964
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
11965
|
+
scratch: ScratchConfigSchema.optional().describe("Per-agent scratch volume. Relocates every agent's build/package " + "caches (uv, npm, bun, playwright, puppeteer, pip user-site) off the " + "root disk onto a bulk device, bind-mounted at /scratch inside each " + "container. Framework-injected for EVERY agent — not routed through " + "the admin-only `bind_mounts:` escalation, because the biggest cache " + "consumers are ordinary non-admin agents. Omit the block to accept " + "defaults; the feature is a no-op unless `scratch.volume` exists on " + "the host."),
|
|
11966
|
+
disk: DiskConfigSchema.default({}).describe("Root-disk headroom thresholds for `switchroom doctor`. Measured " + "against the filesystem holding the agents directory (and the scratch " + "volume when that feature is engaged), not against `/`. Omit the block " + "to accept the defaults (WARN at 80% used, FAIL at 90%)."),
|
|
11944
11967
|
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
11945
11968
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
11946
11969
|
fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health — job-spec-anchored, operator-facing issue tracker (RFC " + "fleet-health.md, serves fleet-stays-healthy). Assigns the owner agent " + "that runs the nightly model-free sensor + weekly deep-dive. Default " + "unset owner_agent → inert; the admin page renders an empty state."),
|
|
@@ -21565,7 +21588,7 @@ function allocateAgentUid(name) {
|
|
|
21565
21588
|
}
|
|
21566
21589
|
|
|
21567
21590
|
// src/build-info.ts
|
|
21568
|
-
var VERSION = "0.21.
|
|
21591
|
+
var VERSION = "0.21.13";
|
|
21569
21592
|
|
|
21570
21593
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21571
21594
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
@@ -4434,7 +4434,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
4434
4434
|
});
|
|
4435
4435
|
|
|
4436
4436
|
// src/config/schema.ts
|
|
4437
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
4437
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, ScratchConfigSchema, DiskConfigSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
4438
4438
|
var init_schema = __esm(() => {
|
|
4439
4439
|
init_zod();
|
|
4440
4440
|
init_observation_scopes();
|
|
@@ -5307,6 +5307,27 @@ var init_schema = __esm(() => {
|
|
|
5307
5307
|
notify_on_detect: exports_external.boolean().default(false).describe("KEN-129 — operator-in-the-loop update prompt. Only consulted " + "when apply_on_detect is false (auto-apply supersedes notify): " + "a newly detected release posts ONE operator approval card " + "('fleet is behind — tap to apply') via an admin agent's " + "gateway; Approve runs hostd's update_apply path (fleet-" + "mutation-locked, durable status rows, get_status-pollable). " + "Dedup on release id: the last-notified id persists in " + "~/.switchroom/release-notify-state.json, so a card that " + "reached the operator is never re-posted for the same release."),
|
|
5308
5308
|
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
5309
5309
|
});
|
|
5310
|
+
ScratchConfigSchema = exports_external.object({
|
|
5311
|
+
enabled: exports_external.boolean().default(true).describe("Whether agents get a scratch mount at /scratch with their package " + "caches redirected there. Default: true — but the feature only " + "engages when `volume` actually exists on the host, so a single-disk " + "machine is unaffected. Set false to opt out even where it does."),
|
|
5312
|
+
volume: exports_external.string().refine((v) => v.startsWith("/"), {
|
|
5313
|
+
message: "scratch.volume must be an absolute host path"
|
|
5314
|
+
}).default("/mnt/bulkdata").describe("Absolute host path of the bulk device's mountpoint. MUST already " + "exist — its presence is the single probe that turns the feature on, " + "so a typo degrades to the pre-existing behaviour rather than " + "quietly relocating caches somewhere that is still the root disk. " + "Default: /mnt/bulkdata."),
|
|
5315
|
+
subdir: exports_external.string().refine((v) => !v.startsWith("/") && !v.split("/").includes(".."), {
|
|
5316
|
+
message: "scratch.subdir must be a relative path without '..' segments"
|
|
5317
|
+
}).default("switchroom/scratch").describe("Relative path under `volume` holding the per-agent scratch " + "directories (`<volume>/<subdir>/<agent>`). Default: " + "switchroom/scratch.")
|
|
5318
|
+
});
|
|
5319
|
+
DiskConfigSchema = exports_external.object({
|
|
5320
|
+
warn_pct: exports_external.number().int().min(1).max(99).default(80).describe("Used-percentage at or above which `switchroom doctor` WARNs about " + "the filesystem holding the agents directory. Default 80 — the " + "reference fleet was at 85% when the condition was found by hand."),
|
|
5321
|
+
fail_pct: exports_external.number().int().min(2).max(100).default(90).describe("Used-percentage at or above which `switchroom doctor` FAILs. Must " + "be greater than `warn_pct`. Default 90."),
|
|
5322
|
+
reap_report: exports_external.object({
|
|
5323
|
+
enabled: exports_external.boolean().default(true).describe("Whether doctor checks that the report-only worktree sweep " + "(`switchroom worktree reap-report --append <file>`) is actually " + "running. Set false on a host that deliberately does not run it."),
|
|
5324
|
+
log: exports_external.string().min(1).default("/var/log/switchroom/reap-report.jsonl").describe("Path of the JSONL evidence log the scheduled `worktree " + "reap-report --append` writes. Doctor reads the newest record's " + "`generatedAt` — it detects the sweep by its OUTPUT, so it is " + "agnostic about whether an operator crontab, /etc/cron.d, or a " + "systemd timer drives it. Default " + "/var/log/switchroom/reap-report.jsonl (the path documented in " + "docs/operators/worktree-gc.md)."),
|
|
5325
|
+
max_age_hours: exports_external.number().int().min(1).max(720).default(48).describe("How old the newest record in `log` may be before doctor WARNs " + "that the sweep has stopped running. Default 48 — twice the " + "documented daily cadence, so a single missed run is not noise.")
|
|
5326
|
+
}).default({}).describe("Liveness check for the report-only worktree sweep. REPORT-ONLY by " + "construction: doctor reads an evidence log and never invokes any " + "reclaim path.")
|
|
5327
|
+
}).refine((v) => v.fail_pct > v.warn_pct, {
|
|
5328
|
+
message: "disk.fail_pct must be greater than disk.warn_pct",
|
|
5329
|
+
path: ["fail_pct"]
|
|
5330
|
+
});
|
|
5310
5331
|
HostControlConfigSchema = exports_external.object({
|
|
5311
5332
|
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
5312
5333
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
@@ -5381,6 +5402,8 @@ var init_schema = __esm(() => {
|
|
|
5381
5402
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
5382
5403
|
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
5383
5404
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
5405
|
+
scratch: ScratchConfigSchema.optional().describe("Per-agent scratch volume. Relocates every agent's build/package " + "caches (uv, npm, bun, playwright, puppeteer, pip user-site) off the " + "root disk onto a bulk device, bind-mounted at /scratch inside each " + "container. Framework-injected for EVERY agent — not routed through " + "the admin-only `bind_mounts:` escalation, because the biggest cache " + "consumers are ordinary non-admin agents. Omit the block to accept " + "defaults; the feature is a no-op unless `scratch.volume` exists on " + "the host."),
|
|
5406
|
+
disk: DiskConfigSchema.default({}).describe("Root-disk headroom thresholds for `switchroom doctor`. Measured " + "against the filesystem holding the agents directory (and the scratch " + "volume when that feature is engaged), not against `/`. Omit the block " + "to accept the defaults (WARN at 80% used, FAIL at 90%)."),
|
|
5384
5407
|
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
5385
5408
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
5386
5409
|
fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health — job-spec-anchored, operator-facing issue tracker (RFC " + "fleet-health.md, serves fleet-stays-healthy). Assigns the owner agent " + "that runs the nightly model-free sensor + weekly deep-dive. Default " + "unset owner_agent → inert; the admin page renders an empty state."),
|
|
@@ -4030,7 +4030,7 @@ var init_observation_scopes = __esm(() => {
|
|
|
4030
4030
|
});
|
|
4031
4031
|
|
|
4032
4032
|
// src/config/schema.ts
|
|
4033
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
4033
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AntiConfabulationDirectiveSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, BuzzChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleServiceTokenSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, ScratchConfigSchema, DiskConfigSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, ConfigRepoConfigSchema, SwitchroomConfigSchema;
|
|
4034
4034
|
var init_schema = __esm(() => {
|
|
4035
4035
|
init_zod();
|
|
4036
4036
|
init_observation_scopes();
|
|
@@ -4903,6 +4903,27 @@ var init_schema = __esm(() => {
|
|
|
4903
4903
|
notify_on_detect: exports_external.boolean().default(false).describe("KEN-129 — operator-in-the-loop update prompt. Only consulted " + "when apply_on_detect is false (auto-apply supersedes notify): " + "a newly detected release posts ONE operator approval card " + "('fleet is behind — tap to apply') via an admin agent's " + "gateway; Approve runs hostd's update_apply path (fleet-" + "mutation-locked, durable status rows, get_status-pollable). " + "Dedup on release id: the last-notified id persists in " + "~/.switchroom/release-notify-state.json, so a card that " + "reached the operator is never re-posted for the same release."),
|
|
4904
4904
|
image_ref: exports_external.string().default("ghcr.io/switchroom/switchroom-agent:latest").describe("Image reference whose remote digest is compared to the local " + "image digest. Defaults to the agent image's :latest tag, which " + "is the canonical signal that a release has been promoted.")
|
|
4905
4905
|
});
|
|
4906
|
+
ScratchConfigSchema = exports_external.object({
|
|
4907
|
+
enabled: exports_external.boolean().default(true).describe("Whether agents get a scratch mount at /scratch with their package " + "caches redirected there. Default: true — but the feature only " + "engages when `volume` actually exists on the host, so a single-disk " + "machine is unaffected. Set false to opt out even where it does."),
|
|
4908
|
+
volume: exports_external.string().refine((v) => v.startsWith("/"), {
|
|
4909
|
+
message: "scratch.volume must be an absolute host path"
|
|
4910
|
+
}).default("/mnt/bulkdata").describe("Absolute host path of the bulk device's mountpoint. MUST already " + "exist — its presence is the single probe that turns the feature on, " + "so a typo degrades to the pre-existing behaviour rather than " + "quietly relocating caches somewhere that is still the root disk. " + "Default: /mnt/bulkdata."),
|
|
4911
|
+
subdir: exports_external.string().refine((v) => !v.startsWith("/") && !v.split("/").includes(".."), {
|
|
4912
|
+
message: "scratch.subdir must be a relative path without '..' segments"
|
|
4913
|
+
}).default("switchroom/scratch").describe("Relative path under `volume` holding the per-agent scratch " + "directories (`<volume>/<subdir>/<agent>`). Default: " + "switchroom/scratch.")
|
|
4914
|
+
});
|
|
4915
|
+
DiskConfigSchema = exports_external.object({
|
|
4916
|
+
warn_pct: exports_external.number().int().min(1).max(99).default(80).describe("Used-percentage at or above which `switchroom doctor` WARNs about " + "the filesystem holding the agents directory. Default 80 — the " + "reference fleet was at 85% when the condition was found by hand."),
|
|
4917
|
+
fail_pct: exports_external.number().int().min(2).max(100).default(90).describe("Used-percentage at or above which `switchroom doctor` FAILs. Must " + "be greater than `warn_pct`. Default 90."),
|
|
4918
|
+
reap_report: exports_external.object({
|
|
4919
|
+
enabled: exports_external.boolean().default(true).describe("Whether doctor checks that the report-only worktree sweep " + "(`switchroom worktree reap-report --append <file>`) is actually " + "running. Set false on a host that deliberately does not run it."),
|
|
4920
|
+
log: exports_external.string().min(1).default("/var/log/switchroom/reap-report.jsonl").describe("Path of the JSONL evidence log the scheduled `worktree " + "reap-report --append` writes. Doctor reads the newest record's " + "`generatedAt` — it detects the sweep by its OUTPUT, so it is " + "agnostic about whether an operator crontab, /etc/cron.d, or a " + "systemd timer drives it. Default " + "/var/log/switchroom/reap-report.jsonl (the path documented in " + "docs/operators/worktree-gc.md)."),
|
|
4921
|
+
max_age_hours: exports_external.number().int().min(1).max(720).default(48).describe("How old the newest record in `log` may be before doctor WARNs " + "that the sweep has stopped running. Default 48 — twice the " + "documented daily cadence, so a single missed run is not noise.")
|
|
4922
|
+
}).default({}).describe("Liveness check for the report-only worktree sweep. REPORT-ONLY by " + "construction: doctor reads an evidence log and never invokes any " + "reclaim path.")
|
|
4923
|
+
}).refine((v) => v.fail_pct > v.warn_pct, {
|
|
4924
|
+
message: "disk.fail_pct must be greater than disk.warn_pct",
|
|
4925
|
+
path: ["fail_pct"]
|
|
4926
|
+
});
|
|
4906
4927
|
HostControlConfigSchema = exports_external.object({
|
|
4907
4928
|
enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip — the gateway's /restart, /new, /reset, " + "and /update apply slash-commands all dispatch through hostd, and " + "without it those verbs fail on docker-mode installs because the " + "agent container has no docker binary/socket). " + "When true, the compose generator emits per-agent bind mounts " + "at `~/.switchroom/hostd/<name>/sock` for every admin-flagged " + "agent. Install the daemon with `switchroom hostd install` — " + "it runs as a docker container in its own compose project " + "(`switchroom-hostd`), separate from the agent fleet's compose " + "project so `up -d --remove-orphans` cycles of the fleet " + "can't recreate the daemon mid-RPC. See RFC C §5.1. " + "Set enabled: false only on legacy systemd-mode installs that " + "still rely on the in-container `spawnSwitchroomDetached` " + "shellout (removal is tracked as RFC C Phase 3)."),
|
|
4908
4929
|
auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
|
|
@@ -4977,6 +4998,8 @@ var init_schema = __esm(() => {
|
|
|
4977
4998
|
microsoft_workspace: MicrosoftWorkspaceConfigSchema.describe("RFC #1873 (Microsoft 365 integration). Top-level Microsoft Workspace " + "configuration — OAuth client credentials (Entra app), authority " + "endpoint (defaults to /common for personal MSA + work), and the " + "org_mode opt-in for Teams/SharePoint surfaces. Block is optional; " + "when omitted the broker does not register the Microsoft provider."),
|
|
4978
4999
|
notion_workspace: NotionWorkspaceConfigSchema.describe("RFC reference/rfcs/notion-integration.md. Top-level Notion integration " + "config — vault key for the integration token, friendly-name → " + "database UUID map, optional MCP-package version pin, and optional " + "global rate-limit override (default 3 rps, Notion's documented " + "public-API limit). Block is optional; when omitted no agent gets a " + "Notion MCP entry regardless of per-agent config."),
|
|
4979
5000
|
quota: QuotaConfigSchema.optional().describe("Optional weekly/monthly USD spend budgets rendered in the session " + "greeting. Usage is read from ccusage at runtime; no network calls."),
|
|
5001
|
+
scratch: ScratchConfigSchema.optional().describe("Per-agent scratch volume. Relocates every agent's build/package " + "caches (uv, npm, bun, playwright, puppeteer, pip user-site) off the " + "root disk onto a bulk device, bind-mounted at /scratch inside each " + "container. Framework-injected for EVERY agent — not routed through " + "the admin-only `bind_mounts:` escalation, because the biggest cache " + "consumers are ordinary non-admin agents. Omit the block to accept " + "defaults; the feature is a no-op unless `scratch.volume` exists on " + "the host."),
|
|
5002
|
+
disk: DiskConfigSchema.default({}).describe("Root-disk headroom thresholds for `switchroom doctor`. Measured " + "against the filesystem holding the agents directory (and the scratch " + "volume when that feature is engaged), not against `/`. Omit the block " + "to accept the defaults (WARN at 80% used, FAIL at 90%)."),
|
|
4980
5003
|
host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (reference/rfcs/host-control-daemon.md). Omit the block " + "to accept defaults; set `enabled: false` only on legacy systemd-" + "mode installs (removal tracked as RFC C Phase 3)."),
|
|
4981
5004
|
hostd: HostdConfigSchema.default({}).describe("hostd verb-level knobs (RFC admin-agent-config-edit). Distinct " + "from `host_control:` which governs whether the daemon runs at " + "all. Scopes the opt-in flag and rate cap for the " + "`config_propose_edit` verb (disabled by default)."),
|
|
4982
5005
|
fleet_health: FleetHealthConfigSchema.default({}).describe("Fleet Health — job-spec-anchored, operator-facing issue tracker (RFC " + "fleet-health.md, serves fleet-stays-healthy). Assigns the owner agent " + "that runs the nightly model-free sensor + weekly deep-dive. Default " + "unset owner_agent → inert; the admin page renders an empty state."),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.13",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"lint:claude-cli-lockstep": "node scripts/check-claude-cli-lockstep.mjs",
|
|
58
58
|
"lint:changelog-entry": "node scripts/check-changelog-entry.mjs",
|
|
59
59
|
"changelog:generate": "node scripts/gen-changelog-entry.mjs",
|
|
60
|
+
"changelog:cut": "node scripts/cut-changelog-release.mjs",
|
|
60
61
|
"lint:agent-attribution-trailers": "node scripts/check-agent-attribution-trailers.mjs",
|
|
61
62
|
"lint:hostd-template-guard": "bun scripts/check-hostd-template-guard.ts",
|
|
62
63
|
"lint:bench-baseline-anonymised": "node scripts/check-bench-baseline-anonymised.mjs",
|
|
@@ -83,6 +84,7 @@
|
|
|
83
84
|
"@types/bun": "^1.3.11",
|
|
84
85
|
"@types/node": "^22.0.0",
|
|
85
86
|
"@vitest/coverage-v8": "3.2.4",
|
|
87
|
+
"commonmark": "0.31.2",
|
|
86
88
|
"typescript": "^5.7.0",
|
|
87
89
|
"vitest": "^3.2.4"
|
|
88
90
|
},
|
|
@@ -139,17 +139,21 @@ is not a rebuttal.
|
|
|
139
139
|
`AWAITING_CHECKS` means a required workflow is not listening for
|
|
140
140
|
`merge_group`; `.github/MERGE-QUEUE.md` owns that failure mode and the
|
|
141
141
|
invariants that prevent it.
|
|
142
|
-
- **Stage the
|
|
143
|
-
changes shippable code must add a
|
|
142
|
+
- **Stage the changelog note author-side, before `gh pr create`.** A PR that
|
|
143
|
+
changes shippable code must add a NEW `changelog.d/` fragment file (#4469's
|
|
144
144
|
`check-changelog-entry.mjs`, part of `npm run lint`, enforces it). Don't
|
|
145
|
-
hand-
|
|
146
|
-
|
|
147
|
-
(idempotently — a second run is a no-op)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
hand-edit `CHANGELOG.md`: run `bun run changelog:generate` in the repo — it
|
|
146
|
+
derives a per-PR fragment (`changelog.d/<pr>-<slug>.<type>.md`) from your
|
|
147
|
+
conventional-commit title (idempotently — a second run is a no-op) and
|
|
148
|
+
respects the escape hatches. Then commit the fragment so it lands in your
|
|
149
|
+
own push. Fragment files are per-PR, so they never conflict with another
|
|
150
|
+
in-flight PR — editing the shared `## Unreleased` section does, which is
|
|
151
|
+
exactly the conflict class fragments replace (see `changelog.d/README.md`).
|
|
152
|
+
It runs AUTHOR-side, not in CI, because a CI commit-back with the default
|
|
153
|
+
`GITHUB_TOKEN` cannot re-trigger the required checks and would wedge the PR
|
|
154
|
+
(`.github/MERGE-QUEUE.md` § "Author-side changelog generation"). Opt a
|
|
155
|
+
docs/chore/test PR out with the `no-changelog` label or a `[skip changelog]`
|
|
156
|
+
token on its own line.
|
|
153
157
|
- **A test that wouldn't fail on the bug it guards is not a test.** Assert the
|
|
154
158
|
observable outcome, not that the code path executed.
|
|
155
159
|
- **Prefer a deterministic mechanism over prompt discipline.** If a check, a
|
|
@@ -12,13 +12,15 @@ Switchroom is a multi-agent orchestrator built on Claude Code. It manages multip
|
|
|
12
12
|
|
|
13
13
|
**One `switchroom.yaml` to rule them all.** All agents are configured from a single file using a three-layer cascade. See [cascade.md](cascade.md) for full merge semantics.
|
|
14
14
|
|
|
15
|
-
**Agents as Docker containers.** Each agent runs as a long-lived `claude` process inside its own container (`switchroom-<name>`), supervised by Docker Compose with `restart:
|
|
15
|
+
**Agents as Docker containers.** Each agent runs as a long-lived `claude` process inside its own container (`switchroom-<name>`), supervised by Docker Compose with `restart: always` (`src/agents/compose.ts`). The agent service itself has NO healthcheck — it `depends_on` `vault-broker` (`service_started`), `approval-kernel` (`service_started`), and `switchroom-auth-broker` (`service_healthy`), so it waits on the auth-broker's healthcheck before booting. Healthchecks exist on `vault-broker`, `approval-kernel`, `switchroom-auth-broker`, and `voice-sidecar` — not on the agent container. The `start.sh` script sets environment variables and execs into `claude`. Claude Code handles session persistence and tool execution.
|
|
16
16
|
|
|
17
|
-
**Telegram as the primary interface.** The `switchroom-telegram` MCP plugin connects Claude Code to Telegram, providing
|
|
17
|
+
**Telegram as the primary interface.** The `switchroom-telegram` MCP plugin connects Claude Code to Telegram, providing message-handling tools (see [telegram.md](telegram.md) for the current tool list and categories — don't hard-code a count here, it drifts).
|
|
18
18
|
|
|
19
19
|
**Hindsight for memory.** Cross-session memory uses the Hindsight MCP server — a semantic vector store with knowledge graphs, mental models, and directives. Each agent has its own named collection.
|
|
20
20
|
|
|
21
|
-
**Skills as reusable behavior.** Shared skills live in `~/.switchroom/skills/` (or `switchroom.skills_dir`). Scaffold symlinks selected skills into each agent's
|
|
21
|
+
**Skills as reusable behavior.** Shared skills live in `~/.switchroom/skills/` (or `switchroom.skills_dir`). Scaffold symlinks selected skills into each agent's `.claude/skills/` directory (`src/agents/scaffold.ts`; `migrateLegacySkillsDir` migrates any pre-existing symlinks from the old `<agentDir>/skills/` location). Claude Code loads them at session start.
|
|
22
|
+
|
|
23
|
+
**Beyond the agent containers.** The agent fleet's compose file (`generateCompose`, `src/agents/compose.ts`) emits the per-agent `switchroom-<name>` services plus three shared services every agent depends on: `vault-broker`, `approval-kernel`, and `switchroom-auth-broker` (agent `depends_on`, `src/agents/compose.ts` around `emitAgentService`'s `depends_on` block). Optionally, per-agent `voice-sidecar` services are emitted too. `hostd` (`src/cli/hostd.ts`) and `web` (`src/cli/webd.ts`) run as their OWN separate compose projects (`switchroom-hostd`, `switchroom-web`) — not part of the agent fleet compose file, and not self-healing on an image-pin bump the way agents are.
|
|
22
24
|
|
|
23
25
|
## Directory layout
|
|
24
26
|
|
|
@@ -34,10 +36,12 @@ Switchroom is a multi-agent orchestrator built on Claude Code. It manages multip
|
|
|
34
36
|
├── start.sh # launcher (sets env, execs claude)
|
|
35
37
|
├── settings.json # Claude Code settings
|
|
36
38
|
├── .mcp.json # MCP server config
|
|
37
|
-
├── CLAUDE.md # agent identity (
|
|
38
|
-
|
|
39
|
+
├── CLAUDE.md # agent identity (reconcile rewrites content ABOVE the
|
|
40
|
+
│ # `# --- Yours ---` marker; below it always survives.
|
|
41
|
+
│ # `--preserve-claude-md` opts out of the rewrite.)
|
|
39
42
|
├── .claude/
|
|
40
|
-
│
|
|
43
|
+
│ ├── agents/ # sub-agent definition files
|
|
44
|
+
│ └── skills/ # symlinks to ~/.switchroom/skills/<name>/
|
|
41
45
|
└── telegram/
|
|
42
46
|
├── history.db # SQLite message buffer
|
|
43
47
|
└── access.json # per-agent access control
|
|
@@ -52,7 +56,7 @@ Switchroom is a multi-agent orchestrator built on Claude Code. It manages multip
|
|
|
52
56
|
5. MCP servers connect (Hindsight, switchroom-telegram, others)
|
|
53
57
|
6. Telegram plugin polls for messages
|
|
54
58
|
7. User sends message → plugin fires `UserPromptSubmit` hook → Claude responds
|
|
55
|
-
8. `switchroom agent reconcile <name>` — re-apply switchroom.yaml (
|
|
59
|
+
8. `switchroom agent reconcile <name>` — re-apply switchroom.yaml (rewrites `.mcp.json` + `settings.json` + `start.sh` + CLAUDE.md's managed section above the `# --- Yours ---` marker; pass `--preserve-claude-md` to skip the CLAUDE.md rewrite)
|
|
56
60
|
|
|
57
61
|
## Deep dives
|
|
58
62
|
|
|
@@ -16,13 +16,16 @@ The resolved value at any field is determined by the **merge type** for that fie
|
|
|
16
16
|
|
|
17
17
|
| Merge type | Fields | Behavior |
|
|
18
18
|
|---|---|---|
|
|
19
|
-
| **Union** | `tools.allow`, `tools.deny`, `skills` | Combine across all layers, dedup |
|
|
19
|
+
| **Union** | `tools.allow`, `tools.deny`, `skills`, `secrets`, `allowed_tools`, `disallowed_tools`, `extra_stable_files` | Combine across all layers, dedup-preserving-order (defaults first) |
|
|
20
20
|
| **Override** | `model`, `extends`, `dangerous_mode`, most scalars | Agent wins entirely |
|
|
21
|
-
| **Per-key merge** | `mcp_servers`, `env`, `
|
|
22
|
-
| **Per-field merge** | `
|
|
23
|
-
| **Per-
|
|
21
|
+
| **Per-key merge** | `mcp_servers`, `env`, `bundled_skills` | Agent wins on key conflict, others preserved |
|
|
22
|
+
| **Per-key merge with field-level merge on conflict** | `subagents` | Agent wins per key; on a key present at both layers, fields are merged field-by-field (not a whole-definition replacement — that was the pre-#682 bug) |
|
|
23
|
+
| **Per-field merge** | `soul`, `session`, `session_continuity`, `channels`, `reactions`, `reaction_dispatch` | Agent wins per sub-field. `channels` and `reactions`/`reaction_dispatch` sub-arrays (e.g. `trigger_emojis`) use REPLACE semantics, not union. |
|
|
24
|
+
| **One-level-deep merge** | `memory` (`recall`/`retain`/`disposition` sub-objects), `litellm` (scalars + per-key `tags`) | Top-level fields override; the named sub-object merges one level deep instead of replacing wholesale, so overriding one knob doesn't drop its siblings |
|
|
25
|
+
| **Per-event concat** | `hooks` | Defaults appended first, then agent (no dedup — identical entries may be intentional) |
|
|
24
26
|
| **Concatenate** | `schedule`, `system_prompt_append`, `claude_md_raw`, `cli_args` | Defaults prepended |
|
|
25
27
|
| **Deep merge** | `settings_raw` | Recursive object merge, agent wins |
|
|
28
|
+
| **Replace-if-unset** | `release` | Whole-block replace; an agent-declared `release` is NOT field-merged with defaults (deliberate — a pinned agent must not silently inherit a channel/pin from the fleet, or vice versa) |
|
|
26
29
|
|
|
27
30
|
## Examples
|
|
28
31
|
|
|
@@ -65,7 +68,7 @@ Prefer TypeScript.
|
|
|
65
68
|
Never use `any`.
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
### subagents per-key merge
|
|
71
|
+
### subagents per-key merge with field-level merge on conflict
|
|
69
72
|
```yaml
|
|
70
73
|
defaults:
|
|
71
74
|
subagents:
|
|
@@ -77,11 +80,16 @@ agents:
|
|
|
77
80
|
dev:
|
|
78
81
|
subagents:
|
|
79
82
|
worker:
|
|
80
|
-
description: "Code implementation with tests" #
|
|
81
|
-
|
|
82
|
-
tools: [Read, Edit, Write, Bash]
|
|
83
|
+
description: "Code implementation with tests" # overrides only this field
|
|
84
|
+
tools: [Read, Edit, Write, Bash] # added; model still inherited
|
|
83
85
|
# researcher and reviewer inherited from defaults unchanged
|
|
84
86
|
```
|
|
87
|
+
`worker`'s resolved definition is `{ description: "Code implementation with
|
|
88
|
+
tests", model: sonnet, tools: [Read, Edit, Write, Bash] }` — the agent layer
|
|
89
|
+
merges field-by-field onto the matching default key, it does not replace the
|
|
90
|
+
whole `worker` definition (`src/config/merge.ts`, `subagents: per-key merge,
|
|
91
|
+
with field-level merge on conflict`; whole-def replacement was the pre-#682
|
|
92
|
+
bug this fixed).
|
|
85
93
|
|
|
86
94
|
### hooks per-event concat
|
|
87
95
|
```yaml
|
|
@@ -103,10 +111,21 @@ agents:
|
|
|
103
111
|
Profiles can be defined in two places (inline takes priority):
|
|
104
112
|
|
|
105
113
|
1. **Inline** in `profiles:` section of switchroom.yaml
|
|
106
|
-
2. **Filesystem** at `profiles/<name>/` — contains `CLAUDE.md.hbs`, `SOUL.md.hbs
|
|
114
|
+
2. **Filesystem** at `profiles/<name>/` — contains `CLAUDE.md.hbs`, plus optional `SOUL.md.hbs` and `skills/`
|
|
107
115
|
|
|
108
116
|
An agent inherits from at most one profile via `extends: <name>`. Profiles themselves do not chain.
|
|
109
117
|
|
|
110
118
|
## Vault references
|
|
111
119
|
|
|
112
|
-
Secrets in switchroom.yaml use `vault:key-name` syntax
|
|
120
|
+
Secrets in switchroom.yaml use `vault:key-name` syntax; `vault:<key>#<filename>`
|
|
121
|
+
additionally inlines one named file's contents as a string from a
|
|
122
|
+
`kind: "files"` vault entry (`src/vault/resolver.ts:194`,
|
|
123
|
+
`resolveSingleReference`). At scaffold/reconcile time these are resolved
|
|
124
|
+
from `~/.switchroom/vault.enc` and written into `start.sh` as environment
|
|
125
|
+
variables — never stored in plaintext in switchroom.yaml.
|
|
126
|
+
|
|
127
|
+
In production, agent-runtime vault reads do NOT go through that scaffold-time
|
|
128
|
+
decrypt path — they go through the vault-broker daemon over a local socket
|
|
129
|
+
(`resolveVaultReferencesViaBroker`, `src/vault/resolver.ts:288`), which is
|
|
130
|
+
what enforces the per-agent grant/deny model (`VAULT-BROKER-DENIED`,
|
|
131
|
+
`vault_request_access`) documented for agents at runtime.
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Switchroom generates Claude Code custom sub-agent files (`.claude/agents/<name>.md`) from `switchroom.yaml`. This enables the "Opus plans, Sonnet implements" pattern: the main agent delegates to cheaper models running in the background.
|
|
4
4
|
|
|
5
|
-
## Default sub-agents
|
|
5
|
+
## Default sub-agents (starter template, not a hard-coded default)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`subagents` is `.optional()` with no schema-level `.default()`
|
|
8
|
+
(`src/config/schema.ts`), and nothing in `src/agents/scaffold.ts` injects a
|
|
9
|
+
worker/researcher/reviewer set — an agent with no `subagents` block anywhere
|
|
10
|
+
in its cascade simply has none. The three-sub-agent pattern below is the
|
|
11
|
+
**starter template** shipped in `examples/switchroom.yaml` (`worker` at
|
|
12
|
+
`:111`, `researcher` at `:137`, `reviewer` at `:149`), not something
|
|
13
|
+
switchroom ships by default:
|
|
8
14
|
|
|
9
15
|
| Sub-agent | Model | Purpose |
|
|
10
16
|
|-----------|-------|---------|
|
|
@@ -12,7 +18,9 @@ Switchroom ships three default sub-agents that every agent inherits:
|
|
|
12
18
|
| **researcher** | Haiku | Exploration — codebase search, docs, investigation |
|
|
13
19
|
| **reviewer** | Sonnet | Quality review — correctness, completeness, security |
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
If you want this pattern, declare it under `defaults.subagents` (or a
|
|
22
|
+
profile) yourself — copy it from `examples/switchroom.yaml` — and it will
|
|
23
|
+
then flow through the cascade to every agent.
|
|
16
24
|
|
|
17
25
|
## How delegation works
|
|
18
26
|
|
|
@@ -30,15 +38,15 @@ Each sub-agent supports the full Claude Code frontmatter spec:
|
|
|
30
38
|
|
|
31
39
|
| Field | Description |
|
|
32
40
|
|-------|-------------|
|
|
33
|
-
| `description` | (
|
|
41
|
+
| `description` | Schema-optional (a partial override, e.g. `isolation` only, need not restate it — the cascade retains the base definition's description on merge) but effectively required for a fresh, non-overriding definition: when the main agent should delegate here |
|
|
34
42
|
| `model` | `sonnet`, `opus`, `haiku`, full model ID, or `inherit` |
|
|
35
43
|
| `background` | Run non-blocking. Default: false |
|
|
36
44
|
| `isolation` | `worktree` — own git branch for file work |
|
|
37
45
|
| `tools` | Tool allowlist (inherits all if omitted) |
|
|
38
46
|
| `disallowedTools` | Tool denylist |
|
|
39
47
|
| `maxTurns` | Auto-stop after N turns |
|
|
40
|
-
| `permissionMode` | `default`, `acceptEdits`, `auto`, `bypassPermissions`, `plan` |
|
|
41
|
-
| `effort` | `low`, `medium`, `high`, `max` |
|
|
48
|
+
| `permissionMode` | `default`, `acceptEdits`, `auto`, `dontAsk`, `bypassPermissions`, `plan` |
|
|
49
|
+
| `effort` | `low`, `medium`, `high`, `xhigh`, `max` |
|
|
42
50
|
| `color` | Display color in task list |
|
|
43
51
|
| `memory` | `user`, `project`, or `local` for persistent learning |
|
|
44
52
|
| `skills` | Skills to preload |
|
|
@@ -46,7 +54,7 @@ Each sub-agent supports the full Claude Code frontmatter spec:
|
|
|
46
54
|
|
|
47
55
|
## Cascade behavior
|
|
48
56
|
|
|
49
|
-
Sub-agents are **per-key merged
|
|
57
|
+
Sub-agents are **per-key merged, with field-level merge on conflict** (`src/config/merge.ts`) — see [cascade.md](cascade.md). An agent overrides a specific sub-agent by declaring one with the same name; only the fields it sets are replaced, everything else is inherited from the base definition:
|
|
50
58
|
|
|
51
59
|
```yaml
|
|
52
60
|
defaults:
|
|
@@ -66,22 +74,15 @@ agents:
|
|
|
66
74
|
# researcher and reviewer inherited unchanged from defaults
|
|
67
75
|
```
|
|
68
76
|
|
|
69
|
-
## Model resolution
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
| Plan | Inherit | Claude Code built-in |
|
|
82
|
-
| general-purpose | Inherit | Claude Code built-in |
|
|
83
|
-
| worker | Sonnet | Switchroom default |
|
|
84
|
-
| researcher | Haiku | Switchroom default |
|
|
85
|
-
| reviewer | Sonnet | Switchroom default |
|
|
86
|
-
|
|
87
|
-
All sub-agents share the same `.claude/agents/` directory. Switchroom-generated files don't conflict with Claude Code's built-ins.
|
|
77
|
+
## Model resolution and Claude Code's built-in sub-agents
|
|
78
|
+
|
|
79
|
+
How Claude Code itself resolves a sub-agent's model (env var precedence,
|
|
80
|
+
per-invocation overrides) and how switchroom-generated `.claude/agents/*.md`
|
|
81
|
+
files coexist with Claude Code's own built-in sub-agents (e.g. Explore,
|
|
82
|
+
Plan, general-purpose) is upstream Claude Code CLI behaviour — this repo's
|
|
83
|
+
`src/` has no code that reads or sets a `CLAUDE_CODE_SUBAGENT_MODEL` env var,
|
|
84
|
+
so switchroom's own source cannot confirm or deny any specific precedence
|
|
85
|
+
order or built-in roster here. Consult Anthropic's Claude Code documentation
|
|
86
|
+
for the authoritative answer; all switchroom does is render one `.md` file
|
|
87
|
+
per configured sub-agent into `.claude/agents/<name>.md` and let Claude Code
|
|
88
|
+
own everything downstream of that file.
|
|
@@ -2,32 +2,45 @@
|
|
|
2
2
|
|
|
3
3
|
Switchroom ships an enhanced `switchroom-telegram` MCP plugin that replaces the official marketplace plugin. It is the default — no configuration needed.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
13
|
-
| `forward_message` |
|
|
14
|
-
| `send_typing` |
|
|
15
|
-
| `download_attachment` | Fetch files attached to inbound messages. |
|
|
16
|
-
| `
|
|
5
|
+
## MCP tools
|
|
6
|
+
|
|
7
|
+
The plugin's tool count changes as features land — don't hard-code a number
|
|
8
|
+
here; `telegram-plugin/bridge/bridge.ts`'s `TOOL_SCHEMAS` array is the source
|
|
9
|
+
of truth. As of this writing it defines 21 tool schemas, grouped by purpose:
|
|
10
|
+
|
|
11
|
+
| Category | Tools | What it does |
|
|
12
|
+
|----------|-------|-------------|
|
|
13
|
+
| Messaging | `reply`, `edit_message`, `delete_message`, `forward_message`, `progress_update` | `reply` is the single final-answer tool — send text, photos, or documents. Chunks anything over the 32768-char rich-message cap (4096 applies only to plain-text degradations). Supports threading, topic routing, file attachments. `edit_message` updates a previously sent message silently (no push notification). `delete_message` removes a bot-sent message (48h Telegram API limit). `forward_message` quotes/resurfaces earlier messages with thread support. `progress_update` posts a short interim status line mid-turn. |
|
|
14
|
+
| Interactivity | `react`, `send_typing`, `ask_user`, `send_checklist`, `update_checklist`, `send_sticker`, `send_gif` | `react` adds emoji reactions (Telegram whitelist). `send_typing` shows a typing indicator (5s auto-expire). `ask_user` blocks for a structured reply. `send_checklist`/`update_checklist` render and mutate a tappable checklist card. `send_sticker`/`send_gif` send media. |
|
|
15
|
+
| History & attachments | `download_attachment`, `get_recent_messages` | Fetch files attached to inbound messages; query the SQLite history buffer with pagination and thread filtering. |
|
|
16
|
+
| Vault & secrets | `vault_request_save`, `vault_request_access`, `request_secret` | Post approval cards for saving/granting/requesting vault-backed secrets. |
|
|
17
|
+
| Memory | `mental_model_propose` | Post an approval card to create/refresh a Hindsight mental model. |
|
|
18
|
+
| Linear (conditional) | `linear_agent_activity`, `linear_create_issue`, `linear_agent_setup` | Only registered when Linear integration is configured for the agent. |
|
|
17
19
|
|
|
18
20
|
## Emoji status lifecycle
|
|
19
21
|
|
|
20
|
-
The plugin automatically reacts to inbound messages with a
|
|
22
|
+
The plugin automatically reacts to inbound messages with a state machine
|
|
23
|
+
(`telegram-plugin/status-reactions.ts`) tracking CURRENT TURN ACTIVITY, not
|
|
24
|
+
delivery state. The real state set: `queued, thinking, coding, web,
|
|
25
|
+
compacting, awaiting, undelivered, error, stallSoft, stallHard`. Working
|
|
26
|
+
states (`thinking`, `tool`, `coding`, `web`, `compacting`) cycle freely and
|
|
27
|
+
bidirectionally within one turn — none is "higher" than another. The only
|
|
28
|
+
terminal state is reached via `finalize()`, triggered by the Stop hook
|
|
29
|
+
(`turn_end`).
|
|
30
|
+
|
|
31
|
+
A representative progression:
|
|
21
32
|
|
|
22
33
|
```
|
|
23
|
-
👀 queued → 🤔 thinking → 👨💻
|
|
34
|
+
👀 queued → 🤔 thinking → 👨💻 coding → 👍 done
|
|
24
35
|
```
|
|
25
36
|
|
|
26
|
-
Stall watchdogs
|
|
37
|
+
Stall watchdogs auto-promote to `🥱` (stallSoft) at 30s idle, `😨` (stallHard)
|
|
38
|
+
at 90s — so the user always knows the agent is alive. `🔥` is reserved for
|
|
39
|
+
genuine 5xx server errors, not for "streaming" — there is no streaming state.
|
|
27
40
|
|
|
28
41
|
Tool-specific reactions:
|
|
29
|
-
- `👨💻` for Bash/Edit/Write
|
|
30
|
-
- `⚡` for web search/fetch
|
|
42
|
+
- `👨💻` for Bash/Edit/Write (coding)
|
|
43
|
+
- `⚡` for web search/fetch (web)
|
|
31
44
|
|
|
32
45
|
## Message history
|
|
33
46
|
|
|
@@ -55,6 +68,20 @@ bisecting code fences or table rows; bodies that degrade to plain text fall
|
|
|
55
68
|
back to the legacy 4096-char cap. See
|
|
56
69
|
`reference/telegram-formatting-guide.md` for the full vocabulary.
|
|
57
70
|
|
|
71
|
+
## Inbound message attributes
|
|
72
|
+
|
|
73
|
+
Every inbound Telegram message arrives as a `<channel source="telegram" ...>`
|
|
74
|
+
tag whose attributes are built in
|
|
75
|
+
`telegram-plugin/gateway/inbound-router.ts:112-484`. Notable ones:
|
|
76
|
+
|
|
77
|
+
| Attribute | Meaning |
|
|
78
|
+
|-----------|---------|
|
|
79
|
+
| `reply_to_message_id` | Set when the user long-pressed a prior message and chose Reply — that message is the antecedent for "this"/"that" pronoun references. `reply_to_text`/`reply_to_role`/`reply_to_kind` accompany it. |
|
|
80
|
+
| `message_thread_id` | The forum topic the message came from. |
|
|
81
|
+
| `origin_turn_id` | Pass this back on a reply (instead of `message_thread_id`) so the answer routes to the topic this message came from, even if a message from another topic arrived mid-turn. |
|
|
82
|
+
| `attachment_file_id`, `attachment_kind` | Present when the inbound message has a file attachment; feed `attachment_file_id` to `download_attachment`. |
|
|
83
|
+
| `forwarded_from`, `forwarded_from_type`, `forwarded_from_id`, `forwarded_date` | Server-stamped forward-origin context (Bot API 7.0+ `forward_origin`) — trustworthy provenance, unlike the forwarded body text. A multi-origin burst carries numbered siblings (`forwarded_from_2`, ...). |
|
|
84
|
+
|
|
58
85
|
## Access control
|
|
59
86
|
|
|
60
87
|
`telegram/access.json` per agent:
|
|
@@ -22,14 +22,20 @@ Cut a release of `switchroom/switchroom` and get it live on the fleet. This is a
|
|
|
22
22
|
1. `git fetch origin`, confirm `main` is at the commit you want released.
|
|
23
23
|
2. `gh pr list --state open` — confirm no PR meant for this release is still open. Ask the operator if unsure.
|
|
24
24
|
3. Confirm CI on `main` is green (`gh run list --branch main --limit 3`).
|
|
25
|
-
4. Read
|
|
25
|
+
4. Read the staged notes — they live in TWO places, both **continuously maintained** as PRs merge and both enforced by `scripts/check-changelog-entry.mjs` (part of `npm run lint`): the `changelog.d/` **fragment files** (the primary path — every shippable PR adds its own `<pr>-<slug>.<type>.md`; see `changelog.d/README.md`) and any legacy/hand-staged entries under `## Unreleased` in `CHANGELOG.md`. Together they should already read as a near-complete draft — you assemble and tidy, you do NOT author from scratch. **Nothing staged at release time (no fragments AND an empty `## Unreleased`) is an ANOMALY, not the normal state** — `bun run changelog:cut` refuses to cut it: it means either there genuinely is nothing to release, OR the enforcement was bypassed (`no-changelog` labels / `[skip changelog]` tokens on their own line on PRs that should have staged notes). Before assuming the former, cross-check against `git log <last-tag>..origin/main` — if real shippable work landed but nothing is staged, investigate and reconstruct rather than shipping a hollow release (this is exactly the v0.20.12 failure mode the enforcement exists to prevent).
|
|
26
26
|
5. Pick the next version: read the latest tag (`git tag --list 'v*' --sort=-v:refname | head -1`) and bump the patch (or minor if the operator asks). Confirm with the operator which.
|
|
27
27
|
|
|
28
|
-
## Step 1 —
|
|
28
|
+
## Step 1 — Assemble the changelog (the release commit is CHANGELOG + fragment deletions only)
|
|
29
29
|
|
|
30
|
-
- The
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
- The notes are staged continuously (see pre-flight 4), so this step is **one command + a review**, not authorship:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bun run changelog:cut -- --version vX.Y.Z --summary "<one-line summary>"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`scripts/cut-changelog-release.mjs` folds every `changelog.d/` fragment (plus anything hand-staged under `## Unreleased`) into a new `## vX.Y.Z — <summary>` section grouped by category, **re-seeds a fresh empty `## Unreleased` block** (header + convention comment) below `# Changelog`, and **deletes the consumed fragment files**. Use `--dry-run` first to preview the assembled section. It FAILS LOUDLY on a double cut or an empty cut — if it refuses for "nothing to release", STOP and resolve the anomaly (pre-flight 4); do not invent a release note.
|
|
37
|
+
- Read through the assembled section and lightly edit for grouping/summary before committing.
|
|
38
|
+
- The release commit touches **CHANGELOG.md and the deleted `changelog.d/` fragments only**. Do NOT bump `package.json` (placeholder discipline). (A CHANGELOG + fragment-deletion PR changes no shippable code, so `check-changelog-entry.mjs` passes it without a staged note of its own — deleting fragments never counts as staging one.)
|
|
33
39
|
- Branch protection blocks direct push to `main`, so: create a `release/vX.Y.Z` branch, push it, open a `chore: release vX.Y.Z` PR (base `main`), arm auto-merge (squash, delete-branch) on green CI.
|
|
34
40
|
|
|
35
41
|
## Step 2 — Create the DRAFT release on a PINNED SHA, then push the tag
|