switchroom 0.13.5 → 0.13.8

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.
@@ -11307,6 +11307,10 @@ var QuotaConfigSchema = exports_external.object({
11307
11307
  var HostControlConfigSchema = exports_external.object({
11308
11308
  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).")
11309
11309
  });
11310
+ var HostdConfigSchema = exports_external.object({
11311
+ config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
11312
+ config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
11313
+ });
11310
11314
  var SwitchroomConfigSchema = exports_external.object({
11311
11315
  switchroom: exports_external.object({
11312
11316
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11333,6 +11337,7 @@ var SwitchroomConfigSchema = exports_external.object({
11333
11337
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11334
11338
  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."),
11335
11339
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/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)."),
11340
+ 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. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
11336
11341
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11337
11342
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11338
11343
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -11307,6 +11307,10 @@ var QuotaConfigSchema = exports_external.object({
11307
11307
  var HostControlConfigSchema = exports_external.object({
11308
11308
  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).")
11309
11309
  });
11310
+ var HostdConfigSchema = exports_external.object({
11311
+ config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit §3). Default false — the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
11312
+ config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit §5). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
11313
+ });
11310
11314
  var SwitchroomConfigSchema = exports_external.object({
11311
11315
  switchroom: exports_external.object({
11312
11316
  version: exports_external.literal(1).describe("Config schema version"),
@@ -11333,6 +11337,7 @@ var SwitchroomConfigSchema = exports_external.object({
11333
11337
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration — " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
11334
11338
  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."),
11335
11339
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/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)."),
11340
+ 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. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a — disabled by default)."),
11336
11341
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
11337
11342
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
11338
11343
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -13520,7 +13520,7 @@ var init_zod = __esm(() => {
13520
13520
  });
13521
13521
 
13522
13522
  // src/config/schema.ts
13523
- var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, HostControlConfigSchema, SwitchroomConfigSchema;
13523
+ var CodeRepoEntrySchema, AgentBindMountSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, ReactionsSchema, ReleaseBlock, NetworkIsolationSchema, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13871,6 +13871,10 @@ var init_schema = __esm(() => {
13871
13871
  HostControlConfigSchema = exports_external.object({
13872
13872
  enabled: exports_external.boolean().default(true).describe("Whether the host-control daemon is in use. Default: true (since " + "RFC C Phase 2 default-flip \u2014 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` \u2014 " + "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 \u00a75.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).")
13873
13873
  });
13874
+ HostdConfigSchema = exports_external.object({
13875
+ config_edit_enabled: exports_external.boolean().default(false).describe("Opt-in toggle for the `config_propose_edit` hostd verb (RFC " + "admin-agent-config-edit \u00a73). Default false \u2014 the verb returns " + "`E_CONFIG_EDIT_DISABLED` until the operator explicitly flips " + "this to true. When true (and once PR 1c lands the apply path), " + "admin agents can propose unified-diff patches against " + "`/state/config/switchroom.yaml`, gated by an operator approval " + "card in the primary chat. Same trust posture as `update_apply` " + "and `agent_restart`: the human-in-the-loop tap is the security " + "boundary, not the agent's judgement."),
13876
+ config_edit_rate_per_hour: exports_external.number().int().min(1).max(20).default(3).describe("Per-requesting-agent rate cap for `config_propose_edit` cards " + "(RFC admin-agent-config-edit \u00a75). Default 3 cards/hour; min 1, " + "max 20. Implemented as a sqlite token bucket in PR 1c; the " + "field is wired here in PR 1a so operators can pin it before the " + "limiter is live. Above the cap, the verb returns " + "`E_RATE_LIMITED` without raising a card.")
13877
+ });
13874
13878
  SwitchroomConfigSchema = exports_external.object({
13875
13879
  switchroom: exports_external.object({
13876
13880
  version: exports_external.literal(1).describe("Config schema version"),
@@ -13897,6 +13901,7 @@ var init_schema = __esm(() => {
13897
13901
  google_workspace: GoogleWorkspaceConfigSchema.describe("RFC G canonical key. Top-level Google Workspace configuration \u2014 " + "OAuth client credentials, approver allowlist, and tier knob (`core` " + "| `extended` | `complete`, default `core`). Mutually exclusive with " + "`drive:` at the top level (loader fails fast if both are set)."),
13898
13902
  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."),
13899
13903
  host_control: HostControlConfigSchema.default({}).describe("Host-control daemon configuration. Defaults to enabled=true since " + "RFC C Phase 2 (docs/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)."),
13904
+ 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. Currently scopes the opt-in flag and rate cap for the new " + "`config_propose_edit` verb (PR 1a \u2014 disabled by default)."),
13900
13905
  google_accounts: exports_external.record(exports_external.string().regex(/^[^@\s:]+@[^@\s:]+\.[^@\s:]+$/, {
13901
13906
  message: "Account key must be a Google account email like 'alice@example.com' (colons not allowed)"
13902
13907
  }).transform((v) => v.trim().toLowerCase()), exports_external.object({
@@ -23140,7 +23145,7 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23140
23145
  lines.push(` - "ALL"`);
23141
23146
  lines.push(` read_only: true`);
23142
23147
  lines.push(` tmpfs:`);
23143
- lines.push(` - /tmp:size=256m,mode=1777`);
23148
+ lines.push(` - /tmp:size=1g,mode=1777`);
23144
23149
  lines.push(` depends_on:`);
23145
23150
  lines.push(` vault-broker:`);
23146
23151
  lines.push(` condition: service_started`);
@@ -29067,7 +29072,7 @@ function decodeResponse3(line) {
29067
29072
  const obj = JSON.parse(line);
29068
29073
  return ResponseSchema3.parse(obj);
29069
29074
  }
29070
- var MAX_FRAME_BYTES3, RequestEnvelope, AgentRestartRequestSchema, UpgradeStatusRequestSchema, GetStatusRequestSchema, AgentNameSchema, UpdateCheckRequestSchema, UpdateApplyRequestSchema, ApplyRequestSchema, AgentStartRequestSchema, AgentStopRequestSchema, AgentLogsRequestSchema, AgentExecRequestSchema, DoctorRequestSchema, AgentSmokeRequestSchema, RequestSchema3, ResultSchema, ResponseEnvelope, ResponseSchema3;
29075
+ var MAX_FRAME_BYTES3, RequestEnvelope, AgentRestartRequestSchema, UpgradeStatusRequestSchema, GetStatusRequestSchema, AgentNameSchema, UpdateCheckRequestSchema, UpdateApplyRequestSchema, ApplyRequestSchema, AgentStartRequestSchema, AgentStopRequestSchema, AgentLogsRequestSchema, AgentExecRequestSchema, DoctorRequestSchema, AgentSmokeRequestSchema, ConfigProposeEditRequestSchema, RequestSchema3, ResultSchema, ResponseEnvelope, ResponseSchema3;
29071
29076
  var init_protocol3 = __esm(() => {
29072
29077
  init_zod();
29073
29078
  MAX_FRAME_BYTES3 = 64 * 1024;
@@ -29161,6 +29166,15 @@ var init_protocol3 = __esm(() => {
29161
29166
  deep: exports_external.boolean().optional()
29162
29167
  })
29163
29168
  });
29169
+ ConfigProposeEditRequestSchema = exports_external.object({
29170
+ ...RequestEnvelope,
29171
+ op: exports_external.literal("config_propose_edit"),
29172
+ args: exports_external.object({
29173
+ unified_diff: exports_external.string().min(1).max(MAX_FRAME_BYTES3 - 1024),
29174
+ reason: exports_external.string().min(1).max(500),
29175
+ target_path: exports_external.literal("/state/config/switchroom.yaml")
29176
+ })
29177
+ });
29164
29178
  RequestSchema3 = exports_external.discriminatedUnion("op", [
29165
29179
  AgentRestartRequestSchema,
29166
29180
  UpgradeStatusRequestSchema,
@@ -29173,7 +29187,8 @@ var init_protocol3 = __esm(() => {
29173
29187
  AgentLogsRequestSchema,
29174
29188
  AgentExecRequestSchema,
29175
29189
  DoctorRequestSchema,
29176
- AgentSmokeRequestSchema
29190
+ AgentSmokeRequestSchema,
29191
+ ConfigProposeEditRequestSchema
29177
29192
  ]);
29178
29193
  ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
29179
29194
  ResponseEnvelope = {
@@ -47009,6 +47024,31 @@ async function dispatchTool2(name, args) {
47009
47024
  };
47010
47025
  break;
47011
47026
  }
47027
+ case "config_propose_edit": {
47028
+ if (!args.unified_diff || typeof args.unified_diff !== "string") {
47029
+ return errorText2("config_propose_edit: unified_diff is required (non-empty string).");
47030
+ }
47031
+ if (!args.reason || typeof args.reason !== "string") {
47032
+ return errorText2("config_propose_edit: reason is required (non-empty string, \u2264500 chars).");
47033
+ }
47034
+ if (args.reason.length > 500) {
47035
+ return errorText2("config_propose_edit: reason is capped at 500 chars (RFC \u00a73.3).");
47036
+ }
47037
+ if (args.target_path !== "/state/config/switchroom.yaml") {
47038
+ return errorText2("config_propose_edit: target_path must be '/state/config/switchroom.yaml'.");
47039
+ }
47040
+ req = {
47041
+ v: 1,
47042
+ op: "config_propose_edit",
47043
+ request_id: makeRequestId("mcp-config-propose-edit"),
47044
+ args: {
47045
+ unified_diff: args.unified_diff,
47046
+ reason: args.reason,
47047
+ target_path: "/state/config/switchroom.yaml"
47048
+ }
47049
+ };
47050
+ break;
47051
+ }
47012
47052
  default:
47013
47053
  return errorText2(`unknown tool: ${name}`);
47014
47054
  }
@@ -47219,6 +47259,32 @@ var init_server4 = __esm(() => {
47219
47259
  }
47220
47260
  }
47221
47261
  },
47262
+ {
47263
+ name: "config_propose_edit",
47264
+ description: "Propose a unified-diff patch against /state/config/switchroom.yaml " + "(RFC admin-agent-config-edit). When fully shipped the host validates " + "the patch (applies cleanly + post-patch yaml parses against the " + "config schema) and raises a Telegram approval card in the OPERATOR's " + "primary chat \u2014 NOT yours; the requesting agent's chat is not the " + "approval surface. Admin-only at the wire layer. " + "Current status (PR 1a \u2014 skeleton): the tool is registered but the " + "feature is OFF by default; calling it returns " + "E_CONFIG_EDIT_DISABLED until the operator sets " + "hostd.config_edit_enabled=true in switchroom.yaml. Even when enabled " + "in this PR, the call returns E_NOT_IMPLEMENTED \u2014 the validation " + "pipeline (PR 1b) and apply path (PR 1c) ship in follow-up PRs.",
47265
+ inputSchema: {
47266
+ type: "object",
47267
+ required: ["unified_diff", "reason", "target_path"],
47268
+ properties: {
47269
+ unified_diff: {
47270
+ type: "string",
47271
+ minLength: 1,
47272
+ description: "Unified diff against switchroom.yaml. Must have \u22653 lines " + "context (enforced in PR 1b); no path-traversal or multi-file " + "diffs. LF-only, \u22641 MB."
47273
+ },
47274
+ reason: {
47275
+ type: "string",
47276
+ minLength: 1,
47277
+ maxLength: 500,
47278
+ description: "Human-readable rationale shown to the operator on the " + "approval card. Capped at 500 chars (RFC \u00a73.3)."
47279
+ },
47280
+ target_path: {
47281
+ type: "string",
47282
+ enum: ["/state/config/switchroom.yaml"],
47283
+ description: "Must be the literal string '/state/config/switchroom.yaml'. " + "Future-proofs against multi-file diffs and gives the validator " + "a single canonical path to anchor on."
47284
+ }
47285
+ }
47286
+ }
47287
+ },
47222
47288
  {
47223
47289
  name: "get_status",
47224
47290
  description: "Read the most recent terminal `update_apply` audit row " + "(channel, pin, resolved_sha, install_context, result, " + "exit_code, stderr_tail). Use this after issuing an " + "`update_apply` to confirm what actually rolled out, or to " + "report the last update on demand. Returns the parsed audit " + "entry as JSON.",
@@ -47248,8 +47314,8 @@ var {
47248
47314
  } = import__.default;
47249
47315
 
47250
47316
  // src/build-info.ts
47251
- var VERSION = "0.13.5";
47252
- var COMMIT_SHA = "cb688641";
47317
+ var VERSION = "0.13.8";
47318
+ var COMMIT_SHA = "bb713414";
47253
47319
 
47254
47320
  // src/cli/agent.ts
47255
47321
  init_source();
@@ -48695,35 +48761,37 @@ function buildWorkspaceContext(args) {
48695
48761
  systemPromptAppendShellQuoted: (() => {
48696
48762
  const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(agentConfig);
48697
48763
  const baseAppend = agentConfig.system_prompt_append ?? "";
48698
- const telegramGuidance = `## Progress updates (human-style check-ins)
48699
-
48700
- You're talking to a human colleague on Telegram. Alongside the emoji status
48701
- ladder, send a short \`progress_update\` at inflection points, the moments a
48702
- senior colleague would ping the person who asked them to do something:
48703
-
48704
- - **Plan formed:** "Got it. Going to do X first, then Y, then Z."
48705
- - **Pivot or blocker:** "First approach didn't work because <reason>. Trying
48706
- <alternative> instead."
48707
- - **Chunk finished:** "Done with X. Starting Y now."
48708
-
48709
- Keep them short (one or two sentences). Don't narrate every step, the pinned
48710
- progress card shows that for free. Don't send an update on a trivial one-shot
48711
- task. Send them when a colleague would genuinely want to know what's happening.
48712
-
48713
- Final answers still go through \`stream_reply\` with done=true as usual,
48714
- \`progress_update\` is only for mid-turn check-ins.
48715
-
48716
- ## Think out loud before tool calls
48717
-
48718
- When you're about to call a tool \u2014 especially on the second and later
48719
- tool calls in a turn \u2014 lead the assistant message with one short
48720
- sentence naming what you're doing: "Reading the config.", "Running the
48721
- migration.", "Searching for X." The progress card pairs that sentence
48722
- with the tool as a natural-language step, so the user can tell what's
48723
- happening without decoding raw tool names. Without a preamble the card
48724
- goes quiet during long tool chains and feels stuck. Keep it to one
48725
- line; don't repeat the preamble before every call in a fast sequence,
48726
- but do refresh it when you switch to a genuinely different step.`;
48764
+ const telegramGuidance = `## Talking to a human on Telegram
48765
+
48766
+ There is a real person on the other end. Every turn should feel like
48767
+ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
48768
+
48769
+ 1. **Acknowledge first.** On any turn that needs real work \u2014 a file
48770
+ read, a search, a command \u2014 your FIRST action is a short \`reply\`
48771
+ in your own voice ("on it \u2014 checking now"), before you start. Skip
48772
+ it only when the whole answer is one sentence you can give straight
48773
+ away.
48774
+ 2. **Then go quiet and work.** Heads-down is correct \u2014 do NOT narrate
48775
+ every tool call. A typing indicator runs automatically while you
48776
+ work; you do not maintain it.
48777
+ 3. **Surface meaningful progress** at genuine inflection points \u2014 a
48778
+ hard step finished, a blocker, a pivot, dispatching a sub-agent, a
48779
+ notably slow wait, a finding worth knowing now. One short \`reply\`,
48780
+ \`disable_notification: true\`.
48781
+ 4. **Hand back delegations with synthesis.** When a sub-agent / worker
48782
+ returns, re-enter in YOUR voice \u2014 what it found, and what you are
48783
+ doing next. Never let its raw report stand as your reply.
48784
+ 5. **Deliver the answer** as a final \`reply\`.
48785
+
48786
+ The one thing to avoid is *spam*: a reply on every tool call, on a
48787
+ timer, or repeating what you already said. Responsive and human, never
48788
+ a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
48789
+ acknowledging, or *instead* of an update at a real milestone, is the
48790
+ black box this exists to prevent.
48791
+
48792
+ Every turn that answers a user message ends with a user-visible
48793
+ \`reply\` (or \`stream_reply\` done=true) \u2014 Telegram is all the user
48794
+ sees; your terminal output never reaches them.`;
48727
48795
  const memoryGuidance = `## Memory \u2014 proactive, conversational
48728
48796
 
48729
48797
  You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.
@@ -49417,6 +49485,7 @@ function buildSettingsHooksBlock(p) {
49417
49485
  }
49418
49486
  ] : [];
49419
49487
  const useHotReloadStable = agentConfig.channels?.telegram?.hotReloadStable === true;
49488
+ const turnPacingDirective = "<turn-pacing>You are messaging a human. First action this turn: if " + "answering needs any tool call (a file read, a search, a command), " + "send a SHORT acknowledgement via the reply tool with " + "disable_notification true BEFORE the first tool call. Then work; " + "surface meaningful progress in human prose at real milestones; hand " + "back any sub-agent findings in your own voice; deliver the answer. " + "Skip the opening ack only for a one-sentence answer you can give " + "immediately.</turn-pacing>";
49420
49489
  const switchroomUserPromptSubmit = [
49421
49490
  ...useHotReloadStable ? [
49422
49491
  {
@@ -49446,6 +49515,15 @@ function buildSettingsHooksBlock(p) {
49446
49515
  timeout: 3
49447
49516
  }
49448
49517
  ]
49518
+ },
49519
+ {
49520
+ hooks: [
49521
+ {
49522
+ type: "command",
49523
+ command: wrap("hook:turn-pacing", `printf '%s\\n' ${shellSingleQuote(turnPacingDirective)}`),
49524
+ timeout: 3
49525
+ }
49526
+ ]
49449
49527
  }
49450
49528
  ];
49451
49529
  if (userHooks) {
@@ -49627,35 +49705,37 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
49627
49705
  systemPromptAppendShellQuoted: (() => {
49628
49706
  const useSwitchroomPlugin = usesSwitchroomTelegramPlugin(agentConfig);
49629
49707
  const baseAppend = agentConfig.system_prompt_append ?? "";
49630
- const telegramGuidance = `## Progress updates (human-style check-ins)
49631
-
49632
- You're talking to a human colleague on Telegram. Alongside the emoji status
49633
- ladder, send a short \`progress_update\` at inflection points, the moments a
49634
- senior colleague would ping the person who asked them to do something:
49635
-
49636
- - **Plan formed:** "Got it. Going to do X first, then Y, then Z."
49637
- - **Pivot or blocker:** "First approach didn't work because <reason>. Trying
49638
- <alternative> instead."
49639
- - **Chunk finished:** "Done with X. Starting Y now."
49640
-
49641
- Keep them short (one or two sentences). Don't narrate every step, the pinned
49642
- progress card shows that for free. Don't send an update on a trivial one-shot
49643
- task. Send them when a colleague would genuinely want to know what's happening.
49644
-
49645
- Final answers still go through \`stream_reply\` with done=true as usual,
49646
- \`progress_update\` is only for mid-turn check-ins.
49647
-
49648
- ## Think out loud before tool calls
49649
-
49650
- When you're about to call a tool \u2014 especially on the second and later
49651
- tool calls in a turn \u2014 lead the assistant message with one short
49652
- sentence naming what you're doing: "Reading the config.", "Running the
49653
- migration.", "Searching for X." The progress card pairs that sentence
49654
- with the tool as a natural-language step, so the user can tell what's
49655
- happening without decoding raw tool names. Without a preamble the card
49656
- goes quiet during long tool chains and feels stuck. Keep it to one
49657
- line; don't repeat the preamble before every call in a fast sequence,
49658
- but do refresh it when you switch to a genuinely different step.`;
49708
+ const telegramGuidance = `## Talking to a human on Telegram
49709
+
49710
+ There is a real person on the other end. Every turn should feel like
49711
+ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
49712
+
49713
+ 1. **Acknowledge first.** On any turn that needs real work \u2014 a file
49714
+ read, a search, a command \u2014 your FIRST action is a short \`reply\`
49715
+ in your own voice ("on it \u2014 checking now"), before you start. Skip
49716
+ it only when the whole answer is one sentence you can give straight
49717
+ away.
49718
+ 2. **Then go quiet and work.** Heads-down is correct \u2014 do NOT narrate
49719
+ every tool call. A typing indicator runs automatically while you
49720
+ work; you do not maintain it.
49721
+ 3. **Surface meaningful progress** at genuine inflection points \u2014 a
49722
+ hard step finished, a blocker, a pivot, dispatching a sub-agent, a
49723
+ notably slow wait, a finding worth knowing now. One short \`reply\`,
49724
+ \`disable_notification: true\`.
49725
+ 4. **Hand back delegations with synthesis.** When a sub-agent / worker
49726
+ returns, re-enter in YOUR voice \u2014 what it found, and what you are
49727
+ doing next. Never let its raw report stand as your reply.
49728
+ 5. **Deliver the answer** as a final \`reply\`.
49729
+
49730
+ The one thing to avoid is *spam*: a reply on every tool call, on a
49731
+ timer, or repeating what you already said. Responsive and human, never
49732
+ a flood. Going quiet mid-work is fine \u2014 going quiet *instead* of
49733
+ acknowledging, or *instead* of an update at a real milestone, is the
49734
+ black box this exists to prevent.
49735
+
49736
+ Every turn that answers a user message ends with a user-visible
49737
+ \`reply\` (or \`stream_reply\` done=true) \u2014 Telegram is all the user
49738
+ sees; your terminal output never reaches them.`;
49659
49739
  const memoryGuidance = `## Memory \u2014 proactive, conversational
49660
49740
 
49661
49741
  You have Hindsight tools: \`mcp__hindsight__sync_retain\`, \`mcp__hindsight__delete_memory\`, \`mcp__hindsight__recall\`, \`mcp__hindsight__reflect\`. Use them without being asked.