switchroom 0.13.33 → 0.13.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/timezone-hook.sh +1 -1
  2. package/dist/agent-scheduler/index.js +8 -1
  3. package/dist/auth-broker/index.js +8 -1
  4. package/dist/cli/switchroom.js +176 -26
  5. package/dist/host-control/main.js +5222 -203
  6. package/dist/vault/approvals/kernel-server.js +9 -2
  7. package/dist/vault/broker/server.js +9 -2
  8. package/package.json +1 -1
  9. package/profiles/default/CLAUDE.md.hbs +1 -1
  10. package/telegram-plugin/dist/gateway/gateway.js +234 -31
  11. package/telegram-plugin/docs/waiting-ux-spec.md +40 -0
  12. package/telegram-plugin/gateway/config-approval-handler.test.ts +188 -1
  13. package/telegram-plugin/gateway/config-approval-handler.ts +170 -15
  14. package/telegram-plugin/gateway/diff-preview-card.test.ts +2 -2
  15. package/telegram-plugin/gateway/diff-preview-card.ts +2 -2
  16. package/telegram-plugin/gateway/drive-write-approval.test.ts +70 -0
  17. package/telegram-plugin/gateway/drive-write-approval.ts +51 -2
  18. package/telegram-plugin/gateway/error-envelope-card.ts +64 -0
  19. package/telegram-plugin/gateway/gateway.ts +112 -15
  20. package/telegram-plugin/gateway/ipc-protocol.ts +10 -1
  21. package/telegram-plugin/gateway/oversize-card-body.test.ts +108 -0
  22. package/telegram-plugin/gateway/oversize-card-body.ts +114 -0
  23. package/telegram-plugin/gateway/unhandled-rejection-policy.ts +46 -1
  24. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +118 -41
  25. package/telegram-plugin/hooks/silent-end-scan.mjs +190 -0
  26. package/telegram-plugin/pending-work-progress.ts +37 -1
  27. package/telegram-plugin/tests/boot-clears-clean-shutdown-marker.test.ts +75 -0
  28. package/telegram-plugin/tests/error-envelope-unlock-card.test.ts +79 -0
  29. package/telegram-plugin/tests/pending-work-progress.test.ts +134 -0
  30. package/telegram-plugin/tests/silent-end-integration.test.ts +268 -0
  31. package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +242 -0
  32. package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +314 -0
  33. package/telegram-plugin/tests/silent-end.test.ts +227 -38
  34. package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +51 -6
@@ -45,7 +45,7 @@ fi
45
45
  # `-d "@<unix>"` is required (Linux-only, fine for switchroom production).
46
46
  NOW_UNIX=$(date +%s)
47
47
  ROUNDED=$(( NOW_UNIX - (NOW_UNIX % 900) ))
48
- NOW=$(TZ="$TZ_VAL" date -d "@$ROUNDED" '+%Y-%m-%d %H:%M %Z (UTC%:z)')
48
+ NOW=$(TZ="$TZ_VAL" date -d "@$ROUNDED" '+%A %Y-%m-%d %I:%M %p %Z (UTC%:z)')
49
49
 
50
50
  if [ "$TZ_UNSET" = "1" ]; then
51
51
  MSG="Current local time: $NOW ($TZ_VAL — WARNING: SWITCHROOM_TIMEZONE unset; compose env may be stale, run \`switchroom apply && switchroom agent restart <agent>\` to refresh)"
@@ -11302,8 +11302,15 @@ var QuotaConfigSchema = exports_external.object({
11302
11302
  weekly_budget_usd: exports_external.number().positive().optional().describe("Weekly USD spend budget. If unset, the greeting shows raw usage only."),
11303
11303
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11304
11304
  });
11305
+ var AutoReleaseCheckSchema = exports_external.object({
11306
+ enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false — opt-in."),
11307
+ interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
11308
+ apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
11309
+ 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.")
11310
+ });
11305
11311
  var HostControlConfigSchema = exports_external.object({
11306
- 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).")
11312
+ 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)."),
11313
+ 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.")
11307
11314
  });
11308
11315
  var HostdConfigSchema = exports_external.object({
11309
11316
  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."),
@@ -11302,8 +11302,15 @@ var QuotaConfigSchema = exports_external.object({
11302
11302
  weekly_budget_usd: exports_external.number().positive().optional().describe("Weekly USD spend budget. If unset, the greeting shows raw usage only."),
11303
11303
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
11304
11304
  });
11305
+ var AutoReleaseCheckSchema = exports_external.object({
11306
+ enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false — opt-in."),
11307
+ interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
11308
+ apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
11309
+ 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.")
11310
+ });
11305
11311
  var HostControlConfigSchema = exports_external.object({
11306
- 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).")
11312
+ 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)."),
11313
+ 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.")
11307
11314
  });
11308
11315
  var HostdConfigSchema = exports_external.object({
11309
11316
  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."),
@@ -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, HostdConfigSchema, 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, AutoReleaseCheckSchema, HostControlConfigSchema, HostdConfigSchema, SwitchroomConfigSchema;
13524
13524
  var init_schema = __esm(() => {
13525
13525
  init_zod();
13526
13526
  CodeRepoEntrySchema = exports_external.object({
@@ -13866,8 +13866,15 @@ var init_schema = __esm(() => {
13866
13866
  weekly_budget_usd: exports_external.number().positive().optional().describe("Weekly USD spend budget. If unset, the greeting shows raw usage only."),
13867
13867
  monthly_budget_usd: exports_external.number().positive().optional().describe("Monthly USD spend budget. If unset, the greeting shows raw usage only.")
13868
13868
  });
13869
+ AutoReleaseCheckSchema = exports_external.object({
13870
+ enabled: exports_external.boolean().default(false).describe("When true, hostd polls the remote release tag every " + "`interval_minutes` and applies + restarts the fleet when a new " + "release is detected. Default false \u2014 opt-in."),
13871
+ interval_minutes: exports_external.number().int().min(5).max(1440).default(5).describe("Poll interval in minutes. Floor of 5m matches the agent-config " + "cron rate limit; ceiling of 1440m (24h) is a sanity cap."),
13872
+ apply_on_detect: exports_external.boolean().default(true).describe("When false, hostd logs `release_detected` but does NOT call " + "update_apply / restart all. Useful for dogfooding the detector " + "without rolling the fleet."),
13873
+ 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.")
13874
+ });
13869
13875
  HostControlConfigSchema = exports_external.object({
13870
- 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).")
13876
+ 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)."),
13877
+ 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.")
13871
13878
  });
13872
13879
  HostdConfigSchema = exports_external.object({
13873
13880
  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."),
@@ -29101,7 +29108,7 @@ function decodeResponse3(line) {
29101
29108
  const obj = JSON.parse(line);
29102
29109
  return ResponseSchema3.parse(obj);
29103
29110
  }
29104
- var MAX_FRAME_BYTES3, RequestEnvelope, AgentRestartRequestSchema, UpgradeStatusRequestSchema, GetStatusRequestSchema, AgentNameSchema2, UpdateCheckRequestSchema, UpdateApplyRequestSchema, ApplyRequestSchema, AgentStartRequestSchema, AgentStopRequestSchema, AgentLogsRequestSchema, AgentExecRequestSchema, DoctorRequestSchema, AgentSmokeRequestSchema, ConfigProposeEditRequestSchema, RequestSchema3, ResultSchema, ResponseEnvelope, ResponseSchema3;
29111
+ var MAX_FRAME_BYTES3, RequestEnvelope, AgentRestartRequestSchema, UpgradeStatusRequestSchema, GetStatusRequestSchema, AgentNameSchema2, UpdateCheckRequestSchema, UpdateApplyRequestSchema, ApplyRequestSchema, AgentStartRequestSchema, AgentStopRequestSchema, AgentLogsRequestSchema, AgentExecRequestSchema, DoctorRequestSchema, AgentSmokeRequestSchema, ConfigProposeEditRequestSchema, RequestSchema3, ResultSchema, ErrorFixSchema, ErrorEnvelopeSchema, ResponseEnvelope, ResponseSchema3;
29105
29112
  var init_protocol3 = __esm(() => {
29106
29113
  init_zod();
29107
29114
  MAX_FRAME_BYTES3 = 64 * 1024;
@@ -29220,6 +29227,45 @@ var init_protocol3 = __esm(() => {
29220
29227
  ConfigProposeEditRequestSchema
29221
29228
  ]);
29222
29229
  ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
29230
+ ErrorFixSchema = exports_external.discriminatedUnion("kind", [
29231
+ exports_external.object({
29232
+ kind: exports_external.literal("flip_yaml_flag"),
29233
+ yaml_path: exports_external.string(),
29234
+ to: exports_external.unknown()
29235
+ }),
29236
+ exports_external.object({
29237
+ kind: exports_external.literal("request_vault_grant"),
29238
+ vault_key: exports_external.string()
29239
+ }),
29240
+ exports_external.object({
29241
+ kind: exports_external.literal("operator_action"),
29242
+ subkind: exports_external.enum(["policy_denied", "infra", "out_of_scope"]),
29243
+ operator_steps: exports_external.array(exports_external.string()).min(1).optional()
29244
+ }),
29245
+ exports_external.object({
29246
+ kind: exports_external.literal("retry_after"),
29247
+ retry_at: exports_external.string()
29248
+ }),
29249
+ exports_external.object({
29250
+ kind: exports_external.literal("quota_exceeded"),
29251
+ quota: exports_external.string(),
29252
+ current: exports_external.number(),
29253
+ limit: exports_external.number()
29254
+ }),
29255
+ exports_external.object({
29256
+ kind: exports_external.literal("bad_input"),
29257
+ field: exports_external.string().optional()
29258
+ })
29259
+ ]);
29260
+ ErrorEnvelopeSchema = exports_external.object({
29261
+ v: exports_external.literal(1),
29262
+ code: exports_external.string().regex(/^(E_[A-Z0-9_]+|VAULT-[A-Z-]+)$/),
29263
+ human: exports_external.string().min(1),
29264
+ why: exports_external.string().optional(),
29265
+ fix: ErrorFixSchema.optional(),
29266
+ docs: exports_external.string().url().optional(),
29267
+ request_id: exports_external.string().min(1)
29268
+ });
29223
29269
  ResponseEnvelope = {
29224
29270
  v: exports_external.literal(1),
29225
29271
  request_id: exports_external.string().min(1).max(128),
@@ -29229,7 +29275,8 @@ var init_protocol3 = __esm(() => {
29229
29275
  audit_id: exports_external.string().min(1).optional(),
29230
29276
  stdout_tail: exports_external.string().optional(),
29231
29277
  stderr_tail: exports_external.string().optional(),
29232
- error: exports_external.string().optional()
29278
+ error: exports_external.string().optional(),
29279
+ error_envelope: ErrorEnvelopeSchema.optional()
29233
29280
  };
29234
29281
  ResponseSchema3 = exports_external.object(ResponseEnvelope);
29235
29282
  });
@@ -47403,10 +47450,18 @@ async function dispatchTool2(name, args) {
47403
47450
  if (resp.result === "started" || resp.result === "completed") {
47404
47451
  return jsonText2(resp);
47405
47452
  }
47406
- return {
47407
- content: [{ type: "text", text: JSON.stringify(resp) }],
47408
- isError: true
47409
- };
47453
+ const content = [
47454
+ { type: "text", text: JSON.stringify(resp) }
47455
+ ];
47456
+ if (resp.error_envelope) {
47457
+ const env2 = resp.error_envelope;
47458
+ content.push({
47459
+ type: "text",
47460
+ text: `Structured error \u2014 fix.kind=${env2.fix?.kind ?? "none"}
47461
+ ` + JSON.stringify(env2, null, 2)
47462
+ });
47463
+ }
47464
+ return { content, isError: true };
47410
47465
  }
47411
47466
  function resolveAuditLogPath() {
47412
47467
  if (process.env.HOSTD_AUDIT_LOG_PATH)
@@ -47656,8 +47711,8 @@ var {
47656
47711
  } = import__.default;
47657
47712
 
47658
47713
  // src/build-info.ts
47659
- var VERSION = "0.13.33";
47660
- var COMMIT_SHA = "a8018071";
47714
+ var VERSION = "0.13.36";
47715
+ var COMMIT_SHA = "73e8bb05";
47661
47716
 
47662
47717
  // src/cli/agent.ts
47663
47718
  init_source();
@@ -49707,17 +49762,16 @@ function classifyChange(path, agentDir, useHotReloadStable) {
49707
49762
  return "stale-till-restart";
49708
49763
  }
49709
49764
  function buildSettingsHooksBlock(p) {
49710
- const { agentName, agentConfig, hindsightEnabled, useSwitchroomPlugin, configPath } = p;
49765
+ const { agentName, agentConfig, hindsightEnabled, useSwitchroomPlugin } = p;
49711
49766
  const userHooks = translateHooksToClaudeShape(agentConfig.hooks);
49712
49767
  const wrapper = `bash "${join8(DOCKER_BIN_PATH, "run-hook.sh")}"`;
49713
49768
  const wrap = (source, command) => `${wrapper} ${shellSingleQuote(source)} ${command}`;
49714
49769
  const handoffEnabled = agentConfig.session_continuity?.enabled !== false;
49715
- const handoffConfigArg = configPath ? ` --config ${shellSingleQuote(DOCKER_CONFIG_PATH)}` : "";
49716
49770
  const stopHooks = [];
49717
49771
  if (handoffEnabled) {
49718
49772
  stopHooks.push({
49719
49773
  type: "command",
49720
- command: wrap("hook:handoff", `switchroom${handoffConfigArg} handoff ${agentName}`),
49774
+ command: wrap("hook:handoff", `switchroom handoff ${agentName}`),
49721
49775
  timeout: 35,
49722
49776
  async: true
49723
49777
  });
@@ -61526,6 +61580,23 @@ function registerVaultBackupCommand(vault, program3) {
61526
61580
  });
61527
61581
  }
61528
61582
 
61583
+ // src/cli/vault-denied-envelope.ts
61584
+ var ENVELOPE_SENTINEL = "ERROR-ENVELOPE:";
61585
+ function writeVaultDeniedEnvelope(vaultKey, brokerCode, human) {
61586
+ const envelope = {
61587
+ v: 1,
61588
+ code: "VAULT-BROKER-DENIED",
61589
+ human: `${brokerCode}: ${human}`,
61590
+ fix: {
61591
+ kind: "request_vault_grant",
61592
+ vault_key: vaultKey
61593
+ },
61594
+ request_id: `vault-cli-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`
61595
+ };
61596
+ process.stderr.write(`${ENVELOPE_SENTINEL} ${JSON.stringify(envelope)}
61597
+ `);
61598
+ }
61599
+
61529
61600
  // src/cli/vault.ts
61530
61601
  function isSandboxContext() {
61531
61602
  return process.env.SWITCHROOM_RUNTIME === "docker";
@@ -61751,6 +61822,7 @@ function registerVaultCommand(program3) {
61751
61822
  }
61752
61823
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
61753
61824
  `);
61825
+ writeVaultDeniedEnvelope(key, result.code, result.msg);
61754
61826
  process.exit(VAULT_EXIT_DENIED);
61755
61827
  }
61756
61828
  if (inSandbox) {
@@ -61966,6 +62038,7 @@ Push passphrase to broker for future requests? [Y/n]: `);
61966
62038
  process.stderr.write(`VAULT-BROKER-DENIED [${result.code}]: ${result.msg}
61967
62039
  ` + `${recoveryHint("denied", key)}
61968
62040
  `);
62041
+ writeVaultDeniedEnvelope(key, result.code, result.msg);
61969
62042
  process.exit(2);
61970
62043
  }
61971
62044
  } else {
@@ -70736,21 +70809,32 @@ init_loader();
70736
70809
  import { resolve as resolve33 } from "node:path";
70737
70810
  function registerHandoffCommand(program3) {
70738
70811
  program3.command("handoff <agent>", { hidden: true }).description("Build the agent's session handoff sidecars \u2014 a transcript-tail " + "briefing (.handoff.md) and topic line (.handoff-topic). " + "[internal \u2014 used by the Stop hook]").option("--max-turns <n>", "Max turns kept in the handoff transcript tail", String(DEFAULT_MAX_TURNS)).action(withConfigError(async (agentName, opts) => {
70739
- const config = getConfig(program3);
70740
- const agentConfig = config.agents[agentName];
70741
- if (!agentConfig) {
70742
- process.stderr.write(`handoff: agent "${agentName}" not defined in switchroom.yaml
70812
+ let agentConfig;
70813
+ let agentDir;
70814
+ try {
70815
+ const config = getConfig(program3);
70816
+ agentConfig = config.agents[agentName];
70817
+ if (!agentConfig) {
70818
+ process.stderr.write(`handoff: agent "${agentName}" not defined in switchroom.yaml
70743
70819
  `);
70744
- return;
70820
+ return;
70821
+ }
70822
+ const agentsDir = resolveAgentsDir(config);
70823
+ agentDir = resolve33(agentsDir, agentName);
70824
+ } catch (err) {
70825
+ if (!(err instanceof ConfigError))
70826
+ throw err;
70827
+ process.stderr.write(`handoff: yaml unavailable (${err.message}); using defaults
70828
+ `);
70829
+ agentConfig = undefined;
70830
+ agentDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
70745
70831
  }
70746
- const continuity = agentConfig.session_continuity;
70832
+ const continuity = agentConfig?.session_continuity;
70747
70833
  if (continuity?.enabled === false) {
70748
70834
  process.stderr.write(`handoff: session_continuity.enabled=false for "${agentName}"; skipping
70749
70835
  `);
70750
70836
  return;
70751
70837
  }
70752
- const agentsDir = resolveAgentsDir(config);
70753
- const agentDir = resolve33(agentsDir, agentName);
70754
70838
  const claudeConfigDir = resolve33(agentDir, ".claude");
70755
70839
  const jsonl = findLatestSessionJsonl(claudeConfigDir);
70756
70840
  if (!jsonl) {
@@ -76133,7 +76217,34 @@ function denyPendingScheduleEntry(opts) {
76133
76217
 
76134
76218
  // src/cli/agent-config-write.ts
76135
76219
  import { existsSync as existsSync73, readFileSync as readFileSync59 } from "node:fs";
76220
+ import { randomUUID as randomUUID5 } from "node:crypto";
76136
76221
  var MAX_ENTRIES_PER_AGENT = 20;
76222
+ var MIN_CRON_INTERVAL_MIN = 5;
76223
+ function extractCronSmallestGapMin(expr) {
76224
+ const fields = expr.trim().split(/\s+/);
76225
+ if (fields.length < 5)
76226
+ return 0;
76227
+ const min = fields[0];
76228
+ if (min === "*")
76229
+ return 1;
76230
+ const step = min.match(/^\*\/(\d+)$/);
76231
+ if (step)
76232
+ return Number(step[1]);
76233
+ if (min.includes(",")) {
76234
+ const parts = min.split(",").map((s) => Number(s)).filter((n) => Number.isFinite(n));
76235
+ if (parts.length >= 2) {
76236
+ const sorted = [...parts].sort((a, b) => a - b);
76237
+ let smallest = Infinity;
76238
+ for (let i = 1;i < sorted.length; i++) {
76239
+ const gap = sorted[i] - sorted[i - 1];
76240
+ if (gap > 0 && gap < smallest)
76241
+ smallest = gap;
76242
+ }
76243
+ return Number.isFinite(smallest) ? smallest : 0;
76244
+ }
76245
+ }
76246
+ return 0;
76247
+ }
76137
76248
  function checkOperatorContext(verb, env2 = process.env) {
76138
76249
  if (env2.SWITCHROOM_OPERATOR === "1")
76139
76250
  return { ok: true };
@@ -76146,8 +76257,45 @@ function checkOperatorContext(verb, env2 = process.env) {
76146
76257
  }
76147
76258
  return { ok: true };
76148
76259
  }
76260
+ function buildEnvelopeForCode(code, message, extra) {
76261
+ const request_id = `agent-config-${randomUUID5()}`;
76262
+ if (code === "E_CRON_TOO_FREQUENT") {
76263
+ return {
76264
+ v: 1,
76265
+ code,
76266
+ human: message,
76267
+ fix: {
76268
+ kind: "quota_exceeded",
76269
+ quota: "cron_min_interval_minutes",
76270
+ current: typeof extra.requested_interval_min === "number" ? extra.requested_interval_min : 0,
76271
+ limit: MIN_CRON_INTERVAL_MIN
76272
+ },
76273
+ request_id
76274
+ };
76275
+ }
76276
+ if (code === "E_QUOTA_EXCEEDED") {
76277
+ const current = typeof extra.current === "number" ? extra.current : MAX_ENTRIES_PER_AGENT;
76278
+ return {
76279
+ v: 1,
76280
+ code,
76281
+ human: message,
76282
+ fix: {
76283
+ kind: "quota_exceeded",
76284
+ quota: "schedule_entries_per_agent",
76285
+ current,
76286
+ limit: MAX_ENTRIES_PER_AGENT
76287
+ },
76288
+ request_id
76289
+ };
76290
+ }
76291
+ return;
76292
+ }
76149
76293
  function emitError(code, message, extra = {}) {
76150
- process.stderr.write(JSON.stringify({ code, message, ...extra }) + `
76294
+ const error_envelope = buildEnvelopeForCode(code, message, extra);
76295
+ const line = { code, message, ...extra };
76296
+ if (error_envelope)
76297
+ line.error_envelope = error_envelope;
76298
+ process.stderr.write(JSON.stringify(line) + `
76151
76299
  `);
76152
76300
  }
76153
76301
  function exitCodeFor(code) {
@@ -76211,7 +76359,8 @@ function scheduleAdd(opts) {
76211
76359
  ok: false,
76212
76360
  code: "E_CRON_TOO_FREQUENT",
76213
76361
  message: "cron interval is tighter than the minimum (5 minutes)",
76214
- exit: 9
76362
+ exit: 9,
76363
+ meta: { requested_interval_min: extractCronSmallestGapMin(opts.cronExpr) }
76215
76364
  };
76216
76365
  }
76217
76366
  const rej = filterOverlaySecrets(dry.doc, "overlay");
@@ -76229,7 +76378,8 @@ function scheduleAdd(opts) {
76229
76378
  ok: false,
76230
76379
  code: "E_QUOTA_EXCEEDED",
76231
76380
  message: `agent already has ${existing.length} overlay entries (max ${MAX_ENTRIES_PER_AGENT})`,
76232
- exit: 9
76381
+ exit: 9,
76382
+ meta: { current: existing.length }
76233
76383
  };
76234
76384
  }
76235
76385
  const hash2 = cronUnitHash(opts.cronExpr, opts.prompt);
@@ -76428,7 +76578,7 @@ function registerAgentConfigWriteCommands(program3) {
76428
76578
  else if (process.env.SWITCHROOM_AGENT_NAME)
76429
76579
  resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
76430
76580
  if (!r.ok) {
76431
- emitError(r.code, r.message);
76581
+ emitError(r.code, r.message, r.meta ?? {});
76432
76582
  appendAudit(resolvedAgent, "schedule.add", { cron: opts.cron, prompt: opts.prompt, name: opts.name, code: r.code, would_recreate: false }, r.exit);
76433
76583
  process.exit(r.exit);
76434
76584
  }
@@ -76541,7 +76691,7 @@ function registerAgentConfigWriteCommands(program3) {
76541
76691
  else if (process.env.SWITCHROOM_AGENT_NAME)
76542
76692
  resolvedAgent = process.env.SWITCHROOM_AGENT_NAME;
76543
76693
  if (!r.ok) {
76544
- emitError(r.code, r.message);
76694
+ emitError(r.code, r.message, r.meta ?? {});
76545
76695
  appendAudit(resolvedAgent, "schedule.remove", { ...opts, code: r.code }, r.exit);
76546
76696
  process.exit(r.exit);
76547
76697
  }