switchroom 0.12.0 → 0.12.1

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 (29) hide show
  1. package/README.md +26 -11
  2. package/dist/auth-broker/index.js +1 -1
  3. package/dist/cli/skill-validate-pretool.mjs +7209 -0
  4. package/dist/cli/switchroom.js +869 -430
  5. package/dist/vault/broker/server.js +31 -22
  6. package/package.json +3 -2
  7. package/profiles/_shared/agent-self-service.md.hbs +1 -1
  8. package/skills/skill-creator/SKILL.md +52 -0
  9. package/telegram-plugin/auth-snapshot-format.ts +5 -5
  10. package/telegram-plugin/dist/gateway/gateway.js +62 -8
  11. package/telegram-plugin/gateway/access-validator.test.ts +8 -8
  12. package/telegram-plugin/gateway/access-validator.ts +1 -1
  13. package/telegram-plugin/gateway/boot-probes.ts +43 -3
  14. package/telegram-plugin/gateway/gateway.ts +72 -0
  15. package/telegram-plugin/recent-outbound-dedup.ts +1 -1
  16. package/telegram-plugin/registry/turns-schema.ts +1 -1
  17. package/telegram-plugin/tests/auth-add-flow.test.ts +1 -1
  18. package/telegram-plugin/tests/auth-command-format2.test.ts +4 -4
  19. package/telegram-plugin/tests/auth-snapshot-format.test.ts +17 -17
  20. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +10 -10
  21. package/telegram-plugin/tests/boot-probes.test.ts +37 -2
  22. package/telegram-plugin/tests/fixtures/service-log-current-claude-code.bin +1 -1
  23. package/telegram-plugin/tests/fleet-state.test.ts +3 -2
  24. package/telegram-plugin/tests/secret-detect-audit.test.ts +1 -1
  25. package/telegram-plugin/tests/secret-detect-pipeline.test.ts +7 -6
  26. package/telegram-plugin/tests/secret-detect-suppressor-no-silent-allow.test.ts +6 -5
  27. package/telegram-plugin/tests/secret-detect.test.ts +8 -8
  28. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +8 -8
  29. package/telegram-plugin/tests/vault-request-access-tool.test.ts +51 -0
@@ -21988,6 +21988,7 @@ __export(exports_client, {
21988
21988
  listGrantsViaBroker: () => listGrantsViaBroker,
21989
21989
  getViaBrokerStructured: () => getViaBrokerStructured,
21990
21990
  getViaBroker: () => getViaBroker,
21991
+ defaultBrokerSocketPath: () => defaultBrokerSocketPath,
21991
21992
  createBrokerClient: () => createBrokerClient,
21992
21993
  brokerIsComposeManaged: () => brokerIsComposeManaged,
21993
21994
  VaultTokenRejectedError: () => VaultTokenRejectedError
@@ -22252,7 +22253,7 @@ async function lockViaBroker(opts) {
22252
22253
  async function unlockViaBroker(passphrase, opts) {
22253
22254
  const dataSocketPath = resolveBrokerSocketPath(opts);
22254
22255
  const unlockSocketPath = unlockSocketFor(dataSocketPath);
22255
- const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
22256
+ const timeoutMs = opts?.timeoutMs ?? UNLOCK_TIMEOUT_MS;
22256
22257
  return new Promise((resolve8) => {
22257
22258
  let settled = false;
22258
22259
  const settle = (val) => {
@@ -22354,7 +22355,7 @@ async function revokeGrantViaBroker(id, opts) {
22354
22355
  return { kind: "error", msg: resp.msg };
22355
22356
  return { kind: "error", msg: "unexpected broker response" };
22356
22357
  }
22357
- var DEFAULT_TIMEOUT_MS = 2000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH, VaultTokenRejectedError;
22358
+ var DEFAULT_TIMEOUT_MS = 2000, UNLOCK_TIMEOUT_MS = 30000, LEGACY_SOCKET_PATH, OPERATOR_SOCKET_PATH, VaultTokenRejectedError;
22358
22359
  var init_client = __esm(() => {
22359
22360
  init_protocol();
22360
22361
  init_peercred();
@@ -23085,6 +23086,8 @@ function emitAgentService(lines, a, imageTag, buildMode, buildContext, homePrefi
23085
23086
  NPM_CONFIG_PREFIX: "/state/agent/home/.npm-global",
23086
23087
  PIP_BREAK_SYSTEM_PACKAGES: "1",
23087
23088
  PIP_USER: "1",
23089
+ DISABLE_AUTOUPDATER: "1",
23090
+ CLAUDE_CODE_ATTRIBUTION_HEADER: "0",
23088
23091
  SWITCHROOM_AGENT_NAME: a.name,
23089
23092
  SWITCHROOM_CONTAINER: "1",
23090
23093
  SWITCHROOM_VAULT_BROKER_SOCK: `/run/switchroom/broker/sock`,
@@ -27070,6 +27073,151 @@ var init_via_claude = __esm(() => {
27070
27073
  ];
27071
27074
  });
27072
27075
 
27076
+ // src/vault/broker/acl.ts
27077
+ function parseCronUnit(unitName) {
27078
+ const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
27079
+ if (!m)
27080
+ return null;
27081
+ const agentName = m[1];
27082
+ const index = parseInt(m[2], 10);
27083
+ if (!agentName)
27084
+ return null;
27085
+ return { agentName, index };
27086
+ }
27087
+ function agentSlugFromPeer(peer) {
27088
+ if (peer.systemdUnit === null)
27089
+ return null;
27090
+ const parsed = parseCronUnit(peer.systemdUnit);
27091
+ return parsed?.agentName ?? null;
27092
+ }
27093
+ function checkEntryScope(scope, agentSlug) {
27094
+ if (scope === undefined || scope === null) {
27095
+ return { allow: true };
27096
+ }
27097
+ const deny = scope.deny ?? [];
27098
+ const allow = scope.allow ?? [];
27099
+ if (agentSlug !== null && deny.includes(agentSlug)) {
27100
+ return {
27101
+ allow: false,
27102
+ reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
27103
+ };
27104
+ }
27105
+ if (allow.length > 0) {
27106
+ if (agentSlug === null || !allow.includes(agentSlug)) {
27107
+ return {
27108
+ allow: false,
27109
+ reason: agentSlug === null ? "caller agent slug could not be determined; entry has a non-empty allow list (scope-allow)" : `agent '${agentSlug}' is not in the entry's allow list (scope-allow)`
27110
+ };
27111
+ }
27112
+ }
27113
+ return { allow: true };
27114
+ }
27115
+ function checkAcl(peer, config, key) {
27116
+ if (peer.systemdUnit !== null) {
27117
+ const parsed = parseCronUnit(peer.systemdUnit);
27118
+ if (parsed === null) {
27119
+ return {
27120
+ allow: false,
27121
+ reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
27122
+ };
27123
+ }
27124
+ const { agentName, index } = parsed;
27125
+ const agentConfig = config.agents?.[agentName];
27126
+ if (!agentConfig) {
27127
+ return { allow: false, reason: `agent '${agentName}' not found in config` };
27128
+ }
27129
+ const schedule = agentConfig.schedule ?? [];
27130
+ if (index >= schedule.length || index < 0) {
27131
+ return {
27132
+ allow: false,
27133
+ reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
27134
+ };
27135
+ }
27136
+ const entry = schedule[index];
27137
+ const allowedKeys = entry.secrets ?? [];
27138
+ if (!allowedKeys.includes(key)) {
27139
+ return {
27140
+ allow: false,
27141
+ reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
27142
+ };
27143
+ }
27144
+ return { allow: true };
27145
+ }
27146
+ return {
27147
+ allow: false,
27148
+ reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
27149
+ };
27150
+ }
27151
+ function checkAclByAgent(config, agentName, key) {
27152
+ if (!agentName) {
27153
+ return { allow: false, reason: "agent name unresolved" };
27154
+ }
27155
+ const agentConfig = config.agents?.[agentName];
27156
+ if (!agentConfig) {
27157
+ return { allow: false, reason: `agent '${agentName}' not found in config` };
27158
+ }
27159
+ const googleSlot = parseGoogleAccountSlotKey(key);
27160
+ if (googleSlot !== null) {
27161
+ return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
27162
+ }
27163
+ const agentBot = agentConfig.bot_token;
27164
+ const botRef = agentBot && agentBot.length > 0 ? agentBot : config.telegram?.bot_token;
27165
+ if (typeof botRef === "string" && botRef.startsWith("vault:")) {
27166
+ const botKey = botRef.slice("vault:".length).split("#")[0];
27167
+ if (botKey.length > 0 && botKey === key) {
27168
+ return { allow: true };
27169
+ }
27170
+ }
27171
+ const schedule = agentConfig.schedule ?? [];
27172
+ if (schedule.length === 0) {
27173
+ return {
27174
+ allow: false,
27175
+ reason: `agent '${agentName}' has no schedule entries declaring 'secrets'; nothing is broker-accessible`
27176
+ };
27177
+ }
27178
+ for (const entry of schedule) {
27179
+ const allowed = entry?.secrets ?? [];
27180
+ if (allowed.includes(key)) {
27181
+ return { allow: true };
27182
+ }
27183
+ }
27184
+ return {
27185
+ allow: false,
27186
+ reason: `key '${key}' not in ACL for agent '${agentName}'`
27187
+ };
27188
+ }
27189
+ function parseGoogleAccountSlotKey(key) {
27190
+ const match = key.match(/^google:([^:]+):([a-z_]+)$/);
27191
+ if (!match)
27192
+ return null;
27193
+ return { account: match[1], field: match[2] };
27194
+ }
27195
+ function checkGoogleAccountAcl(config, agentName, account, key) {
27196
+ const accounts = config.google_accounts ?? {};
27197
+ const accountKey = account.toLowerCase();
27198
+ const accountEntry = accounts[accountKey] ?? accounts[account];
27199
+ if (!accountEntry) {
27200
+ return {
27201
+ allow: false,
27202
+ reason: `google_accounts['${account}'] not configured (key '${key}')`
27203
+ };
27204
+ }
27205
+ const enabled = accountEntry.enabled_for ?? [];
27206
+ if (enabled.length === 0) {
27207
+ return {
27208
+ allow: false,
27209
+ reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
27210
+ };
27211
+ }
27212
+ if (!enabled.includes(agentName)) {
27213
+ return {
27214
+ allow: false,
27215
+ reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
27216
+ };
27217
+ }
27218
+ return { allow: true };
27219
+ }
27220
+
27073
27221
  // src/util/audit-hashchain.ts
27074
27222
  import { createHash as createHash6 } from "node:crypto";
27075
27223
  import { openSync as openSync7, readSync as readSync2, fstatSync as fstatSync2, closeSync as closeSync7 } from "node:fs";
@@ -27916,6 +28064,108 @@ var init_doctor_auth_broker = __esm(() => {
27916
28064
  init_paths();
27917
28065
  });
27918
28066
 
28067
+ // src/cli/doctor-hostd.ts
28068
+ import { spawnSync as spawnSync6 } from "node:child_process";
28069
+ function realDockerInspect(ref, format) {
28070
+ try {
28071
+ const r = spawnSync6("docker", ["inspect", ref, "--format", format], {
28072
+ encoding: "utf-8",
28073
+ timeout: 5000
28074
+ });
28075
+ if (r.status !== 0)
28076
+ return null;
28077
+ const out = (r.stdout ?? "").trim();
28078
+ return out.length > 0 ? out : null;
28079
+ } catch {
28080
+ return null;
28081
+ }
28082
+ }
28083
+ function imageCreatedAt(container, dockerInspect) {
28084
+ const imageId = dockerInspect(container, "{{.Image}}");
28085
+ if (!imageId)
28086
+ return null;
28087
+ return dockerInspect(imageId, "{{.Created}}");
28088
+ }
28089
+ function runHostdChecks(config, deps = {}) {
28090
+ const dockerInspect = deps.dockerInspect ?? realDockerInspect;
28091
+ const enabled = config.host_control?.enabled === true;
28092
+ if (!enabled) {
28093
+ return [
28094
+ {
28095
+ name: "hostd: configured",
28096
+ status: "warn",
28097
+ detail: "host_control.enabled is not true \u2014 /restart and /update apply " + "use the legacy in-agent fallback, which fails on docker installs (#926)",
28098
+ fix: "Set `host_control: { enabled: true }` in switchroom.yaml and run `switchroom hostd install`"
28099
+ }
28100
+ ];
28101
+ }
28102
+ const results = [
28103
+ {
28104
+ name: "hostd: configured",
28105
+ status: "ok",
28106
+ detail: "host_control.enabled: true"
28107
+ }
28108
+ ];
28109
+ const status = dockerInspect(HOSTD_CONTAINER, "{{.State.Status}}");
28110
+ if (status !== "running") {
28111
+ results.push({
28112
+ name: "hostd: running",
28113
+ status: "fail",
28114
+ detail: status === null ? `${HOSTD_CONTAINER} container not found` : `${HOSTD_CONTAINER} is ${status}, not running`,
28115
+ fix: "Run `switchroom hostd install` on the host to (re)create the daemon"
28116
+ });
28117
+ return results;
28118
+ }
28119
+ results.push({
28120
+ name: "hostd: running",
28121
+ status: "ok",
28122
+ detail: `${HOSTD_CONTAINER} running`
28123
+ });
28124
+ const hostdCreated = imageCreatedAt(HOSTD_CONTAINER, dockerInspect);
28125
+ let agentCreated = null;
28126
+ for (const name of Object.keys(config.agents ?? {})) {
28127
+ agentCreated = imageCreatedAt(`switchroom-${name}`, dockerInspect);
28128
+ if (agentCreated)
28129
+ break;
28130
+ }
28131
+ if (!hostdCreated || !agentCreated) {
28132
+ results.push({
28133
+ name: "hostd: image drift",
28134
+ status: "ok",
28135
+ detail: "skipped \u2014 no running agent image to compare against"
28136
+ });
28137
+ return results;
28138
+ }
28139
+ const hostdMs = Date.parse(hostdCreated);
28140
+ const agentMs = Date.parse(agentCreated);
28141
+ if (Number.isNaN(hostdMs) || Number.isNaN(agentMs)) {
28142
+ results.push({
28143
+ name: "hostd: image drift",
28144
+ status: "ok",
28145
+ detail: "skipped \u2014 unparseable image timestamps"
28146
+ });
28147
+ return results;
28148
+ }
28149
+ const lagHours = (agentMs - hostdMs) / 3600000;
28150
+ if (lagHours > HOSTD_DRIFT_HOURS) {
28151
+ results.push({
28152
+ name: "hostd: image drift",
28153
+ status: "warn",
28154
+ detail: `hostd image is ~${lagHours.toFixed(1)}h older than the agent fleet ` + `image \u2014 likely a \`switchroom update --skip-images\` left it behind ` + `(that flag skips the refresh-hostd step)`,
28155
+ fix: "Run `switchroom hostd install` (or `switchroom update` without `--skip-images`)"
28156
+ });
28157
+ } else {
28158
+ results.push({
28159
+ name: "hostd: image drift",
28160
+ status: "ok",
28161
+ detail: `in sync with the agent fleet image (\u00b1${HOSTD_DRIFT_HOURS}h)`
28162
+ });
28163
+ }
28164
+ return results;
28165
+ }
28166
+ var HOSTD_CONTAINER = "switchroom-hostd", HOSTD_DRIFT_HOURS = 2;
28167
+ var init_doctor_hostd = () => {};
28168
+
27919
28169
  // src/cli/doctor-drive.ts
27920
28170
  import {
27921
28171
  existsSync as realExistsSync,
@@ -28205,6 +28455,181 @@ function runCredentialsMigrationChecks(config, deps = {}) {
28205
28455
  }
28206
28456
  var init_doctor_credentials_migration = () => {};
28207
28457
 
28458
+ // src/cli/doctor-secret-access.ts
28459
+ import {
28460
+ accessSync,
28461
+ constants as fsConstants4,
28462
+ existsSync as existsSync46,
28463
+ realpathSync as realpathSync4,
28464
+ statSync as statSync20
28465
+ } from "node:fs";
28466
+ import { userInfo } from "node:os";
28467
+ function resolveVaultPath2(config) {
28468
+ return config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
28469
+ }
28470
+ function defaultStatVault(path4) {
28471
+ if (!existsSync46(path4)) {
28472
+ return { exists: false, readable: false, uid: -1, mode: 0, realPath: path4 };
28473
+ }
28474
+ let real = path4;
28475
+ try {
28476
+ real = realpathSync4(path4);
28477
+ } catch {}
28478
+ let uid = -1;
28479
+ let mode = 0;
28480
+ try {
28481
+ const s = statSync20(real);
28482
+ uid = s.uid;
28483
+ mode = s.mode & 511;
28484
+ } catch {
28485
+ return { exists: true, readable: false, uid, mode, realPath: real };
28486
+ }
28487
+ let readable = false;
28488
+ try {
28489
+ accessSync(real, fsConstants4.R_OK);
28490
+ readable = true;
28491
+ } catch {
28492
+ readable = false;
28493
+ }
28494
+ return { exists: true, readable, uid, mode, realPath: real };
28495
+ }
28496
+ function collectVaultRefs2(value, out) {
28497
+ if (typeof value === "string") {
28498
+ if (value.startsWith("vault:")) {
28499
+ const key = value.slice("vault:".length).split("#")[0].trim();
28500
+ if (key)
28501
+ out.add(key);
28502
+ }
28503
+ return;
28504
+ }
28505
+ if (Array.isArray(value)) {
28506
+ for (const v of value)
28507
+ collectVaultRefs2(v, out);
28508
+ return;
28509
+ }
28510
+ if (value && typeof value === "object") {
28511
+ for (const v of Object.values(value)) {
28512
+ collectVaultRefs2(v, out);
28513
+ }
28514
+ }
28515
+ }
28516
+ function runSecretAccessChecks(config, deps = {}) {
28517
+ const results = [];
28518
+ const vaultPath = deps.vaultPath ?? resolveVaultPath2(config);
28519
+ const statVault = deps.statVault ?? defaultStatVault;
28520
+ const selfUid = deps.selfUid ?? (typeof process.getuid === "function" ? process.getuid() : -1);
28521
+ let selfUser = deps.selfUser;
28522
+ if (selfUser === undefined) {
28523
+ try {
28524
+ selfUser = userInfo().username;
28525
+ } catch {
28526
+ selfUser = "<you>";
28527
+ }
28528
+ }
28529
+ const vf = statVault(vaultPath);
28530
+ if (!vf.exists) {
28531
+ results.push({
28532
+ name: "vault: operator readable",
28533
+ status: "ok",
28534
+ detail: `vault file not present at ${vaultPath} \u2014 see the Vault section`
28535
+ });
28536
+ } else if (!vf.readable) {
28537
+ results.push({
28538
+ name: "vault: operator readable",
28539
+ status: "fail",
28540
+ detail: `${vf.realPath} is owned by uid ${vf.uid} (mode 0${vf.mode.toString(8)}) \u2014 ` + `the operator (uid ${selfUid} ${selfUser}) cannot read it, so every ` + `\`switchroom vault \u2026\` fails. The broker still works (CAP_DAC_READ_SEARCH), ` + `which masks this until you touch the vault directly.`,
28541
+ fix: `sudo chown ${selfUser}:${selfUser} ${vf.realPath}`
28542
+ });
28543
+ } else {
28544
+ results.push({
28545
+ name: "vault: operator readable",
28546
+ status: "ok",
28547
+ detail: `operator can read ${vf.realPath}`
28548
+ });
28549
+ }
28550
+ const passphrase = deps.passphrase ?? process.env.SWITCHROOM_VAULT_PASSPHRASE;
28551
+ if (!passphrase) {
28552
+ results.push({
28553
+ name: "agent secret access",
28554
+ status: "warn",
28555
+ detail: "SWITCHROOM_VAULT_PASSPHRASE not set \u2014 cannot enumerate vault keys/ACLs " + "to verify per-agent secret access",
28556
+ fix: "Export SWITCHROOM_VAULT_PASSPHRASE and re-run `switchroom doctor`"
28557
+ });
28558
+ return results;
28559
+ }
28560
+ let entries;
28561
+ try {
28562
+ entries = (deps.openVault ?? openVault)(passphrase, vaultPath);
28563
+ } catch (err) {
28564
+ results.push({
28565
+ name: "agent secret access",
28566
+ status: vf.readable ? "fail" : "warn",
28567
+ detail: `cannot open the vault: ${err.message}`,
28568
+ fix: vf.readable ? "SWITCHROOM_VAULT_PASSPHRASE may be wrong, or the vault is corrupt" : "fix the vault file ownership above first (operator cannot read it)"
28569
+ });
28570
+ return results;
28571
+ }
28572
+ const agents = Object.keys(config.agents ?? {});
28573
+ for (const name of agents) {
28574
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, config.agents[name]);
28575
+ const cronKeys = new Set;
28576
+ for (const entry of resolved.schedule ?? []) {
28577
+ for (const s of entry.secrets ?? [])
28578
+ cronKeys.add(s);
28579
+ }
28580
+ const refKeys = new Set;
28581
+ collectVaultRefs2(resolved, refKeys);
28582
+ const needed = new Set([...cronKeys, ...refKeys]);
28583
+ if (needed.size === 0) {
28584
+ results.push({
28585
+ name: `secret access: ${name}`,
28586
+ status: "ok",
28587
+ detail: "no declared vault secrets"
28588
+ });
28589
+ continue;
28590
+ }
28591
+ const gaps = [];
28592
+ for (const key of [...needed].sort()) {
28593
+ const isGoogleSlot = key.startsWith("google:");
28594
+ if (!isGoogleSlot && !(key in entries)) {
28595
+ gaps.push(`'${key}' missing from the vault`);
28596
+ continue;
28597
+ }
28598
+ const byScope = checkEntryScope(entries[key]?.scope, name);
28599
+ if (cronKeys.has(key)) {
28600
+ const byAgent = checkAclByAgent(config, name, key);
28601
+ if (!byAgent.allow) {
28602
+ gaps.push(`'${key}' (cron) \u2014 no static ACL grants read (${byAgent.reason})`);
28603
+ continue;
28604
+ }
28605
+ }
28606
+ if (!byScope.allow) {
28607
+ gaps.push(`'${key}' \u2014 per-key scope denies read (${byScope.reason})`);
28608
+ }
28609
+ }
28610
+ if (gaps.length === 0) {
28611
+ results.push({
28612
+ name: `secret access: ${name}`,
28613
+ status: "ok",
28614
+ detail: `${needed.size} secret(s): all present + ACL ok`
28615
+ });
28616
+ } else {
28617
+ results.push({
28618
+ name: `secret access: ${name}`,
28619
+ status: "fail",
28620
+ detail: `${gaps.length}/${needed.size} unreachable \u2014 ${gaps.join("; ")}`,
28621
+ fix: "`switchroom vault set <key>` for missing keys; " + "`switchroom vault set <key> --allow " + name + "` to grant this agent read access"
28622
+ });
28623
+ }
28624
+ }
28625
+ return results;
28626
+ }
28627
+ var init_doctor_secret_access = __esm(() => {
28628
+ init_paths();
28629
+ init_merge();
28630
+ init_vault();
28631
+ });
28632
+
28208
28633
  // src/cli/doctor-inlined-secrets.ts
28209
28634
  import { readFileSync as fsReadFileSync } from "node:fs";
28210
28635
  function isSecretShapedKey(key) {
@@ -28394,7 +28819,9 @@ __export(exports_doctor, {
28394
28819
  parsePythonVersion: () => parsePythonVersion,
28395
28820
  parseNodeVersion: () => parseNodeVersion,
28396
28821
  parseEnvFile: () => parseEnvFile,
28822
+ mffEnvState: () => mffEnvState,
28397
28823
  mffEnvPath: () => mffEnvPath,
28824
+ mffAgentName: () => mffAgentName,
28398
28825
  isSwitchroomCheckout: () => isSwitchroomCheckout,
28399
28826
  findChromium: () => findChromium,
28400
28827
  deriveEd25519PublicKeyBytes: () => deriveEd25519PublicKeyBytes,
@@ -28420,18 +28847,18 @@ __export(exports_doctor, {
28420
28847
  checkAgents: () => checkAgents,
28421
28848
  MFF_VAULT_KEY: () => MFF_VAULT_KEY
28422
28849
  });
28423
- import { execSync as execSync3, spawnSync as spawnSync6 } from "node:child_process";
28850
+ import { execSync as execSync3, spawnSync as spawnSync7 } from "node:child_process";
28424
28851
  import {
28425
- accessSync,
28426
- constants as fsConstants4,
28427
- existsSync as existsSync46,
28852
+ accessSync as accessSync2,
28853
+ constants as fsConstants5,
28854
+ existsSync as existsSync47,
28428
28855
  lstatSync as lstatSync5,
28429
28856
  mkdirSync as mkdirSync26,
28430
28857
  readFileSync as readFileSync44,
28431
28858
  readdirSync as readdirSync17,
28432
- statSync as statSync20
28859
+ statSync as statSync21
28433
28860
  } from "node:fs";
28434
- import { join as join41, resolve as resolve29 } from "node:path";
28861
+ import { dirname as dirname12, join as join41, resolve as resolve29 } from "node:path";
28435
28862
  import { createPublicKey, createPrivateKey } from "node:crypto";
28436
28863
  function statusGlyph(status) {
28437
28864
  switch (status) {
@@ -28445,14 +28872,14 @@ function statusGlyph(status) {
28445
28872
  }
28446
28873
  function findInNvm(bin) {
28447
28874
  const nvmRoot = join41(process.env.HOME ?? "", ".nvm", "versions", "node");
28448
- if (!existsSync46(nvmRoot))
28875
+ if (!existsSync47(nvmRoot))
28449
28876
  return null;
28450
28877
  try {
28451
28878
  const versions = readdirSync17(nvmRoot).sort().reverse();
28452
28879
  for (const v of versions) {
28453
28880
  const candidate = join41(nvmRoot, v, "bin", bin);
28454
28881
  try {
28455
- const s = statSync20(candidate);
28882
+ const s = statSync21(candidate);
28456
28883
  if (s.isFile() || s.isSymbolicLink()) {
28457
28884
  return candidate;
28458
28885
  }
@@ -28617,7 +29044,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
28617
29044
  }
28618
29045
  cacheLocations.push(join41(homeDir, ".cache", "ms-playwright"));
28619
29046
  for (const cacheDir of cacheLocations) {
28620
- if (!existsSync46(cacheDir))
29047
+ if (!existsSync47(cacheDir))
28621
29048
  continue;
28622
29049
  try {
28623
29050
  const entries = readdirSync17(cacheDir).filter((e) => e.startsWith("chromium"));
@@ -28629,7 +29056,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
28629
29056
  join41(cacheDir, entry, "chrome-linux", "headless_shell")
28630
29057
  ];
28631
29058
  for (const path4 of candidates2) {
28632
- if (existsSync46(path4))
29059
+ if (existsSync47(path4))
28633
29060
  return path4;
28634
29061
  }
28635
29062
  }
@@ -28652,7 +29079,7 @@ function checkChromium() {
28652
29079
  function checkDepsCacheWritable(depsRoot = resolvePath("~/.switchroom/deps")) {
28653
29080
  try {
28654
29081
  mkdirSync26(depsRoot, { recursive: true });
28655
- accessSync(depsRoot, fsConstants4.W_OK);
29082
+ accessSync2(depsRoot, fsConstants5.W_OK);
28656
29083
  return {
28657
29084
  name: "~/.switchroom/deps writable",
28658
29085
  status: "ok",
@@ -28710,7 +29137,7 @@ function checkLegacyState() {
28710
29137
  const results = [];
28711
29138
  const h = process.env.HOME ?? "/root";
28712
29139
  const clerkDir = join41(h, LEGACY_STATE_DIR);
28713
- const clerkPresent = existsSync46(clerkDir);
29140
+ const clerkPresent = existsSync47(clerkDir);
28714
29141
  results.push({
28715
29142
  name: "legacy ~/.clerk state",
28716
29143
  status: clerkPresent ? "warn" : "ok",
@@ -28752,7 +29179,7 @@ function checkVault(config) {
28752
29179
  status: "ok",
28753
29180
  detail: "Approval auth: passphrase (two-factor)"
28754
29181
  };
28755
- if (!existsSync46(vaultPath)) {
29182
+ if (!existsSync47(vaultPath)) {
28756
29183
  return [
28757
29184
  postureResult,
28758
29185
  {
@@ -28845,7 +29272,7 @@ function checkHindsightConsumer(config, opts) {
28845
29272
  }
28846
29273
  function probeAuthBrokerSocket(consumerName) {
28847
29274
  const containerPath = `/run/switchroom/auth-broker/${consumerName}/sock`;
28848
- const r = spawnSync6("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
29275
+ const r = spawnSync7("docker", ["exec", "switchroom-auth-broker", "test", "-S", containerPath], { stdio: "pipe", timeout: 3000 });
28849
29276
  if (r.error || r.status === null)
28850
29277
  return "unreachable";
28851
29278
  if (r.status === 0)
@@ -28924,7 +29351,7 @@ async function checkHindsight(config) {
28924
29351
  function checkPendingRetainsQueue(dir) {
28925
29352
  const home2 = process.env.HOME ?? "";
28926
29353
  const pendingDir = dir ?? process.env.HINDSIGHT_PENDING_DIR ?? join41(home2, ".hindsight", "pending-retains");
28927
- if (!existsSync46(pendingDir)) {
29354
+ if (!existsSync47(pendingDir)) {
28928
29355
  return {
28929
29356
  name: "pending-retains queue",
28930
29357
  status: "ok",
@@ -28995,7 +29422,7 @@ function tryReadHostFile(path4) {
28995
29422
  }
28996
29423
  }
28997
29424
  function parseEnvFile(path4) {
28998
- if (!existsSync46(path4))
29425
+ if (!existsSync47(path4))
28999
29426
  return {};
29000
29427
  let content;
29001
29428
  try {
@@ -29105,7 +29532,7 @@ async function checkTelegram(config) {
29105
29532
  }
29106
29533
  function checkStartShStale(agentName, startShPath) {
29107
29534
  const label = `${agentName}: start.sh scheduler block`;
29108
- if (!existsSync46(startShPath)) {
29535
+ if (!existsSync47(startShPath)) {
29109
29536
  return {
29110
29537
  name: label,
29111
29538
  status: "warn",
@@ -29173,7 +29600,7 @@ function checkLeakedHomeSwitchroom(agentName, agentDir) {
29173
29600
  function checkRepoHygiene(repoRoot) {
29174
29601
  const results = [];
29175
29602
  const exportDir = join41(repoRoot, "clerk-export");
29176
- if (existsSync46(exportDir)) {
29603
+ if (existsSync47(exportDir)) {
29177
29604
  results.push({
29178
29605
  name: "repo hygiene: clerk-export/ on disk (#1072)",
29179
29606
  status: "warn",
@@ -29182,7 +29609,7 @@ function checkRepoHygiene(repoRoot) {
29182
29609
  });
29183
29610
  }
29184
29611
  const knownTarball = join41(repoRoot, "clerk-export-with-secrets.tar.gz");
29185
- if (existsSync46(knownTarball)) {
29612
+ if (existsSync47(knownTarball)) {
29186
29613
  results.push({
29187
29614
  name: "repo hygiene: clerk-export-with-secrets.tar.gz on disk (#1072)",
29188
29615
  status: "warn",
@@ -29222,10 +29649,10 @@ function checkRepoHygiene(repoRoot) {
29222
29649
  }
29223
29650
  function isSwitchroomCheckout(dir) {
29224
29651
  try {
29225
- if (!existsSync46(join41(dir, ".git")))
29652
+ if (!existsSync47(join41(dir, ".git")))
29226
29653
  return false;
29227
29654
  const pkgPath = join41(dir, "package.json");
29228
- if (!existsSync46(pkgPath))
29655
+ if (!existsSync47(pkgPath))
29229
29656
  return false;
29230
29657
  const pkg = JSON.parse(readFileSync44(pkgPath, "utf-8"));
29231
29658
  return pkg.name === "switchroom";
@@ -29240,7 +29667,7 @@ function checkAgents(config, configPath) {
29240
29667
  const authStatuses = getAllAuthStatuses(config);
29241
29668
  for (const [name, agentConfig] of Object.entries(config.agents)) {
29242
29669
  const agentDir = resolve29(agentsDir, name);
29243
- if (!existsSync46(agentDir)) {
29670
+ if (!existsSync47(agentDir)) {
29244
29671
  results.push({
29245
29672
  name: `${name}: scaffold`,
29246
29673
  status: "fail",
@@ -29292,7 +29719,7 @@ function checkAgents(config, configPath) {
29292
29719
  name: `${name}: auth`,
29293
29720
  status: "fail",
29294
29721
  detail: auth?.pendingAuth ? "pending (auth flow in progress)" : "not authenticated",
29295
- fix: `Run \`switchroom auth add <label> --from-oauth\` then \`switchroom auth use <label>\` (RFC H \u2014 see docs/auth.md)`
29722
+ fix: `Run \`switchroom auth add <label> --via-claude\` then \`switchroom auth use <label>\` (RFC H \u2014 see docs/auth.md)`
29296
29723
  });
29297
29724
  }
29298
29725
  } else {
@@ -29332,13 +29759,13 @@ function checkAgents(config, configPath) {
29332
29759
  name: `${name}: auth slots`,
29333
29760
  status: status2,
29334
29761
  detail,
29335
- fix: quotaOut > 0 ? `Quota-exhausted account(s) will auto-recover when the window resets; broker auto-rotates per \`auth.fallback_order\` (see \`switchroom auth show\`).` : expired > 0 ? `Expired account(s) \u2014 add a fresh one via \`switchroom auth add <label> --from-oauth\` and \`switchroom auth use <label>\` (RFC H).` : undefined
29762
+ fix: quotaOut > 0 ? `Quota-exhausted account(s) will auto-recover when the window resets; broker auto-rotates per \`auth.fallback_order\` (see \`switchroom auth show\`).` : expired > 0 ? `Expired account(s) \u2014 add a fresh one via \`switchroom auth add <label> --via-claude\` and \`switchroom auth use <label>\` (RFC H).` : undefined
29336
29763
  });
29337
29764
  }
29338
29765
  }
29339
29766
  if (agentConfig.channels?.telegram?.plugin === "switchroom") {
29340
29767
  const mcpJsonPath = join41(agentDir, ".mcp.json");
29341
- if (!existsSync46(mcpJsonPath)) {
29768
+ if (!existsSync47(mcpJsonPath)) {
29342
29769
  results.push({
29343
29770
  name: `${name}: .mcp.json`,
29344
29771
  status: "fail",
@@ -29406,8 +29833,28 @@ ${title}`));
29406
29833
  }
29407
29834
  return { oks, warns, fails };
29408
29835
  }
29409
- function mffEnvPath() {
29410
- return resolve29(process.env.HOME ?? "/root", ".switchroom/credentials/my-family-finance/.env");
29836
+ function mffAgentName(config) {
29837
+ for (const [name, ac] of Object.entries(config?.agents ?? {})) {
29838
+ if ((ac?.skills ?? []).includes("my-family-finance")) {
29839
+ return name;
29840
+ }
29841
+ }
29842
+ return;
29843
+ }
29844
+ function mffEnvPath(config) {
29845
+ const home2 = process.env.HOME ?? "/root";
29846
+ const agent = mffAgentName(config);
29847
+ return agent ? resolve29(home2, ".switchroom/credentials", agent, "my-family-finance/.env") : resolve29(home2, ".switchroom/credentials/my-family-finance/.env");
29848
+ }
29849
+ function mffEnvState(envPath) {
29850
+ if (!existsSync47(envPath))
29851
+ return "absent";
29852
+ try {
29853
+ accessSync2(envPath, fsConstants5.R_OK);
29854
+ return "readable";
29855
+ } catch {
29856
+ return "agent-private";
29857
+ }
29411
29858
  }
29412
29859
  function checkMffVaultKeyPresent(passphrase, vaultPath) {
29413
29860
  if (!passphrase) {
@@ -29418,7 +29865,7 @@ function checkMffVaultKeyPresent(passphrase, vaultPath) {
29418
29865
  fix: "Export SWITCHROOM_VAULT_PASSPHRASE to enable MFF vault probes"
29419
29866
  };
29420
29867
  }
29421
- if (!existsSync46(vaultPath)) {
29868
+ if (!existsSync47(vaultPath)) {
29422
29869
  return {
29423
29870
  name: "mff: vault key present",
29424
29871
  status: "fail",
@@ -29471,7 +29918,7 @@ function deriveEd25519PublicKeyBytes(keyMaterial) {
29471
29918
  }
29472
29919
  }
29473
29920
  function checkMffVaultKeyFormat(passphrase, vaultPath) {
29474
- if (!passphrase || !existsSync46(vaultPath)) {
29921
+ if (!passphrase || !existsSync47(vaultPath)) {
29475
29922
  return {
29476
29923
  name: "mff: vault key format",
29477
29924
  status: "warn",
@@ -29513,12 +29960,20 @@ function checkMffVaultKeyFormat(passphrase, vaultPath) {
29513
29960
  }
29514
29961
  }
29515
29962
  function checkMffEnvFile(envPath = mffEnvPath()) {
29516
- if (!existsSync46(envPath)) {
29963
+ const state = mffEnvState(envPath);
29964
+ if (state === "absent") {
29517
29965
  return {
29518
29966
  name: "mff: .env present",
29519
29967
  status: "fail",
29520
29968
  detail: `${envPath} not found`,
29521
- fix: "Create ~/.switchroom/credentials/my-family-finance/.env with MFF_API_URL=https://..."
29969
+ fix: "Place the MFF skill's .env in the per-agent credentials dir, e.g. `~/.switchroom/credentials/<agent>/my-family-finance/.env` (MFF_API_URL=https://...), then `switchroom update`"
29970
+ };
29971
+ }
29972
+ if (state === "agent-private") {
29973
+ return {
29974
+ name: "mff: .env present",
29975
+ status: "ok",
29976
+ detail: `present, agent-private (${envPath}) \u2014 per-agent since WS6-F2; the operator-run doctor cannot read it by design`
29522
29977
  };
29523
29978
  }
29524
29979
  const env2 = parseEnvFile(envPath);
@@ -29537,6 +29992,14 @@ function checkMffEnvFile(envPath = mffEnvPath()) {
29537
29992
  };
29538
29993
  }
29539
29994
  async function checkMffApiReachable(envPath = mffEnvPath(), timeoutMs = 5000) {
29995
+ const state = mffEnvState(envPath);
29996
+ if (state !== "readable") {
29997
+ return {
29998
+ name: "mff: API reachable",
29999
+ status: "warn",
30000
+ detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
30001
+ };
30002
+ }
29540
30003
  const env2 = parseEnvFile(envPath);
29541
30004
  const apiUrl = env2.MFF_API_URL?.trim();
29542
30005
  if (!apiUrl) {
@@ -29581,6 +30044,14 @@ async function checkMffApiReachable(envPath = mffEnvPath(), timeoutMs = 5000) {
29581
30044
  }
29582
30045
  }
29583
30046
  async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
30047
+ const state = mffEnvState(envPath);
30048
+ if (state !== "readable") {
30049
+ return {
30050
+ name: "mff: auth flow",
30051
+ status: "warn",
30052
+ detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
30053
+ };
30054
+ }
29584
30055
  const env2 = parseEnvFile(envPath);
29585
30056
  const apiUrl = env2.MFF_API_URL?.trim();
29586
30057
  if (!apiUrl) {
@@ -29590,9 +30061,9 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
29590
30061
  detail: "skipped (MFF_API_URL not set)"
29591
30062
  };
29592
30063
  }
29593
- const credDir = resolve29(process.env.HOME ?? "/root", ".switchroom/credentials/my-family-finance");
30064
+ const credDir = dirname12(envPath);
29594
30065
  const authScript = join41(credDir, "claude-auth.py");
29595
- if (!existsSync46(authScript)) {
30066
+ if (!existsSync47(authScript)) {
29596
30067
  return {
29597
30068
  name: "mff: auth flow",
29598
30069
  status: "warn",
@@ -29603,7 +30074,7 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
29603
30074
  const python3 = which("python3") ?? "python3";
29604
30075
  let token;
29605
30076
  try {
29606
- const result = spawnSync6(python3, [authScript, "--quiet"], {
30077
+ const result = spawnSync7(python3, [authScript, "--quiet"], {
29607
30078
  timeout: timeoutMs,
29608
30079
  encoding: "utf-8",
29609
30080
  env: { ...process.env, ...env2 }
@@ -29671,6 +30142,14 @@ async function checkMffAuthFlow(envPath = mffEnvPath(), timeoutMs = 8000) {
29671
30142
  }
29672
30143
  }
29673
30144
  async function checkMffCloudflareUa(envPath = mffEnvPath(), timeoutMs = 5000) {
30145
+ const state = mffEnvState(envPath);
30146
+ if (state !== "readable") {
30147
+ return {
30148
+ name: "mff: Cloudflare UA bypass",
30149
+ status: "warn",
30150
+ detail: state === "agent-private" ? "skipped \u2014 MFF creds are per-agent/agent-private since WS6-F2; deep probe must run in-agent" : "skipped (MFF .env not found \u2014 see 'mff: .env present')"
30151
+ };
30152
+ }
29674
30153
  const env2 = parseEnvFile(envPath);
29675
30154
  const apiUrl = env2.MFF_API_URL?.trim();
29676
30155
  if (!apiUrl) {
@@ -29732,7 +30211,7 @@ async function checkMffCloudflareUa(envPath = mffEnvPath(), timeoutMs = 5000) {
29732
30211
  fix: "Check MFF_API_URL and whether the /api/health endpoint is publicly accessible"
29733
30212
  };
29734
30213
  }
29735
- async function checkMff(passphrase, vaultPath, envPath = mffEnvPath()) {
30214
+ async function checkMff(passphrase, vaultPath, config, envPath = mffEnvPath(config)) {
29736
30215
  const results = [];
29737
30216
  results.push(checkMffVaultKeyPresent(passphrase, vaultPath));
29738
30217
  results.push(checkMffVaultKeyFormat(passphrase, vaultPath));
@@ -29835,7 +30314,7 @@ function registerDoctorCommand(program3) {
29835
30314
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
29836
30315
  const vaultPath = config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
29837
30316
  if (opts.skill === "mff") {
29838
- const mffResults = await checkMff(passphrase, vaultPath);
30317
+ const mffResults = await checkMff(passphrase, vaultPath, config);
29839
30318
  if (opts.json) {
29840
30319
  console.log(JSON.stringify({ sections: [{ title: "MFF Skill", results: mffResults }] }, null, 2));
29841
30320
  } else {
@@ -29862,6 +30341,7 @@ function registerDoctorCommand(program3) {
29862
30341
  },
29863
30342
  { title: "Legacy State", results: checkLegacyState() },
29864
30343
  { title: "Vault", results: checkVault(config) },
30344
+ { title: "Vault access", results: runSecretAccessChecks(config) },
29865
30345
  { title: "Memory (Hindsight)", results: await checkHindsight(config) },
29866
30346
  { title: "Telegram", results: await checkTelegram(config) },
29867
30347
  { title: "Agents", results: checkAgents(config, configPath) },
@@ -29869,8 +30349,9 @@ function registerDoctorCommand(program3) {
29869
30349
  { title: "Audit integrity (WS10-F4)", results: runAuditIntegrityChecks() },
29870
30350
  { title: "Docker (Phase 1a)", results: runDockerSection(config) },
29871
30351
  { title: "Auth Broker", results: runAuthBrokerChecks(config) },
30352
+ { title: "Host control (hostd)", results: runHostdChecks(config) },
29872
30353
  { title: "Google Drive", results: runDriveChecks(config) },
29873
- { title: "MFF Skill", results: await checkMff(passphrase, vaultPath) }
30354
+ { title: "MFF Skill", results: await checkMff(passphrase, vaultPath, config) }
29874
30355
  ];
29875
30356
  const cwd = process.cwd();
29876
30357
  if (isSwitchroomCheckout(cwd)) {
@@ -29923,8 +30404,10 @@ var init_doctor = __esm(() => {
29923
30404
  init_hindsight();
29924
30405
  init_doctor_docker();
29925
30406
  init_doctor_auth_broker();
30407
+ init_doctor_hostd();
29926
30408
  init_doctor_drive();
29927
30409
  init_doctor_credentials_migration();
30410
+ init_doctor_secret_access();
29928
30411
  init_doctor_inlined_secrets();
29929
30412
  init_doctor_audit_integrity();
29930
30413
  MANIFEST_WARN_ONLY = new Set([
@@ -45564,9 +46047,9 @@ __export(exports_server, {
45564
46047
  dispatchTool: () => dispatchTool,
45565
46048
  TOOLS: () => TOOLS
45566
46049
  });
45567
- import { spawnSync as spawnSync9 } from "node:child_process";
46050
+ import { spawnSync as spawnSync10 } from "node:child_process";
45568
46051
  function execCli(args) {
45569
- const r = spawnSync9(CLI_BIN, args, {
46052
+ const r = spawnSync10(CLI_BIN, args, {
45570
46053
  encoding: "utf-8",
45571
46054
  env: process.env,
45572
46055
  timeout: 15000
@@ -46059,7 +46542,7 @@ __export(exports_server2, {
46059
46542
  TOOLS: () => TOOLS2
46060
46543
  });
46061
46544
  import { randomBytes as randomBytes13 } from "node:crypto";
46062
- import { existsSync as existsSync69, readFileSync as readFileSync59 } from "node:fs";
46545
+ import { existsSync as existsSync71, readFileSync as readFileSync59 } from "node:fs";
46063
46546
  function selfSocketPath() {
46064
46547
  return `/run/switchroom/hostd/${SELF_AGENT}/sock`;
46065
46548
  }
@@ -46074,7 +46557,7 @@ async function dispatchTool2(name, args) {
46074
46557
  return errorText2("hostd MCP: SWITCHROOM_AGENT_NAME env var is not set \u2014 cannot " + "determine which per-agent socket to talk to.");
46075
46558
  }
46076
46559
  const sockPath = selfSocketPath();
46077
- if (!existsSync69(sockPath)) {
46560
+ if (!existsSync71(sockPath)) {
46078
46561
  return errorText2(`hostd MCP: socket not bound at ${sockPath}. The host-control ` + `daemon is either not installed (run \`switchroom hostd install\`) ` + `or this agent isn't admin-flagged in switchroom.yaml. RFC C ` + `bind-mounts the per-agent socket only when host_control.enabled ` + `is true AND the agent has admin: true.`);
46079
46562
  }
46080
46563
  let req;
@@ -46193,13 +46676,13 @@ function resolveAuditLogPath() {
46193
46676
  if (process.env.HOSTD_AUDIT_LOG_PATH)
46194
46677
  return process.env.HOSTD_AUDIT_LOG_PATH;
46195
46678
  const bindMounted = "/host-home/.switchroom/host-control-audit.log";
46196
- if (existsSync69(bindMounted))
46679
+ if (existsSync71(bindMounted))
46197
46680
  return bindMounted;
46198
46681
  return defaultAuditLogPath2();
46199
46682
  }
46200
46683
  function getLastUpdateApplyStatus() {
46201
46684
  const path8 = resolveAuditLogPath();
46202
- if (!existsSync69(path8)) {
46685
+ if (!existsSync71(path8)) {
46203
46686
  return errorText2(`get_status: audit log not found at ${path8}. No update_apply has run yet?`);
46204
46687
  }
46205
46688
  let raw;
@@ -46411,8 +46894,8 @@ var {
46411
46894
  } = import__.default;
46412
46895
 
46413
46896
  // src/build-info.ts
46414
- var VERSION = "0.12.0";
46415
- var COMMIT_SHA = "2870fb03";
46897
+ var VERSION = "0.12.1";
46898
+ var COMMIT_SHA = "0de1edd1";
46416
46899
 
46417
46900
  // src/cli/agent.ts
46418
46901
  init_source();
@@ -48496,6 +48979,16 @@ function buildSettingsHooksBlock(p) {
48496
48979
  }
48497
48980
  ]
48498
48981
  },
48982
+ {
48983
+ matcher: "^(Write|Edit|MultiEdit)$",
48984
+ hooks: [
48985
+ {
48986
+ type: "command",
48987
+ command: wrap("hook:skill-validate-pretool", `node "${join8(DOCKER_BUNDLED_HOOKS_PATH, "skill-validate-pretool.mjs")}"`),
48988
+ timeout: 10
48989
+ }
48990
+ ]
48991
+ },
48499
48992
  {
48500
48993
  hooks: [
48501
48994
  {
@@ -55184,145 +55677,6 @@ var DEFAULT_AUTO_UNLOCK_PATH = "~/.switchroom/vault-auto-unlock";
55184
55677
 
55185
55678
  // src/vault/broker/server.ts
55186
55679
  init_peercred();
55187
-
55188
- // src/vault/broker/acl.ts
55189
- function parseCronUnit(unitName) {
55190
- const m = unitName.match(/^switchroom-([a-zA-Z0-9_-]+)-cron-(\d+)\.service$/);
55191
- if (!m)
55192
- return null;
55193
- const agentName = m[1];
55194
- const index = parseInt(m[2], 10);
55195
- if (!agentName)
55196
- return null;
55197
- return { agentName, index };
55198
- }
55199
- function agentSlugFromPeer(peer) {
55200
- if (peer.systemdUnit === null)
55201
- return null;
55202
- const parsed = parseCronUnit(peer.systemdUnit);
55203
- return parsed?.agentName ?? null;
55204
- }
55205
- function checkEntryScope(scope, agentSlug) {
55206
- if (scope === undefined || scope === null) {
55207
- return { allow: true };
55208
- }
55209
- const deny = scope.deny ?? [];
55210
- const allow = scope.allow ?? [];
55211
- if (agentSlug !== null && deny.includes(agentSlug)) {
55212
- return {
55213
- allow: false,
55214
- reason: `agent '${agentSlug}' is in the entry's deny list (scope-deny)`
55215
- };
55216
- }
55217
- if (allow.length > 0) {
55218
- if (agentSlug === null || !allow.includes(agentSlug)) {
55219
- return {
55220
- allow: false,
55221
- reason: agentSlug === null ? "caller agent slug could not be determined; entry has a non-empty allow list (scope-allow)" : `agent '${agentSlug}' is not in the entry's allow list (scope-allow)`
55222
- };
55223
- }
55224
- }
55225
- return { allow: true };
55226
- }
55227
- function checkAcl(peer, config, key) {
55228
- if (peer.systemdUnit !== null) {
55229
- const parsed = parseCronUnit(peer.systemdUnit);
55230
- if (parsed === null) {
55231
- return {
55232
- allow: false,
55233
- reason: `systemd unit '${peer.systemdUnit}' does not match switchroom cron unit naming convention`
55234
- };
55235
- }
55236
- const { agentName, index } = parsed;
55237
- const agentConfig = config.agents?.[agentName];
55238
- if (!agentConfig) {
55239
- return { allow: false, reason: `agent '${agentName}' not found in config` };
55240
- }
55241
- const schedule = agentConfig.schedule ?? [];
55242
- if (index >= schedule.length || index < 0) {
55243
- return {
55244
- allow: false,
55245
- reason: `schedule index ${index} out of range for agent '${agentName}' (${schedule.length} entries)`
55246
- };
55247
- }
55248
- const entry = schedule[index];
55249
- const allowedKeys = entry.secrets ?? [];
55250
- if (!allowedKeys.includes(key)) {
55251
- return {
55252
- allow: false,
55253
- reason: `key '${key}' not in ACL for ${agentName}/schedule[${index}]`
55254
- };
55255
- }
55256
- return { allow: true };
55257
- }
55258
- return {
55259
- allow: false,
55260
- reason: "caller is not a switchroom cron unit; use 'switchroom vault get --no-broker' for interactive access"
55261
- };
55262
- }
55263
- function checkAclByAgent(config, agentName, key) {
55264
- if (!agentName) {
55265
- return { allow: false, reason: "agent name unresolved" };
55266
- }
55267
- const agentConfig = config.agents?.[agentName];
55268
- if (!agentConfig) {
55269
- return { allow: false, reason: `agent '${agentName}' not found in config` };
55270
- }
55271
- const googleSlot = parseGoogleAccountSlotKey(key);
55272
- if (googleSlot !== null) {
55273
- return checkGoogleAccountAcl(config, agentName, googleSlot.account, key);
55274
- }
55275
- const schedule = agentConfig.schedule ?? [];
55276
- if (schedule.length === 0) {
55277
- return {
55278
- allow: false,
55279
- reason: `agent '${agentName}' has no schedule entries declaring 'secrets'; nothing is broker-accessible`
55280
- };
55281
- }
55282
- for (const entry of schedule) {
55283
- const allowed = entry?.secrets ?? [];
55284
- if (allowed.includes(key)) {
55285
- return { allow: true };
55286
- }
55287
- }
55288
- return {
55289
- allow: false,
55290
- reason: `key '${key}' not in ACL for agent '${agentName}'`
55291
- };
55292
- }
55293
- function parseGoogleAccountSlotKey(key) {
55294
- const match = key.match(/^google:([^:]+):([a-z_]+)$/);
55295
- if (!match)
55296
- return null;
55297
- return { account: match[1], field: match[2] };
55298
- }
55299
- function checkGoogleAccountAcl(config, agentName, account, key) {
55300
- const accounts = config.google_accounts ?? {};
55301
- const accountKey = account.toLowerCase();
55302
- const accountEntry = accounts[accountKey] ?? accounts[account];
55303
- if (!accountEntry) {
55304
- return {
55305
- allow: false,
55306
- reason: `google_accounts['${account}'] not configured (key '${key}')`
55307
- };
55308
- }
55309
- const enabled = accountEntry.enabled_for ?? [];
55310
- if (enabled.length === 0) {
55311
- return {
55312
- allow: false,
55313
- reason: `google_accounts['${account}'].enabled_for is empty (key '${key}')`
55314
- };
55315
- }
55316
- if (!enabled.includes(agentName)) {
55317
- return {
55318
- allow: false,
55319
- reason: `agent '${agentName}' not in google_accounts['${account}'].enabled_for (key '${key}')`
55320
- };
55321
- }
55322
- return { allow: true };
55323
- }
55324
-
55325
- // src/vault/broker/server.ts
55326
55680
  init_protocol();
55327
55681
 
55328
55682
  // src/vault/broker/audit-log.ts
@@ -57962,35 +58316,37 @@ class VaultBroker {
57962
58316
  const grantId = dotIdx !== -1 ? req.token.slice(0, dotIdx) : undefined;
57963
58317
  const sentinelKey = Object.keys(this.secrets)[0] ?? "__list_check__";
57964
58318
  const tokenCheck = await validateGrant(this.grantsDb, req.token, sentinelKey);
57965
- if (!tokenCheck.ok && tokenCheck.reason !== "grant-key-not-allowed") {
58319
+ if (!tokenCheck.ok && tokenCheck.reason === "grant-revoked") {
57966
58320
  this.auditLogger.write({
57967
58321
  ts: new Date().toISOString(),
57968
58322
  op: "list",
57969
58323
  caller: auditCaller,
57970
58324
  pid: auditPid,
57971
58325
  cgroup: auditCgroup,
57972
- result: `denied:${tokenCheck.reason}`,
58326
+ result: `denied:grant-revoked`,
57973
58327
  method: "grant",
57974
58328
  grant_id: grantId
57975
58329
  });
57976
- socket.write(encodeResponse(errorResponse("DENIED", tokenCheck.reason)));
58330
+ socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
58331
+ return;
58332
+ }
58333
+ if (tokenCheck.ok || tokenCheck.reason === "grant-key-not-allowed") {
58334
+ const grantRow = tokenCheck.ok ? tokenCheck.grant : this.grantsDb.query("SELECT key_allow FROM vault_grants WHERE id = ?").get(grantId ?? "");
58335
+ const allowedKeys = grantRow ? typeof grantRow.key_allow === "string" ? JSON.parse(grantRow.key_allow) : grantRow.key_allow : [];
58336
+ const visibleKeys2 = allowedKeys.filter((k) => (k in this.secrets));
58337
+ this.auditLogger.write({
58338
+ ts: new Date().toISOString(),
58339
+ op: "list",
58340
+ caller: auditCaller,
58341
+ pid: auditPid,
58342
+ cgroup: auditCgroup,
58343
+ result: `allowed:${visibleKeys2.length}`,
58344
+ method: "grant",
58345
+ grant_id: grantId
58346
+ });
58347
+ socket.write(encodeResponse({ ok: true, keys: visibleKeys2 }));
57977
58348
  return;
57978
58349
  }
57979
- const grantRow = tokenCheck.ok ? tokenCheck.grant : this.grantsDb.query("SELECT key_allow FROM vault_grants WHERE id = ?").get(grantId ?? "");
57980
- const allowedKeys = grantRow ? typeof grantRow.key_allow === "string" ? JSON.parse(grantRow.key_allow) : grantRow.key_allow : [];
57981
- const visibleKeys2 = allowedKeys.filter((k) => (k in this.secrets));
57982
- this.auditLogger.write({
57983
- ts: new Date().toISOString(),
57984
- op: "list",
57985
- caller: auditCaller,
57986
- pid: auditPid,
57987
- cgroup: auditCgroup,
57988
- result: `allowed:${visibleKeys2.length}`,
57989
- method: "grant",
57990
- grant_id: grantId
57991
- });
57992
- socket.write(encodeResponse({ ok: true, keys: visibleKeys2 }));
57993
- return;
57994
58350
  }
57995
58351
  if (!isOperator && agentName === null && process.platform === "linux" && peer === null) {
57996
58352
  const reason = "Unable to identify caller (peercred unavailable); denying on Linux";
@@ -58065,10 +58421,9 @@ class VaultBroker {
58065
58421
  });
58066
58422
  socket.write(encodeResponse(entryResponse(entry2)));
58067
58423
  return;
58068
- } else {
58424
+ } else if (grantResult.reason === "grant-revoked") {
58069
58425
  const dotIdx = req.token.indexOf(".");
58070
58426
  const grantId = dotIdx !== -1 ? req.token.slice(0, dotIdx) : undefined;
58071
- const denyReason = grantResult.reason;
58072
58427
  this.auditLogger.write({
58073
58428
  ts: new Date().toISOString(),
58074
58429
  op: "get",
@@ -58076,11 +58431,11 @@ class VaultBroker {
58076
58431
  caller: auditCaller,
58077
58432
  pid: auditPid,
58078
58433
  cgroup: auditCgroup,
58079
- result: `denied:${denyReason}`,
58434
+ result: `denied:grant-revoked`,
58080
58435
  method: "grant",
58081
58436
  grant_id: grantId
58082
58437
  });
58083
- socket.write(encodeResponse(errorResponse("DENIED", denyReason)));
58438
+ socket.write(encodeResponse(errorResponse("DENIED", "grant-revoked")));
58084
58439
  return;
58085
58440
  }
58086
58441
  }
@@ -68131,7 +68486,7 @@ async function stepScaffoldAgents(config, agentBots, userId, nonInteractive, swi
68131
68486
  if (switchroomConfigPath && !config.auth?.active) {
68132
68487
  try {
68133
68488
  await ensureAuthActiveDefault(switchroomConfigPath);
68134
- console.log(source_default.gray(" Set auth.active: default \u2014 run `switchroom auth add default --from-oauth` to log in"));
68489
+ console.log(source_default.gray(" Set auth.active: default \u2014 run `switchroom auth add default --via-claude` to log in"));
68135
68490
  } catch (err) {
68136
68491
  console.log(source_default.yellow(` \u26a0 Could not set auth.active default: ${err.message}`));
68137
68492
  }
@@ -68410,17 +68765,17 @@ init_doctor();
68410
68765
  init_source();
68411
68766
  init_loader();
68412
68767
  init_lifecycle();
68413
- import { cpSync as cpSync2, existsSync as existsSync47, mkdirSync as mkdirSync27, readFileSync as readFileSync45, realpathSync as realpathSync4, rmSync as rmSync12, statSync as statSync21 } from "node:fs";
68414
- import { spawnSync as spawnSync7 } from "node:child_process";
68415
- import { join as join42, dirname as dirname12, resolve as resolve30 } from "node:path";
68768
+ import { cpSync as cpSync2, existsSync as existsSync48, mkdirSync as mkdirSync27, readFileSync as readFileSync45, realpathSync as realpathSync5, rmSync as rmSync12, statSync as statSync22 } from "node:fs";
68769
+ import { spawnSync as spawnSync8 } from "node:child_process";
68770
+ import { join as join42, dirname as dirname13, resolve as resolve30 } from "node:path";
68416
68771
  import { homedir as homedir22 } from "node:os";
68417
68772
  var DEFAULT_COMPOSE_PATH = join42(homedir22(), ".switchroom", "compose", "docker-compose.yml");
68418
68773
  function isGitCheckout(scriptPath) {
68419
- let dir = dirname12(scriptPath);
68774
+ let dir = dirname13(scriptPath);
68420
68775
  for (let i = 0;i < 10; i++) {
68421
- if (existsSync47(join42(dir, ".git")))
68776
+ if (existsSync48(join42(dir, ".git")))
68422
68777
  return true;
68423
- const parent = dirname12(dir);
68778
+ const parent = dirname13(dir);
68424
68779
  if (parent === dir)
68425
68780
  return false;
68426
68781
  dir = parent;
@@ -68458,7 +68813,7 @@ function planUpdate(opts) {
68458
68813
  steps.push({
68459
68814
  name: "pull-images",
68460
68815
  description: "Pull broker / kernel / agent images from GHCR",
68461
- skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync47(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
68816
+ skipReason: opts.skipImages ? "--skip-images flag set" : !existsSync48(composePath) ? `compose file not found at ${composePath} (run \`switchroom apply --compose-only\` first)` : undefined,
68462
68817
  run: () => {
68463
68818
  const r = runner("docker", [
68464
68819
  "compose",
@@ -68537,16 +68892,16 @@ function planUpdate(opts) {
68537
68892
  }
68538
68893
  const source = resolve30(import.meta.dirname, "../../skills");
68539
68894
  const dest = join42(homedir22(), ".switchroom", "skills", "_bundled");
68540
- if (!existsSync47(source)) {
68895
+ if (!existsSync48(source)) {
68541
68896
  process.stderr.write(`switchroom update: sync-bundled-skills \u2014 CLI bundle has no adjacent skills/ at ${source}; skipping.
68542
68897
  `);
68543
68898
  return;
68544
68899
  }
68545
68900
  try {
68546
- if (existsSync47(dest)) {
68901
+ if (existsSync48(dest)) {
68547
68902
  rmSync12(dest, { recursive: true, force: true });
68548
68903
  }
68549
- mkdirSync27(dirname12(dest), { recursive: true });
68904
+ mkdirSync27(dirname13(dest), { recursive: true });
68550
68905
  cpSync2(source, dest, { recursive: true, dereference: false });
68551
68906
  } catch (err) {
68552
68907
  throw new Error(`sync-bundled-skills failed: ${err.message}`);
@@ -68605,7 +68960,7 @@ function planUpdate(opts) {
68605
68960
  return steps;
68606
68961
  }
68607
68962
  function defaultRunner(cmd, args) {
68608
- const r = spawnSync7(cmd, args, { stdio: "inherit" });
68963
+ const r = spawnSync8(cmd, args, { stdio: "inherit" });
68609
68964
  return { status: r.status ?? 1 };
68610
68965
  }
68611
68966
  function writeMarkerInPreferredLocation(agent, reason, runner) {
@@ -68642,16 +68997,16 @@ function defaultStatusProbe(composePath) {
68642
68997
  let scriptPath = rawScriptPath;
68643
68998
  try {
68644
68999
  if (rawScriptPath)
68645
- scriptPath = realpathSync4(rawScriptPath);
69000
+ scriptPath = realpathSync5(rawScriptPath);
68646
69001
  } catch {}
68647
69002
  if (scriptPath) {
68648
69003
  try {
68649
- cliBuiltAt = new Date(statSync21(scriptPath).mtimeMs).toISOString();
69004
+ cliBuiltAt = new Date(statSync22(scriptPath).mtimeMs).toISOString();
68650
69005
  } catch {}
68651
- let dir = dirname12(scriptPath);
69006
+ let dir = dirname13(scriptPath);
68652
69007
  for (let i = 0;i < 8; i++) {
68653
69008
  const pkgPath = join42(dir, "package.json");
68654
- if (existsSync47(pkgPath)) {
69009
+ if (existsSync48(pkgPath)) {
68655
69010
  try {
68656
69011
  const pkg = JSON.parse(readFileSync45(pkgPath, "utf-8"));
68657
69012
  if (typeof pkg.version === "string")
@@ -68661,7 +69016,7 @@ function defaultStatusProbe(composePath) {
68661
69016
  }
68662
69017
  break;
68663
69018
  }
68664
- const parent = dirname12(dir);
69019
+ const parent = dirname13(dir);
68665
69020
  if (parent === dir)
68666
69021
  break;
68667
69022
  dir = parent;
@@ -68674,13 +69029,13 @@ function defaultStatusProbe(composePath) {
68674
69029
  warnings.push("could not resolve CLI version (no package.json found above the resolved script path)");
68675
69030
  }
68676
69031
  const services = [];
68677
- if (!existsSync47(composePath)) {
69032
+ if (!existsSync48(composePath)) {
68678
69033
  warnings.push(`compose file not found at ${composePath}; service status unknown`);
68679
69034
  return { cliVersion, cliBuiltAt, services, warnings };
68680
69035
  }
68681
69036
  let serviceList = [];
68682
69037
  try {
68683
- const r = spawnSync7("docker", ["compose", "-p", "switchroom", "-f", composePath, "config", "--services"], { encoding: "utf-8", timeout: 1e4 });
69038
+ const r = spawnSync8("docker", ["compose", "-p", "switchroom", "-f", composePath, "config", "--services"], { encoding: "utf-8", timeout: 1e4 });
68684
69039
  if (r.status !== 0) {
68685
69040
  warnings.push(`docker compose config --services failed: ${r.stderr?.trim() ?? r.error?.message ?? "unknown"}`);
68686
69041
  return { cliVersion, cliBuiltAt, services, warnings };
@@ -68697,7 +69052,7 @@ function defaultStatusProbe(composePath) {
68697
69052
  let containerCreatedAt = null;
68698
69053
  let status = "<unknown>";
68699
69054
  try {
68700
- const r = spawnSync7("docker", ["inspect", "-f", "{{.Config.Image}}|{{.Created}}|{{.State.Status}}", containerName2], { encoding: "utf-8", timeout: 5000 });
69055
+ const r = spawnSync8("docker", ["inspect", "-f", "{{.Config.Image}}|{{.Created}}|{{.State.Status}}", containerName2], { encoding: "utf-8", timeout: 5000 });
68701
69056
  if (r.status === 0) {
68702
69057
  const [img, created, st] = r.stdout.trim().split("|");
68703
69058
  image = img ?? null;
@@ -68713,7 +69068,7 @@ function defaultStatusProbe(composePath) {
68713
69068
  let imagePulledAt = null;
68714
69069
  if (image) {
68715
69070
  try {
68716
- const r = spawnSync7("docker", ["image", "inspect", "-f", "{{.Id}}|{{.Created}}|{{.Metadata.LastTagTime}}", image], { encoding: "utf-8", timeout: 5000 });
69071
+ const r = spawnSync8("docker", ["image", "inspect", "-f", "{{.Id}}|{{.Created}}|{{.Metadata.LastTagTime}}", image], { encoding: "utf-8", timeout: 5000 });
68717
69072
  if (r.status === 0) {
68718
69073
  const [id, created, lastTag] = r.stdout.trim().split("|");
68719
69074
  imageDigestShort = id?.replace(/^sha256:/, "").slice(0, 12) ?? null;
@@ -68861,8 +69216,8 @@ init_source();
68861
69216
  init_helpers();
68862
69217
  init_lifecycle();
68863
69218
  import { execSync as execSync4 } from "node:child_process";
68864
- import { existsSync as existsSync48, readFileSync as readFileSync46 } from "node:fs";
68865
- import { dirname as dirname13, join as join43 } from "node:path";
69219
+ import { existsSync as existsSync49, readFileSync as readFileSync46 } from "node:fs";
69220
+ import { dirname as dirname14, join as join43 } from "node:path";
68866
69221
  function getClaudeCodeVersion() {
68867
69222
  try {
68868
69223
  const out = execSync4("claude --version 2>/dev/null", {
@@ -68913,15 +69268,15 @@ function locateSwitchroomInstallDir() {
68913
69268
  let dir = import.meta.dirname;
68914
69269
  for (let i = 0;i < 10 && dir && dir !== "/"; i++) {
68915
69270
  const pkgPath = join43(dir, "package.json");
68916
- if (existsSync48(pkgPath)) {
69271
+ if (existsSync49(pkgPath)) {
68917
69272
  try {
68918
69273
  const pkg = JSON.parse(readFileSync46(pkgPath, "utf-8"));
68919
- if (pkg.name === "switchroom" && existsSync48(join43(dir, ".git"))) {
69274
+ if (pkg.name === "switchroom" && existsSync49(join43(dir, ".git"))) {
68920
69275
  return dir;
68921
69276
  }
68922
69277
  } catch {}
68923
69278
  }
68924
- dir = dirname13(dir);
69279
+ dir = dirname14(dir);
68925
69280
  }
68926
69281
  return null;
68927
69282
  }
@@ -69135,13 +69490,13 @@ function registerHandoffCommand(program3) {
69135
69490
  // src/issues/store.ts
69136
69491
  import {
69137
69492
  closeSync as closeSync10,
69138
- existsSync as existsSync49,
69493
+ existsSync as existsSync50,
69139
69494
  mkdirSync as mkdirSync28,
69140
69495
  openSync as openSync10,
69141
69496
  readdirSync as readdirSync18,
69142
69497
  readFileSync as readFileSync47,
69143
69498
  renameSync as renameSync10,
69144
- statSync as statSync22,
69499
+ statSync as statSync23,
69145
69500
  unlinkSync as unlinkSync10,
69146
69501
  writeFileSync as writeFileSync25,
69147
69502
  writeSync as writeSync6
@@ -69467,7 +69822,7 @@ var ISSUES_FILE = "issues.jsonl";
69467
69822
  var ISSUES_LOCK = "issues.lock";
69468
69823
  function readAll(stateDir) {
69469
69824
  const path4 = join44(stateDir, ISSUES_FILE);
69470
- if (!existsSync49(path4))
69825
+ if (!existsSync50(path4))
69471
69826
  return [];
69472
69827
  let raw;
69473
69828
  try {
@@ -69544,7 +69899,7 @@ function record(stateDir, input, nowFn = Date.now) {
69544
69899
  });
69545
69900
  }
69546
69901
  function resolve33(stateDir, fingerprint, nowFn = Date.now) {
69547
- if (!existsSync49(join44(stateDir, ISSUES_FILE)))
69902
+ if (!existsSync50(join44(stateDir, ISSUES_FILE)))
69548
69903
  return 0;
69549
69904
  return withLock(stateDir, () => {
69550
69905
  const all = readAll(stateDir);
@@ -69562,7 +69917,7 @@ function resolve33(stateDir, fingerprint, nowFn = Date.now) {
69562
69917
  });
69563
69918
  }
69564
69919
  function resolveAllBySource(stateDir, source, nowFn = Date.now) {
69565
- if (!existsSync49(join44(stateDir, ISSUES_FILE)))
69920
+ if (!existsSync50(join44(stateDir, ISSUES_FILE)))
69566
69921
  return 0;
69567
69922
  return withLock(stateDir, () => {
69568
69923
  const all = readAll(stateDir);
@@ -69580,7 +69935,7 @@ function resolveAllBySource(stateDir, source, nowFn = Date.now) {
69580
69935
  });
69581
69936
  }
69582
69937
  function prune(stateDir, opts = {}) {
69583
- if (!existsSync49(join44(stateDir, ISSUES_FILE)))
69938
+ if (!existsSync50(join44(stateDir, ISSUES_FILE)))
69584
69939
  return 0;
69585
69940
  return withLock(stateDir, () => {
69586
69941
  const all = readAll(stateDir);
@@ -69637,7 +69992,7 @@ function sweepOrphanTmpFiles(stateDir) {
69637
69992
  continue;
69638
69993
  const tmpPath = join44(stateDir, entry);
69639
69994
  try {
69640
- const stat = statSync22(tmpPath);
69995
+ const stat = statSync23(tmpPath);
69641
69996
  if (stat.mtimeMs < cutoff) {
69642
69997
  unlinkSync10(tmpPath);
69643
69998
  }
@@ -69930,20 +70285,20 @@ function relTime(deltaMs) {
69930
70285
 
69931
70286
  // src/cli/deps.ts
69932
70287
  init_source();
69933
- import { existsSync as existsSync52 } from "node:fs";
70288
+ import { existsSync as existsSync53 } from "node:fs";
69934
70289
  import { homedir as homedir25 } from "node:os";
69935
70290
  import { join as join47, resolve as resolve34 } from "node:path";
69936
70291
 
69937
70292
  // src/deps/python.ts
69938
70293
  import { createHash as createHash9 } from "node:crypto";
69939
70294
  import {
69940
- existsSync as existsSync50,
70295
+ existsSync as existsSync51,
69941
70296
  mkdirSync as mkdirSync29,
69942
70297
  readFileSync as readFileSync48,
69943
70298
  rmSync as rmSync13,
69944
70299
  writeFileSync as writeFileSync26
69945
70300
  } from "node:fs";
69946
- import { dirname as dirname14, join as join45 } from "node:path";
70301
+ import { dirname as dirname15, join as join45 } from "node:path";
69947
70302
  import { homedir as homedir23 } from "node:os";
69948
70303
  import { execFileSync as execFileSync14 } from "node:child_process";
69949
70304
 
@@ -69965,7 +70320,7 @@ function ensurePythonEnv(opts) {
69965
70320
  const { skillName, requirementsPath, force = false } = opts;
69966
70321
  const cacheRoot = opts.cacheRoot ?? defaultPythonCacheRoot();
69967
70322
  const hostPython = opts.pythonBin ?? "python3";
69968
- if (!existsSync50(requirementsPath)) {
70323
+ if (!existsSync51(requirementsPath)) {
69969
70324
  throw new PythonEnvError(`requirements file not found: ${requirementsPath}`);
69970
70325
  }
69971
70326
  const venvDir = join45(cacheRoot, skillName);
@@ -69974,7 +70329,7 @@ function ensurePythonEnv(opts) {
69974
70329
  const pythonBin = join45(binDir, "python");
69975
70330
  const pipBin = join45(binDir, "pip");
69976
70331
  const targetHash = hashFile(requirementsPath);
69977
- if (!force && existsSync50(stampPath) && existsSync50(pythonBin)) {
70332
+ if (!force && existsSync51(stampPath) && existsSync51(pythonBin)) {
69978
70333
  const existingHash = readFileSync48(stampPath, "utf8").trim();
69979
70334
  if (existingHash === targetHash) {
69980
70335
  return {
@@ -69987,10 +70342,10 @@ function ensurePythonEnv(opts) {
69987
70342
  };
69988
70343
  }
69989
70344
  }
69990
- if (existsSync50(venvDir)) {
70345
+ if (existsSync51(venvDir)) {
69991
70346
  rmSync13(venvDir, { recursive: true, force: true });
69992
70347
  }
69993
- mkdirSync29(dirname14(venvDir), { recursive: true });
70348
+ mkdirSync29(dirname15(venvDir), { recursive: true });
69994
70349
  try {
69995
70350
  execFileSync14(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
69996
70351
  } catch (err) {
@@ -70025,13 +70380,13 @@ function ensurePythonEnv(opts) {
70025
70380
  import { createHash as createHash10 } from "node:crypto";
70026
70381
  import {
70027
70382
  copyFileSync as copyFileSync9,
70028
- existsSync as existsSync51,
70383
+ existsSync as existsSync52,
70029
70384
  mkdirSync as mkdirSync30,
70030
70385
  readFileSync as readFileSync49,
70031
70386
  rmSync as rmSync14,
70032
70387
  writeFileSync as writeFileSync27
70033
70388
  } from "node:fs";
70034
- import { dirname as dirname15, join as join46 } from "node:path";
70389
+ import { dirname as dirname16, join as join46 } from "node:path";
70035
70390
  import { homedir as homedir24 } from "node:os";
70036
70391
  import { execFileSync as execFileSync15 } from "node:child_process";
70037
70392
 
@@ -70058,14 +70413,14 @@ function defaultNodeCacheRoot() {
70058
70413
  return join46(homedir24(), ".switchroom", "deps", "node");
70059
70414
  }
70060
70415
  function hashDepInputs(packageJsonPath) {
70061
- const sourceDir = dirname15(packageJsonPath);
70416
+ const sourceDir = dirname16(packageJsonPath);
70062
70417
  const hasher = createHash10("sha256");
70063
70418
  hasher.update(`package.json
70064
70419
  `);
70065
70420
  hasher.update(readFileSync49(packageJsonPath));
70066
70421
  for (const lockName of ALL_LOCKFILES) {
70067
70422
  const lockPath = join46(sourceDir, lockName);
70068
- if (existsSync51(lockPath)) {
70423
+ if (existsSync52(lockPath)) {
70069
70424
  hasher.update(`
70070
70425
  `);
70071
70426
  hasher.update(lockName);
@@ -70080,16 +70435,16 @@ function ensureNodeEnv(opts) {
70080
70435
  const { skillName, packageJsonPath, force = false } = opts;
70081
70436
  const cacheRoot = opts.cacheRoot ?? defaultNodeCacheRoot();
70082
70437
  const installer = opts.installer ?? "bun";
70083
- if (!existsSync51(packageJsonPath)) {
70438
+ if (!existsSync52(packageJsonPath)) {
70084
70439
  throw new NodeEnvError(`package.json not found: ${packageJsonPath}`);
70085
70440
  }
70086
- const sourceDir = dirname15(packageJsonPath);
70441
+ const sourceDir = dirname16(packageJsonPath);
70087
70442
  const envDir = join46(cacheRoot, skillName);
70088
70443
  const stampPath = join46(envDir, ".package.sha256");
70089
70444
  const nodeModulesDir = join46(envDir, "node_modules");
70090
70445
  const binDir = join46(nodeModulesDir, ".bin");
70091
70446
  const targetHash = hashDepInputs(packageJsonPath);
70092
- if (!force && existsSync51(stampPath) && existsSync51(nodeModulesDir)) {
70447
+ if (!force && existsSync52(stampPath) && existsSync52(nodeModulesDir)) {
70093
70448
  const existingHash = readFileSync49(stampPath, "utf8").trim();
70094
70449
  if (existingHash === targetHash) {
70095
70450
  return {
@@ -70101,7 +70456,7 @@ function ensureNodeEnv(opts) {
70101
70456
  };
70102
70457
  }
70103
70458
  }
70104
- if (existsSync51(envDir)) {
70459
+ if (existsSync52(envDir)) {
70105
70460
  rmSync14(envDir, { recursive: true, force: true });
70106
70461
  }
70107
70462
  mkdirSync30(envDir, { recursive: true });
@@ -70109,7 +70464,7 @@ function ensureNodeEnv(opts) {
70109
70464
  let copiedLockfile = false;
70110
70465
  for (const lockName of LOCKFILES_FOR[installer]) {
70111
70466
  const lockPath = join46(sourceDir, lockName);
70112
- if (existsSync51(lockPath)) {
70467
+ if (existsSync52(lockPath)) {
70113
70468
  copyFileSync9(lockPath, join46(envDir, lockName));
70114
70469
  copiedLockfile = true;
70115
70470
  }
@@ -70145,22 +70500,22 @@ function registerDepsCommand(program3) {
70145
70500
  const deps = program3.command("deps").description("Manage cached per-skill dependency environments");
70146
70501
  deps.command("rebuild <skill>").description("Rebuild the Python venv and/or Node node_modules cache for a skill").option("-p, --python", "Rebuild only the Python env").option("-n, --node", "Rebuild only the Node env").action(async (skill, opts) => {
70147
70502
  const skillsRoot = builtinSkillsRoot();
70148
- if (!existsSync52(skillsRoot)) {
70503
+ if (!existsSync53(skillsRoot)) {
70149
70504
  console.error(source_default.red(`Bundled skills pool dir not found at ${skillsRoot} \u2014 run \`switchroom update\` to install it.`));
70150
70505
  process.exit(1);
70151
70506
  }
70152
70507
  const skillDir = join47(skillsRoot, skill);
70153
- if (!existsSync52(skillDir)) {
70508
+ if (!existsSync53(skillDir)) {
70154
70509
  console.error(source_default.red(`Unknown skill: ${skill} (no dir at ${skillDir})`));
70155
70510
  process.exit(1);
70156
70511
  }
70157
70512
  const requirementsPath = join47(skillDir, "requirements.txt");
70158
70513
  const packageJsonPath = join47(skillDir, "package.json");
70159
- const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync52(requirementsPath));
70160
- const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync52(packageJsonPath));
70514
+ const wantPython = opts.python ?? (!opts.python && !opts.node && existsSync53(requirementsPath));
70515
+ const wantNode = opts.node ?? (!opts.python && !opts.node && existsSync53(packageJsonPath));
70161
70516
  let did = 0;
70162
70517
  if (wantPython) {
70163
- if (!existsSync52(requirementsPath)) {
70518
+ if (!existsSync53(requirementsPath)) {
70164
70519
  console.error(source_default.red(`Skill "${skill}" has no requirements.txt at ${requirementsPath}`));
70165
70520
  process.exit(1);
70166
70521
  }
@@ -70184,7 +70539,7 @@ function registerDepsCommand(program3) {
70184
70539
  }
70185
70540
  }
70186
70541
  if (wantNode) {
70187
- if (!existsSync52(packageJsonPath)) {
70542
+ if (!existsSync53(packageJsonPath)) {
70188
70543
  console.error(source_default.red(`Skill "${skill}" has no package.json at ${packageJsonPath}`));
70189
70544
  process.exit(1);
70190
70545
  }
@@ -70217,9 +70572,9 @@ function registerDepsCommand(program3) {
70217
70572
  // src/cli/workspace.ts
70218
70573
  init_helpers();
70219
70574
  init_loader();
70220
- import { existsSync as existsSync53 } from "node:fs";
70575
+ import { existsSync as existsSync54 } from "node:fs";
70221
70576
  import { resolve as resolve35, sep as sep2 } from "node:path";
70222
- import { spawnSync as spawnSync8 } from "node:child_process";
70577
+ import { spawnSync as spawnSync9 } from "node:child_process";
70223
70578
 
70224
70579
  // src/agents/workspace.ts
70225
70580
  import { readFile, stat } from "node:fs/promises";
@@ -70932,7 +71287,7 @@ function registerWorkspaceCommand(program3) {
70932
71287
  process.exit(1);
70933
71288
  }
70934
71289
  const editor = process.env["EDITOR"] ?? process.env["VISUAL"] ?? "vi";
70935
- const child = spawnSync8(editor, [target], { stdio: "inherit" });
71290
+ const child = spawnSync9(editor, [target], { stdio: "inherit" });
70936
71291
  if (child.status !== 0 && child.status !== null) {
70937
71292
  process.exit(child.status);
70938
71293
  }
@@ -70994,12 +71349,12 @@ function registerWorkspaceCommand(program3) {
70994
71349
  if (!dir)
70995
71350
  return;
70996
71351
  const gitDir = resolve35(dir, ".git");
70997
- if (!existsSync53(gitDir)) {
71352
+ if (!existsSync54(gitDir)) {
70998
71353
  process.stdout.write(`Workspace is not a git repository. Re-run \`switchroom agent create ${agentName}\` ` + `or manually \`git init\` in ${dir} to enable versioning.
70999
71354
  `);
71000
71355
  return;
71001
71356
  }
71002
- const statusResult = spawnSync8("git", ["status", "--short"], {
71357
+ const statusResult = spawnSync9("git", ["status", "--short"], {
71003
71358
  cwd: dir,
71004
71359
  encoding: "utf-8"
71005
71360
  });
@@ -71014,7 +71369,7 @@ function registerWorkspaceCommand(program3) {
71014
71369
  return;
71015
71370
  }
71016
71371
  const message = opts.message || `checkpoint: ${new Date().toISOString()}`;
71017
- const addResult = spawnSync8("git", ["add", "-A"], {
71372
+ const addResult = spawnSync9("git", ["add", "-A"], {
71018
71373
  cwd: dir,
71019
71374
  encoding: "utf-8"
71020
71375
  });
@@ -71023,7 +71378,7 @@ function registerWorkspaceCommand(program3) {
71023
71378
  `);
71024
71379
  process.exit(1);
71025
71380
  }
71026
- const commitResult = spawnSync8("git", ["commit", "-m", message], {
71381
+ const commitResult = spawnSync9("git", ["commit", "-m", message], {
71027
71382
  cwd: dir,
71028
71383
  encoding: "utf-8"
71029
71384
  });
@@ -71032,7 +71387,7 @@ function registerWorkspaceCommand(program3) {
71032
71387
  `);
71033
71388
  process.exit(1);
71034
71389
  }
71035
- const shaResult = spawnSync8("git", ["rev-parse", "--short", "HEAD"], {
71390
+ const shaResult = spawnSync9("git", ["rev-parse", "--short", "HEAD"], {
71036
71391
  cwd: dir,
71037
71392
  encoding: "utf-8"
71038
71393
  });
@@ -71048,12 +71403,12 @@ function registerWorkspaceCommand(program3) {
71048
71403
  if (!dir)
71049
71404
  return;
71050
71405
  const gitDir = resolve35(dir, ".git");
71051
- if (!existsSync53(gitDir)) {
71406
+ if (!existsSync54(gitDir)) {
71052
71407
  process.stdout.write(`Workspace is not a git repository.
71053
71408
  `);
71054
71409
  return;
71055
71410
  }
71056
- const child = spawnSync8("git", ["status", "--short"], {
71411
+ const child = spawnSync9("git", ["status", "--short"], {
71057
71412
  cwd: dir,
71058
71413
  stdio: "inherit"
71059
71414
  });
@@ -71073,7 +71428,7 @@ function resolveAgentWorkspaceDirOrExit(program3, agentName) {
71073
71428
  const agentsDir = resolveAgentsDir(config);
71074
71429
  const agentDir = resolve35(agentsDir, agentName);
71075
71430
  const dir = resolveAgentWorkspaceDir(agentDir);
71076
- if (!existsSync53(dir)) {
71431
+ if (!existsSync54(dir)) {
71077
71432
  process.stderr.write(`workspace: ${dir} does not exist yet. Run \`switchroom setup\` or \`switchroom agent scaffold ${agentName}\` to seed it.
71078
71433
  `);
71079
71434
  return;
@@ -71109,7 +71464,7 @@ function safeParseInt(value, fallback) {
71109
71464
  init_helpers();
71110
71465
  init_loader();
71111
71466
  init_merge();
71112
- import { copyFileSync as copyFileSync10, existsSync as existsSync54, readFileSync as readFileSync50, writeFileSync as writeFileSync28 } from "node:fs";
71467
+ import { copyFileSync as copyFileSync10, existsSync as existsSync55, readFileSync as readFileSync50, writeFileSync as writeFileSync28 } from "node:fs";
71113
71468
  import { join as join48, resolve as resolve36 } from "node:path";
71114
71469
  init_schema();
71115
71470
  function resolveSoulTargetOrExit(program3, agentName) {
@@ -71125,7 +71480,7 @@ function resolveSoulTargetOrExit(program3, agentName) {
71125
71480
  const agentsDir = resolveAgentsDir(config);
71126
71481
  const agentDir = resolve36(agentsDir, agentName);
71127
71482
  const workspaceDir = resolveAgentWorkspaceDir(agentDir);
71128
- if (!existsSync54(workspaceDir)) {
71483
+ if (!existsSync55(workspaceDir)) {
71129
71484
  console.error(`soul: ${workspaceDir} does not exist yet. Run \`switchroom setup\` ` + `or \`switchroom agent scaffold ${agentName}\` to seed it.`);
71130
71485
  process.exit(1);
71131
71486
  }
@@ -71151,7 +71506,7 @@ function registerSoulCommand(program3) {
71151
71506
  const t = resolveSoulTargetOrExit(program3, agentName);
71152
71507
  if (!t)
71153
71508
  return;
71154
- if (!existsSync54(t.soulPath)) {
71509
+ if (!existsSync55(t.soulPath)) {
71155
71510
  console.error(`soul: ${t.soulPath} does not exist yet \u2014 run ` + `\`switchroom soul reset ${agentName}\` to seed it.`);
71156
71511
  process.exit(1);
71157
71512
  }
@@ -71166,7 +71521,7 @@ function registerSoulCommand(program3) {
71166
71521
  console.error(`soul: profile "${t.profileName}" ships no SOUL.md.hbs \u2014 ` + `nothing to re-seed from.`);
71167
71522
  process.exit(1);
71168
71523
  }
71169
- const exists = existsSync54(t.soulPath);
71524
+ const exists = existsSync55(t.soulPath);
71170
71525
  if (exists && !opts.yes) {
71171
71526
  if (!isInteractive()) {
71172
71527
  console.error(`soul: ${t.soulPath} already exists. Re-run with --yes to ` + `replace it (the current file is backed up to SOUL.md.bak).`);
@@ -71181,7 +71536,7 @@ function registerSoulCommand(program3) {
71181
71536
  let backupPath;
71182
71537
  if (exists) {
71183
71538
  backupPath = `${t.soulPath}.bak`;
71184
- if (existsSync54(backupPath)) {
71539
+ if (existsSync55(backupPath)) {
71185
71540
  backupPath = `${t.soulPath}.bak.${Date.now()}`;
71186
71541
  }
71187
71542
  copyFileSync10(t.soulPath, backupPath);
@@ -71200,7 +71555,7 @@ function registerSoulCommand(program3) {
71200
71555
  // src/cli/debug.ts
71201
71556
  init_helpers();
71202
71557
  init_loader();
71203
- import { existsSync as existsSync55, readFileSync as readFileSync51, readdirSync as readdirSync19, statSync as statSync23 } from "node:fs";
71558
+ import { existsSync as existsSync56, readFileSync as readFileSync51, readdirSync as readdirSync19, statSync as statSync24 } from "node:fs";
71204
71559
  import { resolve as resolve37, join as join49 } from "node:path";
71205
71560
  import { createHash as createHash11 } from "node:crypto";
71206
71561
  init_merge();
@@ -71216,7 +71571,7 @@ function sha256(content) {
71216
71571
  }
71217
71572
  function findLatestTranscriptJsonl(claudeConfigDir) {
71218
71573
  const projectsDir = join49(claudeConfigDir, "projects");
71219
- if (!existsSync55(projectsDir))
71574
+ if (!existsSync56(projectsDir))
71220
71575
  return;
71221
71576
  try {
71222
71577
  const entries = readdirSync19(projectsDir, { withFileTypes: true });
@@ -71226,9 +71581,9 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
71226
71581
  continue;
71227
71582
  const projectPath = join49(projectsDir, entry.name);
71228
71583
  const transcriptPath = join49(projectPath, "transcript.jsonl");
71229
- if (!existsSync55(transcriptPath))
71584
+ if (!existsSync56(transcriptPath))
71230
71585
  continue;
71231
- const stat3 = statSync23(transcriptPath);
71586
+ const stat3 = statSync24(transcriptPath);
71232
71587
  if (!latest || stat3.mtimeMs > latest.mtime) {
71233
71588
  latest = { path: transcriptPath, mtime: stat3.mtimeMs };
71234
71589
  }
@@ -71289,7 +71644,7 @@ function registerDebugCommand(program3) {
71289
71644
  }
71290
71645
  const agentsDir = resolveAgentsDir(config);
71291
71646
  const agentDir = resolve37(agentsDir, agentName);
71292
- if (!existsSync55(agentDir)) {
71647
+ if (!existsSync56(agentDir)) {
71293
71648
  console.error(`Agent directory not found: ${agentDir}`);
71294
71649
  process.exit(1);
71295
71650
  }
@@ -71344,7 +71699,7 @@ function registerDebugCommand(program3) {
71344
71699
  }
71345
71700
  console.log(`=== Append System Prompt (per-session) ===
71346
71701
  `);
71347
- const handoffContent = existsSync55(handoffPath) ? readFileSync51(handoffPath, "utf-8") : "";
71702
+ const handoffContent = existsSync56(handoffPath) ? readFileSync51(handoffPath, "utf-8") : "";
71348
71703
  if (handoffContent.trim().length > 0) {
71349
71704
  console.log(`-- Handoff Briefing (${formatBytes(handoffContent.length)}) --`);
71350
71705
  console.log(handoffContent);
@@ -71355,7 +71710,7 @@ function registerDebugCommand(program3) {
71355
71710
  }
71356
71711
  console.log(`=== CLAUDE.md (auto-loaded by Claude Code) ===
71357
71712
  `);
71358
- const claudeMdContent = existsSync55(claudeMdPath) ? readFileSync51(claudeMdPath, "utf-8") : "";
71713
+ const claudeMdContent = existsSync56(claudeMdPath) ? readFileSync51(claudeMdPath, "utf-8") : "";
71359
71714
  if (claudeMdContent.trim().length > 0) {
71360
71715
  console.log(`(${formatBytes(claudeMdContent.length)})`);
71361
71716
  console.log(claudeMdContent);
@@ -71366,7 +71721,7 @@ function registerDebugCommand(program3) {
71366
71721
  }
71367
71722
  console.log(`=== Persona (SOUL.md) ===
71368
71723
  `);
71369
- const soulMdContent = existsSync55(soulMdPath) ? readFileSync51(soulMdPath, "utf-8") : existsSync55(workspaceSoulMdPath) ? readFileSync51(workspaceSoulMdPath, "utf-8") : "";
71724
+ const soulMdContent = existsSync56(soulMdPath) ? readFileSync51(soulMdPath, "utf-8") : existsSync56(workspaceSoulMdPath) ? readFileSync51(workspaceSoulMdPath, "utf-8") : "";
71370
71725
  if (soulMdContent.trim().length > 0) {
71371
71726
  console.log(`(${formatBytes(soulMdContent.length)})`);
71372
71727
  console.log(soulMdContent);
@@ -71447,7 +71802,7 @@ init_source();
71447
71802
 
71448
71803
  // src/worktree/claim.ts
71449
71804
  import { execFileSync as execFileSync16 } from "node:child_process";
71450
- import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as existsSync57, unlinkSync as unlinkSync12 } from "node:fs";
71805
+ import { closeSync as closeSync11, mkdirSync as mkdirSync32, openSync as openSync11, existsSync as existsSync58, unlinkSync as unlinkSync12 } from "node:fs";
71451
71806
  import { join as join51, resolve as resolve39 } from "node:path";
71452
71807
  import { homedir as homedir27 } from "node:os";
71453
71808
  import { randomBytes as randomBytes11 } from "node:crypto";
@@ -71459,7 +71814,7 @@ import {
71459
71814
  readFileSync as readFileSync52,
71460
71815
  readdirSync as readdirSync20,
71461
71816
  unlinkSync as unlinkSync11,
71462
- existsSync as existsSync56,
71817
+ existsSync as existsSync57,
71463
71818
  renameSync as renameSync11
71464
71819
  } from "node:fs";
71465
71820
  import { join as join50, resolve as resolve38 } from "node:path";
@@ -71573,7 +71928,7 @@ function expandHome(p) {
71573
71928
  }
71574
71929
  async function claimWorktree(input, codeRepos) {
71575
71930
  const repoPath = resolveRepoPath(input.repo, codeRepos);
71576
- if (!existsSync57(repoPath)) {
71931
+ if (!existsSync58(repoPath)) {
71577
71932
  throw new Error(`Repository path does not exist: ${repoPath}`);
71578
71933
  }
71579
71934
  let concurrencyCap = DEFAULT_CONCURRENCY;
@@ -71627,7 +71982,7 @@ async function claimWorktree(input, codeRepos) {
71627
71982
 
71628
71983
  // src/worktree/release.ts
71629
71984
  import { execFileSync as execFileSync17 } from "node:child_process";
71630
- import { existsSync as existsSync58 } from "node:fs";
71985
+ import { existsSync as existsSync59 } from "node:fs";
71631
71986
  function releaseWorktree(input) {
71632
71987
  const { id } = input;
71633
71988
  const record2 = readRecord(id);
@@ -71635,7 +71990,7 @@ function releaseWorktree(input) {
71635
71990
  return { released: true };
71636
71991
  }
71637
71992
  let gitSuccess = true;
71638
- if (existsSync58(record2.path)) {
71993
+ if (existsSync59(record2.path)) {
71639
71994
  try {
71640
71995
  execFileSync17("git", ["worktree", "remove", "--force", record2.path], {
71641
71996
  cwd: record2.repo,
@@ -71674,7 +72029,7 @@ function listWorktrees() {
71674
72029
 
71675
72030
  // src/worktree/reaper.ts
71676
72031
  import { execFileSync as execFileSync18 } from "node:child_process";
71677
- import { existsSync as existsSync59 } from "node:fs";
72032
+ import { existsSync as existsSync60 } from "node:fs";
71678
72033
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
71679
72034
  function isPathInUse(path7) {
71680
72035
  try {
@@ -71701,7 +72056,7 @@ function hasUncommittedChanges(repoPath, worktreePath) {
71701
72056
  function reapRecord(record2) {
71702
72057
  const { id, path: path7, repo, branch, ownerAgent } = record2;
71703
72058
  let warning = null;
71704
- if (existsSync59(path7)) {
72059
+ if (existsSync60(path7)) {
71705
72060
  if (hasUncommittedChanges(repo, path7)) {
71706
72061
  warning = `[worktree-reaper] Reaped worktree with uncommitted changes: ` + `id=${id} branch=${branch} agent=${ownerAgent ?? "unknown"} path=${path7}`;
71707
72062
  }
@@ -71722,7 +72077,7 @@ function runReaper(nowMs) {
71722
72077
  const warnings = [];
71723
72078
  for (const record2 of records) {
71724
72079
  const heartbeatAge = now - new Date(record2.heartbeatAt).getTime();
71725
- const worktreeExists = existsSync59(record2.path);
72080
+ const worktreeExists = existsSync60(record2.path);
71726
72081
  if (!worktreeExists) {
71727
72082
  deleteRecord(record2.id);
71728
72083
  reaped.push(record2.id);
@@ -72177,7 +72532,7 @@ function registerDriveMcpLauncherCommand(program3) {
72177
72532
 
72178
72533
  // src/cli/apply.ts
72179
72534
  init_source();
72180
- import { accessSync as accessSync2, constants as fsConstants5, copyFileSync as copyFileSync11, existsSync as existsSync62, mkdirSync as mkdirSync35, readdirSync as readdirSync22, renameSync as renameSync12, writeFileSync as writeFileSync32 } from "node:fs";
72535
+ import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync64, mkdirSync as mkdirSync35, readdirSync as readdirSync23, renameSync as renameSync12, writeFileSync as writeFileSync32 } from "node:fs";
72181
72536
  import { mkdir, writeFile } from "node:fs/promises";
72182
72537
  import { spawnSync as childSpawnSync } from "node:child_process";
72183
72538
  import readline from "node:readline";
@@ -72522,7 +72877,7 @@ agents:
72522
72877
 
72523
72878
  // src/cli/apply.ts
72524
72879
  init_resolver();
72525
- import { dirname as dirname18, join as join55, resolve as resolve41 } from "node:path";
72880
+ import { dirname as dirname19, join as join56, resolve as resolve41 } from "node:path";
72526
72881
  import { homedir as homedir29 } from "node:os";
72527
72882
  import { execFileSync as execFileSync19 } from "node:child_process";
72528
72883
  init_vault();
@@ -72530,7 +72885,7 @@ init_loader();
72530
72885
  init_loader();
72531
72886
 
72532
72887
  // src/cli/update-prompt-hook.ts
72533
- import { existsSync as existsSync60, readFileSync as readFileSync53, writeFileSync as writeFileSync31, chmodSync as chmodSync10, mkdirSync as mkdirSync34 } from "node:fs";
72888
+ import { existsSync as existsSync61, readFileSync as readFileSync53, writeFileSync as writeFileSync31, chmodSync as chmodSync10, mkdirSync as mkdirSync34 } from "node:fs";
72534
72889
  import { join as join53 } from "node:path";
72535
72890
  var HOOK_FILENAME = "update-card-on-prompt.sh";
72536
72891
  function updatePromptHookScript() {
@@ -72602,7 +72957,7 @@ function installUpdatePromptHook(agentDir) {
72602
72957
  const scriptPath = join53(hooksDir, HOOK_FILENAME);
72603
72958
  const desired = updatePromptHookScript();
72604
72959
  let installed = false;
72605
- const existing = existsSync60(scriptPath) ? readFileSync53(scriptPath, "utf-8") : "";
72960
+ const existing = existsSync61(scriptPath) ? readFileSync53(scriptPath, "utf-8") : "";
72606
72961
  if (existing !== desired) {
72607
72962
  writeFileSync31(scriptPath, desired, { mode: 493 });
72608
72963
  chmodSync10(scriptPath, 493);
@@ -72613,7 +72968,7 @@ function installUpdatePromptHook(agentDir) {
72613
72968
  } catch {}
72614
72969
  }
72615
72970
  const settingsPath = join53(agentDir, ".claude", "settings.json");
72616
- if (!existsSync60(settingsPath)) {
72971
+ if (!existsSync61(settingsPath)) {
72617
72972
  return { scriptPath, settingsPath, installed };
72618
72973
  }
72619
72974
  const raw = readFileSync53(settingsPath, "utf-8");
@@ -72729,24 +73084,114 @@ function detectInstallType() {
72729
73084
  }
72730
73085
  }
72731
73086
 
73087
+ // src/cli/operator-uid.ts
73088
+ import {
73089
+ chownSync as chownSync2,
73090
+ existsSync as existsSync63,
73091
+ lstatSync as lstatSync7,
73092
+ readdirSync as readdirSync22,
73093
+ realpathSync as realpathSync6,
73094
+ statSync as statSync25
73095
+ } from "node:fs";
73096
+ import { join as join55 } from "node:path";
73097
+ function resolveOperatorUid() {
73098
+ const sudoUid = process.env.SUDO_UID;
73099
+ if (sudoUid !== undefined) {
73100
+ const parsed = parseInt(sudoUid, 10);
73101
+ if (Number.isFinite(parsed) && parsed > 0)
73102
+ return parsed;
73103
+ }
73104
+ if (typeof process.getuid === "function") {
73105
+ const uid = process.getuid();
73106
+ if (uid > 0)
73107
+ return uid;
73108
+ }
73109
+ return;
73110
+ }
73111
+ function operatorOwnedPaths(home2) {
73112
+ const root = join55(home2, ".switchroom");
73113
+ return [
73114
+ join55(root, "vault"),
73115
+ join55(root, "vault-auto-unlock"),
73116
+ join55(root, "vault-audit.log"),
73117
+ join55(root, "host-control-audit.log"),
73118
+ join55(root, "accounts"),
73119
+ join55(root, "compose")
73120
+ ];
73121
+ }
73122
+ function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
73123
+ const chown = deps.chown ?? ((p, u, g) => chownSync2(p, u, g));
73124
+ const exists = deps.exists ?? ((p) => existsSync63(p));
73125
+ const isSymlink = deps.isSymlink ?? ((p) => {
73126
+ try {
73127
+ return lstatSync7(p).isSymbolicLink();
73128
+ } catch {
73129
+ return false;
73130
+ }
73131
+ });
73132
+ const isDir = deps.isDir ?? ((p) => {
73133
+ try {
73134
+ return statSync25(p).isDirectory();
73135
+ } catch {
73136
+ return false;
73137
+ }
73138
+ });
73139
+ const realpath2 = deps.realpath ?? ((p) => {
73140
+ try {
73141
+ return realpathSync6(p);
73142
+ } catch {
73143
+ return p;
73144
+ }
73145
+ });
73146
+ const readdir2 = deps.readdir ?? ((p) => {
73147
+ try {
73148
+ return readdirSync22(p);
73149
+ } catch {
73150
+ return [];
73151
+ }
73152
+ });
73153
+ const chowned = [];
73154
+ const seen = new Set;
73155
+ const visit = (path8) => {
73156
+ if (!exists(path8))
73157
+ return;
73158
+ const target = isSymlink(path8) ? realpath2(path8) : path8;
73159
+ if (seen.has(target))
73160
+ return;
73161
+ seen.add(target);
73162
+ try {
73163
+ chown(target, operatorUid, operatorUid);
73164
+ chowned.push(target);
73165
+ } catch {}
73166
+ if (isDir(target)) {
73167
+ for (const entry of readdir2(target)) {
73168
+ visit(join55(target, entry));
73169
+ }
73170
+ }
73171
+ };
73172
+ for (const p of operatorOwnedPaths(home2))
73173
+ visit(p);
73174
+ return chowned;
73175
+ }
73176
+
72732
73177
  // src/cli/apply.ts
72733
73178
  var EMBEDDED_EXAMPLES = {
72734
73179
  switchroom: switchroom_default,
72735
73180
  minimal: minimal_default
72736
73181
  };
72737
- var DEFAULT_COMPOSE_PATH2 = join55(homedir29(), ".switchroom", "compose", "docker-compose.yml");
73182
+ var DEFAULT_COMPOSE_PATH2 = join56(homedir29(), ".switchroom", "compose", "docker-compose.yml");
72738
73183
  var COMPOSE_PROJECT2 = "switchroom";
72739
73184
  function resolveVaultBindMountDir(homeDir, ctx) {
72740
73185
  const isCustomPath = ctx.migrationKind === "custom-path-skipped";
72741
73186
  if (isCustomPath && ctx.customVaultPath) {
72742
- return dirname18(ctx.customVaultPath);
73187
+ return dirname19(ctx.customVaultPath);
72743
73188
  }
72744
- return join55(homeDir, ".switchroom", "vault");
73189
+ return join56(homeDir, ".switchroom", "vault");
72745
73190
  }
72746
73191
  function inspectVaultBindMountDir(vaultDir) {
72747
- if (!existsSync62(vaultDir))
73192
+ if (!existsSync64(vaultDir))
72748
73193
  return { kind: "missing" };
72749
- const entries = readdirSync22(vaultDir);
73194
+ const entries = readdirSync23(vaultDir);
72750
73195
  const unknown = [];
72751
73196
  for (const name of entries) {
72752
73197
  if (KNOWN_VAULT_ARTIFACT_NAMES.has(name))
@@ -72772,30 +73217,30 @@ function hasVaultRefs(value) {
72772
73217
  async function ensureHostMountSources(config) {
72773
73218
  const home2 = homedir29();
72774
73219
  const dirs = [
72775
- join55(home2, ".switchroom", "approvals"),
72776
- join55(home2, ".switchroom", "scheduler"),
72777
- join55(home2, ".switchroom", "logs"),
72778
- join55(home2, ".switchroom", "compose"),
72779
- join55(home2, ".switchroom", "broker-operator")
73220
+ join56(home2, ".switchroom", "approvals"),
73221
+ join56(home2, ".switchroom", "scheduler"),
73222
+ join56(home2, ".switchroom", "logs"),
73223
+ join56(home2, ".switchroom", "compose"),
73224
+ join56(home2, ".switchroom", "broker-operator")
72780
73225
  ];
72781
73226
  for (const name of Object.keys(config.agents)) {
72782
- dirs.push(join55(home2, ".switchroom", "agents", name));
72783
- dirs.push(join55(home2, ".switchroom", "logs", name));
72784
- dirs.push(join55(home2, ".claude", "projects", name));
73227
+ dirs.push(join56(home2, ".switchroom", "agents", name));
73228
+ dirs.push(join56(home2, ".switchroom", "logs", name));
73229
+ dirs.push(join56(home2, ".claude", "projects", name));
72785
73230
  }
72786
73231
  for (const dir of dirs) {
72787
73232
  await mkdir(dir, { recursive: true });
72788
73233
  }
72789
- const autoUnlockPath = join55(home2, ".switchroom", "vault-auto-unlock");
72790
- if (!existsSync62(autoUnlockPath)) {
73234
+ const autoUnlockPath = join56(home2, ".switchroom", "vault-auto-unlock");
73235
+ if (!existsSync64(autoUnlockPath)) {
72791
73236
  writeFileSync32(autoUnlockPath, "", { mode: 384 });
72792
73237
  }
72793
- const auditLogPath = join55(home2, ".switchroom", "vault-audit.log");
72794
- if (!existsSync62(auditLogPath)) {
73238
+ const auditLogPath = join56(home2, ".switchroom", "vault-audit.log");
73239
+ if (!existsSync64(auditLogPath)) {
72795
73240
  writeFileSync32(auditLogPath, "", { mode: 420 });
72796
73241
  }
72797
- const hostdAuditLogPath = join55(home2, ".switchroom", "host-control-audit.log");
72798
- if (!existsSync62(hostdAuditLogPath)) {
73242
+ const hostdAuditLogPath = join56(home2, ".switchroom", "host-control-audit.log");
73243
+ if (!existsSync64(hostdAuditLogPath)) {
72799
73244
  writeFileSync32(hostdAuditLogPath, "", { mode: 420 });
72800
73245
  }
72801
73246
  }
@@ -72816,7 +73261,7 @@ ${out.trim()}`;
72816
73261
  }
72817
73262
  function runApplyPreflight(config, opts = {}) {
72818
73263
  const vaultPath = resolvePath(config.vault?.path ?? "~/.switchroom/vault.enc");
72819
- if (hasVaultRefs(config) && !existsSync62(vaultPath)) {
73264
+ if (hasVaultRefs(config) && !existsSync64(vaultPath)) {
72820
73265
  throw new Error(`Config references vault keys (vault:<name>) but ${vaultPath} is missing. ` + `Run \`switchroom setup\` first to initialise the vault.`);
72821
73266
  }
72822
73267
  const detect = opts.detectComposeV2 ?? detectComposeV2;
@@ -72827,7 +73272,7 @@ function runApplyPreflight(config, opts = {}) {
72827
73272
  detectAndReportLegacyGdriveSlots(vaultPath);
72828
73273
  }
72829
73274
  function detectAndReportLegacyGdriveSlots(vaultPath) {
72830
- if (!existsSync62(vaultPath))
73275
+ if (!existsSync64(vaultPath))
72831
73276
  return;
72832
73277
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
72833
73278
  if (!passphrase)
@@ -72868,8 +73313,8 @@ function detectAndReportLegacyGdriveSlots(vaultPath) {
72868
73313
  }
72869
73314
  function writeInstallTypeCache(homeDir = homedir29()) {
72870
73315
  const ctx = detectInstallType();
72871
- const dir = join55(homeDir, ".switchroom");
72872
- const out = join55(dir, "install-type.json");
73316
+ const dir = join56(homeDir, ".switchroom");
73317
+ const out = join56(dir, "install-type.json");
72873
73318
  const tmp = `${out}.tmp`;
72874
73319
  mkdirSync35(dir, { recursive: true });
72875
73320
  const payload = {
@@ -72918,14 +73363,14 @@ Applying switchroom config...
72918
73363
  writeOut(source_default.green(` + ${name}`) + source_default.gray(` (${agentConfig.extends ?? "default"}) \u2014 ${detail}
72919
73364
  `));
72920
73365
  try {
72921
- installUpdatePromptHook(join55(agentsDir, name));
73366
+ installUpdatePromptHook(join56(agentsDir, name));
72922
73367
  } catch (hookErr) {
72923
73368
  writeOut(source_default.gray(` (update-prompt hook install failed for ${name}: ${hookErr.message})
72924
73369
  `));
72925
73370
  }
72926
73371
  try {
72927
73372
  const uid = allocateAgentUid(name);
72928
- alignAgentUid(name, join55(agentsDir, name), uid, {
73373
+ alignAgentUid(name, join56(agentsDir, name), uid, {
72929
73374
  confirm: !options.nonInteractive,
72930
73375
  writeOut
72931
73376
  });
@@ -72962,7 +73407,7 @@ Applying switchroom config...
72962
73407
  for (const name of agentNames) {
72963
73408
  try {
72964
73409
  const uid = allocateAgentUid(name);
72965
- alignAgentUid(name, join55(agentsDir, name), uid, {
73410
+ alignAgentUid(name, join56(agentsDir, name), uid, {
72966
73411
  confirm: !options.nonInteractive,
72967
73412
  writeOut
72968
73413
  });
@@ -73036,20 +73481,7 @@ Applying switchroom config...
73036
73481
  process.exit(6);
73037
73482
  }
73038
73483
  const composePath = options.outPath ?? DEFAULT_COMPOSE_PATH2;
73039
- const operatorUid = (() => {
73040
- const sudoUid = process.env.SUDO_UID;
73041
- if (sudoUid !== undefined) {
73042
- const parsed = parseInt(sudoUid, 10);
73043
- if (Number.isFinite(parsed) && parsed > 0)
73044
- return parsed;
73045
- }
73046
- if (typeof process.getuid === "function") {
73047
- const uid = process.getuid();
73048
- if (uid > 0)
73049
- return uid;
73050
- }
73051
- return;
73052
- })();
73484
+ const operatorUid = resolveOperatorUid();
73053
73485
  const composeRelease = resolveRelease({
73054
73486
  override: options.releaseOverride,
73055
73487
  root: config.release
@@ -73064,7 +73496,7 @@ Applying switchroom config...
73064
73496
  switchroomConfigPath,
73065
73497
  operatorUid
73066
73498
  });
73067
- await mkdir(dirname18(composePath), { recursive: true });
73499
+ await mkdir(dirname19(composePath), { recursive: true });
73068
73500
  await writeFile(composePath, composeContent, {
73069
73501
  encoding: "utf8",
73070
73502
  mode: 384
@@ -73079,6 +73511,13 @@ Wrote `) + composePath + source_default.gray(` (${composeBytes} bytes)
73079
73511
  `);
73080
73512
  writeOut(source_default.gray(` (If pull returns 401, login to ghcr.io first: see docs/operators/install.md#ghcr-auth)
73081
73513
  `));
73514
+ if (process.geteuid?.() === 0 && operatorUid !== undefined) {
73515
+ const restored = restoreOperatorOwnership(homedir29(), operatorUid);
73516
+ if (restored.length > 0) {
73517
+ writeOut(source_default.gray(` Restored operator ownership of ${restored.length} ~/.switchroom path(s)
73518
+ `));
73519
+ }
73520
+ }
73082
73521
  writeOut(source_default.bold(`
73083
73522
  Done. Scaffolded ${scaffolded}/${allAgentNames.length} agents.
73084
73523
  `));
@@ -73121,7 +73560,7 @@ function copyExampleConfig2(name) {
73121
73560
  throw new Error(`Invalid example name: ${name} (must match /^[a-z0-9_-]+$/)`);
73122
73561
  }
73123
73562
  const dest = resolve41(process.cwd(), "switchroom.yaml");
73124
- if (existsSync62(dest)) {
73563
+ if (existsSync64(dest)) {
73125
73564
  console.error(source_default.yellow("switchroom.yaml already exists \u2014 skipping example copy"));
73126
73565
  return;
73127
73566
  }
@@ -73132,7 +73571,7 @@ function copyExampleConfig2(name) {
73132
73571
  return;
73133
73572
  }
73134
73573
  const exampleFile = resolve41(import.meta.dirname, `../../examples/${name}.yaml`);
73135
- if (!existsSync62(exampleFile)) {
73574
+ if (!existsSync64(exampleFile)) {
73136
73575
  throw new Error(`Example config not found: ${name}.yaml (available: ${Object.keys(EMBEDDED_EXAMPLES).join(", ")})`);
73137
73576
  }
73138
73577
  copyFileSync11(exampleFile, dest);
@@ -73143,11 +73582,11 @@ function findUnwritableAgentDirs(config, opts) {
73143
73582
  const targets = opts.only ? [opts.only] : Object.keys(config.agents ?? {});
73144
73583
  const unwritable = [];
73145
73584
  for (const name of targets) {
73146
- const startSh = join55(agentsDir, name, "start.sh");
73147
- if (!existsSync62(startSh))
73585
+ const startSh = join56(agentsDir, name, "start.sh");
73586
+ if (!existsSync64(startSh))
73148
73587
  continue;
73149
73588
  try {
73150
- accessSync2(startSh, fsConstants5.W_OK);
73589
+ accessSync3(startSh, fsConstants6.W_OK);
73151
73590
  } catch {
73152
73591
  unwritable.push(name);
73153
73592
  }
@@ -73322,8 +73761,8 @@ function runRedactStdin() {
73322
73761
  }
73323
73762
 
73324
73763
  // src/cli/status-ask.ts
73325
- import { readFileSync as readFileSync54, existsSync as existsSync63, readdirSync as readdirSync23 } from "node:fs";
73326
- import { join as join56 } from "node:path";
73764
+ import { readFileSync as readFileSync54, existsSync as existsSync65, readdirSync as readdirSync24 } from "node:fs";
73765
+ import { join as join57 } from "node:path";
73327
73766
  import { homedir as homedir30 } from "node:os";
73328
73767
 
73329
73768
  // src/status-ask/report.ts
@@ -73645,7 +74084,7 @@ function runReport(opts) {
73645
74084
  function resolveSources(explicitPath) {
73646
74085
  if (explicitPath != null && explicitPath.trim() !== "") {
73647
74086
  const trimmed = explicitPath.trim();
73648
- if (!existsSync63(trimmed)) {
74087
+ if (!existsSync65(trimmed)) {
73649
74088
  process.stderr.write(`status-ask report: ${trimmed}: file not found
73650
74089
  `);
73651
74090
  process.exit(1);
@@ -73659,20 +74098,20 @@ function resolveSources(explicitPath) {
73659
74098
  const config = loadConfig();
73660
74099
  agentsDir = resolveAgentsDir(config);
73661
74100
  } catch {
73662
- agentsDir = join56(homedir30(), ".switchroom", "agents");
74101
+ agentsDir = join57(homedir30(), ".switchroom", "agents");
73663
74102
  }
73664
- if (!existsSync63(agentsDir))
74103
+ if (!existsSync65(agentsDir))
73665
74104
  return [];
73666
74105
  const sources = [];
73667
74106
  let entries;
73668
74107
  try {
73669
- entries = readdirSync23(agentsDir);
74108
+ entries = readdirSync24(agentsDir);
73670
74109
  } catch {
73671
74110
  return [];
73672
74111
  }
73673
74112
  for (const name of entries) {
73674
- const path8 = join56(agentsDir, name, "runtime-metrics.jsonl");
73675
- if (existsSync63(path8)) {
74113
+ const path8 = join57(agentsDir, name, "runtime-metrics.jsonl");
74114
+ if (existsSync65(path8)) {
73676
74115
  sources.push({ path: path8, agent: name });
73677
74116
  }
73678
74117
  }
@@ -73693,17 +74132,17 @@ function inferAgentFromPath(p) {
73693
74132
 
73694
74133
  // src/cli/agent-config.ts
73695
74134
  init_helpers();
73696
- import { join as join57 } from "node:path";
74135
+ import { join as join58 } from "node:path";
73697
74136
  import { homedir as homedir31 } from "node:os";
73698
74137
  import {
73699
- existsSync as existsSync64,
74138
+ existsSync as existsSync66,
73700
74139
  mkdirSync as mkdirSync36,
73701
74140
  appendFileSync as appendFileSync3,
73702
74141
  readFileSync as readFileSync55
73703
74142
  } from "node:fs";
73704
- var AUDIT_ROOT = join57(homedir31(), ".switchroom", "audit");
74143
+ var AUDIT_ROOT = join58(homedir31(), ".switchroom", "audit");
73705
74144
  function auditPathFor(agent) {
73706
- return join57(AUDIT_ROOT, agent, "agent-config.jsonl");
74145
+ return join58(AUDIT_ROOT, agent, "agent-config.jsonl");
73707
74146
  }
73708
74147
  function appendAudit(agent, cmd, args, exit, opts = {}) {
73709
74148
  const row = {
@@ -73717,7 +74156,7 @@ function appendAudit(agent, cmd, args, exit, opts = {}) {
73717
74156
  const path8 = opts.auditPath ?? auditPathFor(agent);
73718
74157
  const dir = path8.slice(0, path8.lastIndexOf("/"));
73719
74158
  try {
73720
- if (!existsSync64(dir)) {
74159
+ if (!existsSync66(dir)) {
73721
74160
  mkdirSync36(dir, { recursive: true });
73722
74161
  }
73723
74162
  appendFileSync3(path8, JSON.stringify(row) + `
@@ -73729,7 +74168,7 @@ function isContainerContext(env2 = process.env, opts = {}) {
73729
74168
  return true;
73730
74169
  const probe2 = opts.dockerEnvPath ?? "/.dockerenv";
73731
74170
  try {
73732
- if (existsSync64(probe2))
74171
+ if (existsSync66(probe2))
73733
74172
  return true;
73734
74173
  } catch {}
73735
74174
  return false;
@@ -73790,7 +74229,7 @@ function getAgentSlice(config, agent) {
73790
74229
  }
73791
74230
  function readAuditTail(agent, limit, opts = {}) {
73792
74231
  const path8 = opts.auditPath ?? auditPathFor(agent);
73793
- if (!existsSync64(path8))
74232
+ if (!existsSync66(path8))
73794
74233
  return [];
73795
74234
  let raw;
73796
74235
  try {
@@ -73950,32 +74389,32 @@ var import_yaml14 = __toESM(require_dist(), 1);
73950
74389
  init_paths();
73951
74390
  import {
73952
74391
  closeSync as closeSync12,
73953
- existsSync as existsSync65,
74392
+ existsSync as existsSync67,
73954
74393
  fsyncSync as fsyncSync5,
73955
74394
  mkdirSync as mkdirSync37,
73956
74395
  openSync as openSync12,
73957
- readdirSync as readdirSync24,
74396
+ readdirSync as readdirSync25,
73958
74397
  readFileSync as readFileSync56,
73959
74398
  renameSync as renameSync13,
73960
- statSync as statSync24,
74399
+ statSync as statSync26,
73961
74400
  unlinkSync as unlinkSync13,
73962
74401
  writeSync as writeSync7
73963
74402
  } from "node:fs";
73964
- import { join as join58, resolve as resolve42 } from "node:path";
74403
+ import { join as join59, resolve as resolve42 } from "node:path";
73965
74404
  var STAGING_SUBDIR = ".staging";
73966
74405
  function overlayPathsFor(agent, opts = {}) {
73967
74406
  const base = opts.root ? resolve42(opts.root, agent) : resolve42(resolveDualPath(`~/.switchroom/agents/${agent}`));
73968
- const scheduleDir = join58(base, "schedule.d");
73969
- const scheduleStagingDir = join58(scheduleDir, STAGING_SUBDIR);
73970
- const skillsDir = join58(base, "skills.d");
73971
- const skillsStagingDir = join58(skillsDir, STAGING_SUBDIR);
74407
+ const scheduleDir = join59(base, "schedule.d");
74408
+ const scheduleStagingDir = join59(scheduleDir, STAGING_SUBDIR);
74409
+ const skillsDir = join59(base, "skills.d");
74410
+ const skillsStagingDir = join59(skillsDir, STAGING_SUBDIR);
73972
74411
  return {
73973
74412
  agentRoot: base,
73974
74413
  scheduleDir,
73975
74414
  scheduleStagingDir,
73976
74415
  skillsDir,
73977
74416
  skillsStagingDir,
73978
- lockPath: join58(base, ".lock"),
74417
+ lockPath: join59(base, ".lock"),
73979
74418
  stagingDir: scheduleStagingDir
73980
74419
  };
73981
74420
  }
@@ -74001,7 +74440,7 @@ function withAgentLock(paths, fn) {
74001
74440
  if (e.code !== "EEXIST")
74002
74441
  throw err;
74003
74442
  try {
74004
- const age = Date.now() - statSync24(paths.lockPath).mtimeMs;
74443
+ const age = Date.now() - statSync26(paths.lockPath).mtimeMs;
74005
74444
  if (age > 30000) {
74006
74445
  unlinkSync13(paths.lockPath);
74007
74446
  continue;
@@ -74029,8 +74468,8 @@ function writeOverlayEntry(agent, slug, yamlText, opts = {}) {
74029
74468
  const paths = overlayPathsFor(agent, opts);
74030
74469
  return withAgentLock(paths, () => {
74031
74470
  ensureDirs(paths);
74032
- const stagingPath = join58(paths.scheduleStagingDir, `${slug}.yaml`);
74033
- const finalPath = join58(paths.scheduleDir, `${slug}.yaml`);
74471
+ const stagingPath = join59(paths.scheduleStagingDir, `${slug}.yaml`);
74472
+ const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
74034
74473
  const fd = openSync12(stagingPath, "w", 384);
74035
74474
  try {
74036
74475
  writeSync7(fd, yamlText);
@@ -74046,8 +74485,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
74046
74485
  const paths = overlayPathsFor(agent, opts);
74047
74486
  return withAgentLock(paths, () => {
74048
74487
  ensureSkillsDirs(paths);
74049
- const stagingPath = join58(paths.skillsStagingDir, `${slug}.yaml`);
74050
- const finalPath = join58(paths.skillsDir, `${slug}.yaml`);
74488
+ const stagingPath = join59(paths.skillsStagingDir, `${slug}.yaml`);
74489
+ const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
74051
74490
  const fd = openSync12(stagingPath, "w", 384);
74052
74491
  try {
74053
74492
  writeSync7(fd, yamlText);
@@ -74062,8 +74501,8 @@ function writeSkillsOverlayEntry(agent, slug, yamlText, opts = {}) {
74062
74501
  function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
74063
74502
  const paths = overlayPathsFor(agent, opts);
74064
74503
  return withAgentLock(paths, () => {
74065
- const finalPath = join58(paths.skillsDir, `${slug}.yaml`);
74066
- if (!existsSync65(finalPath))
74504
+ const finalPath = join59(paths.skillsDir, `${slug}.yaml`);
74505
+ if (!existsSync67(finalPath))
74067
74506
  return false;
74068
74507
  unlinkSync13(finalPath);
74069
74508
  return true;
@@ -74071,13 +74510,13 @@ function deleteSkillsOverlayEntry(agent, slug, opts = {}) {
74071
74510
  }
74072
74511
  function listSkillsOverlayEntries(agent, opts = {}) {
74073
74512
  const paths = overlayPathsFor(agent, opts);
74074
- if (!existsSync65(paths.skillsDir))
74513
+ if (!existsSync67(paths.skillsDir))
74075
74514
  return [];
74076
74515
  const out = [];
74077
- for (const name of readdirSync24(paths.skillsDir)) {
74516
+ for (const name of readdirSync25(paths.skillsDir)) {
74078
74517
  if (!/\.ya?ml$/i.test(name))
74079
74518
  continue;
74080
- const full = join58(paths.skillsDir, name);
74519
+ const full = join59(paths.skillsDir, name);
74081
74520
  try {
74082
74521
  const raw = readFileSync56(full, "utf-8");
74083
74522
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -74089,8 +74528,8 @@ function listSkillsOverlayEntries(agent, opts = {}) {
74089
74528
  function deleteOverlayEntry(agent, slug, opts = {}) {
74090
74529
  const paths = overlayPathsFor(agent, opts);
74091
74530
  return withAgentLock(paths, () => {
74092
- const finalPath = join58(paths.scheduleDir, `${slug}.yaml`);
74093
- if (!existsSync65(finalPath))
74531
+ const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
74532
+ if (!existsSync67(finalPath))
74094
74533
  return false;
74095
74534
  unlinkSync13(finalPath);
74096
74535
  return true;
@@ -74098,13 +74537,13 @@ function deleteOverlayEntry(agent, slug, opts = {}) {
74098
74537
  }
74099
74538
  function listOverlayEntries(agent, opts = {}) {
74100
74539
  const paths = overlayPathsFor(agent, opts);
74101
- if (!existsSync65(paths.scheduleDir))
74540
+ if (!existsSync67(paths.scheduleDir))
74102
74541
  return [];
74103
74542
  const out = [];
74104
- for (const name of readdirSync24(paths.scheduleDir)) {
74543
+ for (const name of readdirSync25(paths.scheduleDir)) {
74105
74544
  if (!/\.ya?ml$/i.test(name))
74106
74545
  continue;
74107
- const full = join58(paths.scheduleDir, name);
74546
+ const full = join59(paths.scheduleDir, name);
74108
74547
  try {
74109
74548
  const raw = readFileSync56(full, "utf-8");
74110
74549
  const slug = name.replace(/\.ya?ml$/i, "");
@@ -74249,23 +74688,23 @@ function reconcileAgentCronOnly(agent) {
74249
74688
  // src/cli/agent-config-pending.ts
74250
74689
  import {
74251
74690
  closeSync as closeSync13,
74252
- existsSync as existsSync66,
74691
+ existsSync as existsSync68,
74253
74692
  fsyncSync as fsyncSync6,
74254
74693
  mkdirSync as mkdirSync38,
74255
74694
  openSync as openSync13,
74256
- readdirSync as readdirSync25,
74695
+ readdirSync as readdirSync26,
74257
74696
  readFileSync as readFileSync57,
74258
74697
  renameSync as renameSync14,
74259
74698
  unlinkSync as unlinkSync14,
74260
74699
  writeFileSync as writeFileSync33,
74261
74700
  writeSync as writeSync8
74262
74701
  } from "node:fs";
74263
- import { join as join59 } from "node:path";
74702
+ import { join as join60 } from "node:path";
74264
74703
  import { randomBytes as randomBytes12 } from "node:crypto";
74265
74704
  var STAGE_ID_PREFIX = "cap_";
74266
74705
  function pendingDir(agent, opts = {}) {
74267
74706
  const paths = overlayPathsFor(agent, opts);
74268
- return join59(paths.scheduleDir, ".pending");
74707
+ return join60(paths.scheduleDir, ".pending");
74269
74708
  }
74270
74709
  function ensurePendingDir(agent, opts = {}) {
74271
74710
  const dir = pendingDir(agent, opts);
@@ -74278,8 +74717,8 @@ function newStageId() {
74278
74717
  function stagePendingScheduleEntry(opts) {
74279
74718
  const dir = ensurePendingDir(opts.agent, { root: opts.root });
74280
74719
  const stageId = opts.stageId ?? newStageId();
74281
- const yamlPath = join59(dir, `${stageId}.yaml`);
74282
- const metaPath = join59(dir, `${stageId}.meta.json`);
74720
+ const yamlPath = join60(dir, `${stageId}.yaml`);
74721
+ const metaPath = join60(dir, `${stageId}.meta.json`);
74283
74722
  const meta = {
74284
74723
  v: 1,
74285
74724
  stage_id: stageId,
@@ -74306,16 +74745,16 @@ function stagePendingScheduleEntry(opts) {
74306
74745
  }
74307
74746
  function listPendingScheduleEntries(agent, opts = {}) {
74308
74747
  const dir = pendingDir(agent, opts);
74309
- if (!existsSync66(dir))
74748
+ if (!existsSync68(dir))
74310
74749
  return [];
74311
74750
  const out = [];
74312
- for (const name of readdirSync25(dir).sort()) {
74751
+ for (const name of readdirSync26(dir).sort()) {
74313
74752
  if (!name.endsWith(".meta.json"))
74314
74753
  continue;
74315
74754
  const stageId = name.slice(0, -".meta.json".length);
74316
- const metaPath = join59(dir, name);
74317
- const yamlPath = join59(dir, `${stageId}.yaml`);
74318
- if (!existsSync66(yamlPath))
74755
+ const metaPath = join60(dir, name);
74756
+ const yamlPath = join60(dir, `${stageId}.yaml`);
74757
+ if (!existsSync68(yamlPath))
74319
74758
  continue;
74320
74759
  try {
74321
74760
  const meta = JSON.parse(readFileSync57(metaPath, "utf-8"));
@@ -74333,8 +74772,8 @@ function commitPendingScheduleEntry(opts) {
74333
74772
  return { committed: false, reason: "not_found" };
74334
74773
  const slug = match.meta.entry.name ?? match.stageId;
74335
74774
  const paths = overlayPathsFor(opts.agent, { root: opts.root });
74336
- const finalPath = join59(paths.scheduleDir, `${slug}.yaml`);
74337
- if (existsSync66(finalPath)) {
74775
+ const finalPath = join60(paths.scheduleDir, `${slug}.yaml`);
74776
+ if (existsSync68(finalPath)) {
74338
74777
  return { committed: false, reason: "slug_collision" };
74339
74778
  }
74340
74779
  renameSync14(match.yamlPath, finalPath);
@@ -74356,7 +74795,7 @@ function denyPendingScheduleEntry(opts) {
74356
74795
  }
74357
74796
 
74358
74797
  // src/cli/agent-config-write.ts
74359
- import { existsSync as existsSync67, readFileSync as readFileSync58 } from "node:fs";
74798
+ import { existsSync as existsSync69, readFileSync as readFileSync58 } from "node:fs";
74360
74799
  var MAX_ENTRIES_PER_AGENT = 20;
74361
74800
  function checkOperatorContext(verb, env2 = process.env) {
74362
74801
  if (env2.SWITCHROOM_OPERATOR === "1")
@@ -74590,7 +75029,7 @@ function scheduleRemove(opts) {
74590
75029
  }
74591
75030
  let priorContent = null;
74592
75031
  try {
74593
- if (existsSync67(match.path))
75032
+ if (existsSync69(match.path))
74594
75033
  priorContent = readFileSync58(match.path, "utf-8");
74595
75034
  } catch {}
74596
75035
  deleteOverlayEntry(agent, match.slug, { root: opts.root });
@@ -74783,10 +75222,10 @@ function registerAgentConfigWriteCommands(program3) {
74783
75222
 
74784
75223
  // src/cli/agent-config-skill-write.ts
74785
75224
  var import_yaml15 = __toESM(require_dist(), 1);
74786
- import { existsSync as existsSync68 } from "node:fs";
75225
+ import { existsSync as existsSync70 } from "node:fs";
74787
75226
  init_reconcile_default_skills();
74788
75227
  var import_yaml16 = __toESM(require_dist(), 1);
74789
- import { join as join60 } from "node:path";
75228
+ import { join as join61 } from "node:path";
74790
75229
  var MAX_SKILLS_PER_AGENT = 20;
74791
75230
  var V1_ALLOWED_SOURCE_PREFIX = "bundled:";
74792
75231
  function exitCodeFor2(code) {
@@ -74861,8 +75300,8 @@ function skillInstall(opts) {
74861
75300
  return err("E_SKILL_QUOTA_EXCEEDED", `agent ${agent} already has ${used} overlay-installed skills (cap ${MAX_SKILLS_PER_AGENT})`);
74862
75301
  }
74863
75302
  const poolDir = opts.bundledSkillsPoolDir ?? getBundledSkillsPoolDir();
74864
- const skillPath = join60(poolDir, skillName);
74865
- if (!existsSync68(skillPath)) {
75303
+ const skillPath = join61(poolDir, skillName);
75304
+ if (!existsSync70(skillPath)) {
74866
75305
  return err("E_SKILL_NOT_FOUND", `bundled skill not found at ${skillPath}. The operator needs to ` + `place the skill at this path before the agent can opt in.`);
74867
75306
  }
74868
75307
  const yamlText = import_yaml15.stringify({ skills: [skillName] });
@@ -75046,27 +75485,27 @@ init_source();
75046
75485
  init_helpers();
75047
75486
  init_loader();
75048
75487
  import {
75049
- existsSync as existsSync70,
75050
- readdirSync as readdirSync26,
75488
+ existsSync as existsSync72,
75489
+ readdirSync as readdirSync27,
75051
75490
  readFileSync as readFileSync60,
75052
75491
  renameSync as renameSync15,
75053
- statSync as statSync25,
75492
+ statSync as statSync27,
75054
75493
  unlinkSync as unlinkSync15
75055
75494
  } from "node:fs";
75056
75495
  import { createHash as createHash12 } from "node:crypto";
75057
- import { join as join61 } from "node:path";
75496
+ import { join as join62 } from "node:path";
75058
75497
  function planCronUnitRenames(agentsDir, agents) {
75059
75498
  const plans = [];
75060
75499
  for (const [agentName, agentConfig] of Object.entries(agents)) {
75061
75500
  const schedule = agentConfig.schedule ?? [];
75062
75501
  if (schedule.length === 0)
75063
75502
  continue;
75064
- const telegramDir = join61(agentsDir, agentName, "telegram");
75065
- if (!existsSync70(telegramDir))
75503
+ const telegramDir = join62(agentsDir, agentName, "telegram");
75504
+ if (!existsSync72(telegramDir))
75066
75505
  continue;
75067
75506
  let entries;
75068
75507
  try {
75069
- entries = readdirSync26(telegramDir);
75508
+ entries = readdirSync27(telegramDir);
75070
75509
  } catch {
75071
75510
  continue;
75072
75511
  }
@@ -75083,8 +75522,8 @@ function planCronUnitRenames(agentsDir, agents) {
75083
75522
  continue;
75084
75523
  plans.push({
75085
75524
  agent: agentName,
75086
- from: join61(telegramDir, file),
75087
- to: join61(telegramDir, canonical),
75525
+ from: join62(telegramDir, file),
75526
+ to: join62(telegramDir, canonical),
75088
75527
  scheduleIdx: idx,
75089
75528
  entry
75090
75529
  });
@@ -75096,7 +75535,7 @@ function sha256File2(path8) {
75096
75535
  return createHash12("sha256").update(readFileSync60(path8)).digest("hex");
75097
75536
  }
75098
75537
  function renamePair(from, to, opts = {}) {
75099
- if (existsSync70(to)) {
75538
+ if (existsSync72(to)) {
75100
75539
  let identical = false;
75101
75540
  try {
75102
75541
  identical = sha256File2(from) === sha256File2(to);
@@ -75189,7 +75628,7 @@ function registerMigrateCommand(program3) {
75189
75628
  const fromSidecar = p.from.replace(/\.sh$/, ".source");
75190
75629
  const toSidecar = p.to.replace(/\.sh$/, ".source");
75191
75630
  let sidecarStatus = null;
75192
- if (existsSync70(fromSidecar) && statSync25(fromSidecar).isFile()) {
75631
+ if (existsSync72(fromSidecar) && statSync27(fromSidecar).isFile()) {
75193
75632
  sidecarStatus = renamePair(fromSidecar, toSidecar);
75194
75633
  }
75195
75634
  switch (status.kind) {
@@ -75222,10 +75661,10 @@ function registerMigrateCommand(program3) {
75222
75661
  init_source();
75223
75662
  init_helpers();
75224
75663
  init_audit_reader();
75225
- import { existsSync as existsSync71, mkdirSync as mkdirSync39, readdirSync as readdirSync27, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync26, copyFileSync as copyFileSync12 } from "node:fs";
75664
+ import { existsSync as existsSync73, mkdirSync as mkdirSync39, readdirSync as readdirSync28, readFileSync as readFileSync61, writeFileSync as writeFileSync34, statSync as statSync28, copyFileSync as copyFileSync12 } from "node:fs";
75226
75665
  import { homedir as homedir32 } from "node:os";
75227
- import { join as join62 } from "node:path";
75228
- import { spawnSync as spawnSync10 } from "node:child_process";
75666
+ import { join as join63 } from "node:path";
75667
+ import { spawnSync as spawnSync11 } from "node:child_process";
75229
75668
  var DEFAULT_IMAGE_TAG = "latest";
75230
75669
  var HOSTD_COMPOSE_PROJECT = "switchroom-hostd";
75231
75670
  function renderHostdComposeFile(opts) {
@@ -75308,14 +75747,14 @@ networks:
75308
75747
  `;
75309
75748
  }
75310
75749
  function hostdDir() {
75311
- return join62(homedir32(), ".switchroom", "hostd");
75750
+ return join63(homedir32(), ".switchroom", "hostd");
75312
75751
  }
75313
75752
  function hostdComposePath() {
75314
- return join62(hostdDir(), "docker-compose.yml");
75753
+ return join63(hostdDir(), "docker-compose.yml");
75315
75754
  }
75316
75755
  function backupExistingCompose() {
75317
75756
  const p = hostdComposePath();
75318
- if (!existsSync71(p))
75757
+ if (!existsSync73(p))
75319
75758
  return null;
75320
75759
  const ts = new Date().toISOString().replace(/[:.]/g, "-");
75321
75760
  const bak = `${p}.bak-${ts}`;
@@ -75323,7 +75762,7 @@ function backupExistingCompose() {
75323
75762
  return bak;
75324
75763
  }
75325
75764
  function runDocker(args) {
75326
- const r = spawnSync10("docker", args, { encoding: "utf8" });
75765
+ const r = spawnSync11("docker", args, { encoding: "utf8" });
75327
75766
  return {
75328
75767
  ok: r.status === 0,
75329
75768
  stdout: r.stdout ?? "",
@@ -75391,7 +75830,7 @@ function doStatus() {
75391
75830
  const composeYml = hostdComposePath();
75392
75831
  console.log(source_default.bold("switchroom-hostd"));
75393
75832
  console.log("");
75394
- if (!existsSync71(composeYml)) {
75833
+ if (!existsSync73(composeYml)) {
75395
75834
  console.log(source_default.yellow(" compose: not installed"));
75396
75835
  console.log(source_default.dim(" run `switchroom hostd install` to set up."));
75397
75836
  return;
@@ -75412,15 +75851,15 @@ function doStatus() {
75412
75851
  } else {
75413
75852
  console.log(source_default.green(` container: ${ps.stdout.trim()}`));
75414
75853
  }
75415
- if (existsSync71(dir)) {
75854
+ if (existsSync73(dir)) {
75416
75855
  const entries = [];
75417
75856
  try {
75418
- for (const name of readdirSync27(dir)) {
75857
+ for (const name of readdirSync28(dir)) {
75419
75858
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
75420
75859
  continue;
75421
- const sockPath = join62(dir, name, "sock");
75422
- if (existsSync71(sockPath)) {
75423
- const st = statSync26(sockPath);
75860
+ const sockPath = join63(dir, name, "sock");
75861
+ if (existsSync73(sockPath)) {
75862
+ const st = statSync28(sockPath);
75424
75863
  if ((st.mode & 61440) === 49152) {
75425
75864
  entries.push(`${name} \u2192 ${sockPath}`);
75426
75865
  }
@@ -75438,7 +75877,7 @@ function doStatus() {
75438
75877
  }
75439
75878
  function doUninstall() {
75440
75879
  const composeYml = hostdComposePath();
75441
- if (!existsSync71(composeYml)) {
75880
+ if (!existsSync73(composeYml)) {
75442
75881
  console.log(source_default.yellow(" No hostd install detected (no compose file at this path)."));
75443
75882
  return;
75444
75883
  }
@@ -75462,7 +75901,7 @@ function registerHostdCommand(program3) {
75462
75901
  hostd.command("uninstall").description("Stop the hostd container. Leaves the compose file in place for re-install.").action(() => doUninstall());
75463
75902
  hostd.command("audit").description("Tail and filter the hostd audit log (privileged-verb call history)").option("--tail <n>", "Number of matching entries to show (default: 50)", "50").option("--agent <name>", "Filter to a specific caller agent").option("--op <verb>", "Filter to a specific hostd verb (e.g. update_apply, agent_restart)").option("--error", "Show only failed (error/denied) entries").option("--verbose", "Show the captured stderr / error tail under each failed row").option("--path <file>", "Override audit log path (for debugging)").action((opts) => {
75464
75903
  const logPath = opts.path ?? defaultAuditLogPath2();
75465
- if (!existsSync71(logPath)) {
75904
+ if (!existsSync73(logPath)) {
75466
75905
  console.error(source_default.yellow(`Audit log not found at ${logPath}.`) + source_default.gray(`
75467
75906
  The log is created when hostd handles its first privileged-verb request.`));
75468
75907
  return;