switchroom 0.13.33 → 0.13.35

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.
@@ -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.35";
47715
+ var COMMIT_SHA = "c41aabe5";
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
  });
@@ -70736,21 +70790,32 @@ init_loader();
70736
70790
  import { resolve as resolve33 } from "node:path";
70737
70791
  function registerHandoffCommand(program3) {
70738
70792
  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
70793
+ let agentConfig;
70794
+ let agentDir;
70795
+ try {
70796
+ const config = getConfig(program3);
70797
+ agentConfig = config.agents[agentName];
70798
+ if (!agentConfig) {
70799
+ process.stderr.write(`handoff: agent "${agentName}" not defined in switchroom.yaml
70743
70800
  `);
70744
- return;
70801
+ return;
70802
+ }
70803
+ const agentsDir = resolveAgentsDir(config);
70804
+ agentDir = resolve33(agentsDir, agentName);
70805
+ } catch (err) {
70806
+ if (!(err instanceof ConfigError))
70807
+ throw err;
70808
+ process.stderr.write(`handoff: yaml unavailable (${err.message}); using defaults
70809
+ `);
70810
+ agentConfig = undefined;
70811
+ agentDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
70745
70812
  }
70746
- const continuity = agentConfig.session_continuity;
70813
+ const continuity = agentConfig?.session_continuity;
70747
70814
  if (continuity?.enabled === false) {
70748
70815
  process.stderr.write(`handoff: session_continuity.enabled=false for "${agentName}"; skipping
70749
70816
  `);
70750
70817
  return;
70751
70818
  }
70752
- const agentsDir = resolveAgentsDir(config);
70753
- const agentDir = resolve33(agentsDir, agentName);
70754
70819
  const claudeConfigDir = resolve33(agentDir, ".claude");
70755
70820
  const jsonl = findLatestSessionJsonl(claudeConfigDir);
70756
70821
  if (!jsonl) {