switchroom 0.21.7 → 0.21.9

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 (59) hide show
  1. package/bin/tmp-reaper.sh +234 -0
  2. package/dist/agent-scheduler/index.js +1 -1
  3. package/dist/auth-broker/index.js +2 -2
  4. package/dist/cli/notion-write-pretool.mjs +1 -1
  5. package/dist/cli/switchroom.js +3520 -2782
  6. package/dist/host-control/main.js +177 -13
  7. package/dist/vault/approvals/kernel-server.js +2 -2
  8. package/dist/vault/broker/server.js +2 -2
  9. package/package.json +6 -5
  10. package/profiles/_base/start.sh.hbs +115 -0
  11. package/profiles/_shared/local-time.md.hbs +6 -0
  12. package/profiles/default/CLAUDE.md.hbs +0 -12
  13. package/skills/switchroom-architecture/telegram.md +12 -10
  14. package/skills/switchroom-cli/SKILL.md +1 -1
  15. package/telegram-plugin/README.md +3 -1
  16. package/telegram-plugin/dist/gateway/gateway.js +1164 -547
  17. package/telegram-plugin/format.ts +12 -4
  18. package/telegram-plugin/gateway/agent-process-liveness.ts +558 -0
  19. package/telegram-plugin/gateway/approval-hold.ts +32 -1
  20. package/telegram-plugin/gateway/approval-outcome-sources.ts +274 -0
  21. package/telegram-plugin/gateway/bridge-dead-watchdog.ts +21 -9
  22. package/telegram-plugin/gateway/callback-query-handlers.ts +87 -15
  23. package/telegram-plugin/gateway/eval-case-proposal-inbound-builders.ts +197 -0
  24. package/telegram-plugin/gateway/gateway.ts +12 -10
  25. package/telegram-plugin/gateway/pending-inbound-buffer.ts +167 -11
  26. package/telegram-plugin/gateway/self-improve-proposal-wiring.test.ts +333 -0
  27. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +152 -3
  28. package/telegram-plugin/gateway/subagent-handback-marker.ts +19 -0
  29. package/telegram-plugin/package.json +1 -1
  30. package/telegram-plugin/render/code-segments.ts +38 -4
  31. package/telegram-plugin/render/dollar-math-guard.ts +16 -1
  32. package/telegram-plugin/render/ir.ts +53 -3
  33. package/telegram-plugin/render/parse.ts +73 -14
  34. package/telegram-plugin/render/render.ts +53 -15
  35. package/telegram-plugin/render/unsupported-token-guard.ts +45 -80
  36. package/telegram-plugin/rich-send.ts +22 -7
  37. package/telegram-plugin/shared/bot-runtime.ts +3 -2
  38. package/telegram-plugin/telegraph.ts +6 -4
  39. package/telegram-plugin/tests/agent-process-liveness.test.ts +406 -0
  40. package/telegram-plugin/tests/approval-hold-record.test.ts +21 -8
  41. package/telegram-plugin/tests/boot-resume-gateway-only-respawn.test.ts +752 -0
  42. package/telegram-plugin/tests/boot-resume-guard-wiring.test.ts +203 -0
  43. package/telegram-plugin/tests/callback-query-handlers.test.ts +143 -1
  44. package/telegram-plugin/tests/eval-case-proposal-inbound-builders.test.ts +144 -0
  45. package/telegram-plugin/tests/grammy-rich-message-types.test.ts +199 -0
  46. package/telegram-plugin/tests/hermes-messages-paging.test.ts +149 -0
  47. package/telegram-plugin/tests/hermes-session-search.test.ts +146 -0
  48. package/telegram-plugin/tests/pending-inbound-buffer.test.ts +443 -2
  49. package/telegram-plugin/tests/render/dollar-math-guard.test.ts +43 -0
  50. package/telegram-plugin/tests/render/guard-composition.test.ts +102 -0
  51. package/telegram-plugin/tests/render/parse.test.ts +30 -5
  52. package/telegram-plugin/tests/render/render.test.ts +9 -4
  53. package/telegram-plugin/tests/render/rich-render.test.ts +46 -5
  54. package/telegram-plugin/tests/render/tg-entity.test.ts +242 -0
  55. package/telegram-plugin/tests/render/unsupported-token-guard.test.ts +66 -66
  56. package/telegram-plugin/tests/sent-text-capture.test.ts +3 -3
  57. package/telegram-plugin/tests/subagent-handback-marker.test.ts +14 -0
  58. package/telegram-plugin/tests/telegraph.test.ts +1 -1
  59. package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +17 -8
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env bash
2
+ # switchroom-tmp-reaper — bounded janitor for the agent container's /tmp tmpfs.
3
+ #
4
+ # WHY THIS EXISTS
5
+ # ---------------
6
+ # An agent container mounts /tmp as a RAM-backed tmpfs, sized by
7
+ # `resources.tmp_size` (src/agents/compose.ts, DEFAULT_TMP_SIZE = 2g), with
8
+ # Docker's default tmpfs flags — `rw,nosuid,nodev,noexec`. Two consequences:
9
+ #
10
+ # 1. It is a hard ceiling. Nothing ages out of a tmpfs; a long-running
11
+ # container accumulates every scratch dir any tool ever forgot to
12
+ # remove until writes start failing ENOSPC. Observed on a live agent:
13
+ # 4,462 top-level entries, 490 MiB used, of which 204 MiB was two
14
+ # orphaned `bun build --compile` artifacts from a single test run
15
+ # (fixed at source in tests/install/static-binary.test.ts, but that
16
+ # only fixes the leak we already found).
17
+ # 2. The failure is NOT localised. Once /tmp is full, everything that
18
+ # stages through it fails — `npm ci`, git, the claude CLI's own
19
+ # scratch — with errors that name the victim, never the cause.
20
+ #
21
+ # The size is deliberately NOT the lever. Raising the tmpfs converts a fast,
22
+ # loud failure into a slow leak that eats host RAM instead. This reaper
23
+ # bounds the accumulation itself.
24
+ #
25
+ # SAFETY CONTRACT (each clause is pinned by tests/tmp-reaper.test.ts)
26
+ # -------------------------------------------------------------------
27
+ # * Scope. Only direct children of the root are considered, the root
28
+ # itself is never removed, and every traversal is `-xdev` with no
29
+ # symlink dereference — so a symlink under /tmp pointing at, say,
30
+ # /state/agent can have the LINK reaped but never its target.
31
+ # * Age. An entry is a candidate only when NOTHING anywhere in its
32
+ # subtree has been modified or accessed within the age threshold
33
+ # (default 24h). A directory's own mtime does not change when a
34
+ # grandchild is written, so checking the entry alone would reap a
35
+ # live tree — the whole subtree is checked.
36
+ # * Open files. An entry is skipped when it, or anything beneath it, is
37
+ # open by any process in the container: /proc/*/fd, /proc/*/cwd,
38
+ # /proc/*/exe and mmapped regions from /proc/*/maps are all consulted.
39
+ # * Logging. Every reaped entry is logged with its size and age, and every
40
+ # pass emits a one-line summary — including passes that reap nothing.
41
+ #
42
+ # TUNING (all validated; garbage falls back to the default)
43
+ # SWITCHROOM_TMP_REAPER=0 disable entirely
44
+ # SWITCHROOM_TMP_REAPER_MIN_AGE_SEC=86400 age threshold (min 3600)
45
+ # SWITCHROOM_TMP_REAPER_INTERVAL_SEC=3600 seconds between passes
46
+ # SWITCHROOM_TMP_REAPER_ROOT=/tmp root (test harness only)
47
+ #
48
+ # Run standalone for a one-shot pass: tmp-reaper.sh --once
49
+ set -uo pipefail
50
+
51
+ REAPER_NAME="switchroom-tmp-reaper"
52
+
53
+ TMP_REAPER_ROOT="${SWITCHROOM_TMP_REAPER_ROOT:-/tmp}"
54
+
55
+ # Age below which an entry is NEVER touched. One hour is the hard floor even
56
+ # if an operator sets something smaller: this reaper cannot distinguish "a
57
+ # build wrote this five minutes ago and will read it back in ten" from
58
+ # garbage, and the age bound is the only thing standing between it and a
59
+ # live workload.
60
+ MIN_AGE_SEC="${SWITCHROOM_TMP_REAPER_MIN_AGE_SEC:-86400}"
61
+ case "$MIN_AGE_SEC" in ''|*[!0-9]*) MIN_AGE_SEC=86400;; esac
62
+ [ "$MIN_AGE_SEC" -lt 3600 ] && MIN_AGE_SEC=3600
63
+
64
+ INTERVAL_SEC="${SWITCHROOM_TMP_REAPER_INTERVAL_SEC:-3600}"
65
+ case "$INTERVAL_SEC" in ''|0|*[!0-9]*) INTERVAL_SEC=3600;; esac
66
+ [ "$INTERVAL_SEC" -lt 60 ] && INTERVAL_SEC=60
67
+
68
+ # Never-reap names. These are conventional tmpfs fixtures whose mtime can sit
69
+ # still for weeks while they are very much in use, and whose removal breaks
70
+ # things in ways that are hard to trace back here.
71
+ TMP_REAPER_KEEP_DEFAULT=".X11-unix .ICE-unix .font-unix .Test-unix .XIM-unix"
72
+ TMP_REAPER_KEEP="${SWITCHROOM_TMP_REAPER_KEEP:-$TMP_REAPER_KEEP_DEFAULT}"
73
+
74
+ _log() { printf '[%s] %s %s\n' "$REAPER_NAME" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"; }
75
+
76
+ # Refuse to operate on a root that is not a plain, existing directory, or that
77
+ # is the filesystem root. The default is /tmp and the override exists for the
78
+ # test harness; this guard is what keeps a mis-set override from being
79
+ # catastrophic rather than merely wrong.
80
+ _root_ok() {
81
+ local r="$1"
82
+ [ -n "$r" ] || { _log "refusing: empty root"; return 1; }
83
+ [ "$r" != "/" ] || { _log "refusing: root is /"; return 1; }
84
+ case "$r" in
85
+ /*) ;;
86
+ *) _log "refusing: root '$r' is not absolute"; return 1;;
87
+ esac
88
+ case "$r" in
89
+ *..*) _log "refusing: root '$r' contains '..'"; return 1;;
90
+ esac
91
+ [ -d "$r" ] || { _log "refusing: root '$r' is not a directory"; return 1; }
92
+ [ ! -L "$r" ] || { _log "refusing: root '$r' is a symlink"; return 1; }
93
+ return 0
94
+ }
95
+
96
+ # Emit every path under $1 that some process in this container currently has
97
+ # open — as an fd, as its cwd, as its executable, or as an mmapped region.
98
+ # Unreadable /proc entries (a process owned by another uid, or one that exited
99
+ # mid-scan) are skipped: they can only cause us to reap LESS, never more.
100
+ #
101
+ # Cost matters here: a busy container has hundreds of processes and thousands
102
+ # of fds, so this uses ONE `find` (with -printf '%l' to read the link targets
103
+ # in-process) and ONE `grep`+`awk` pipeline over the maps files, rather than a
104
+ # readlink fork per fd. A per-fd fork loop measured multiple seconds per pass.
105
+ #
106
+ # CRITICAL: neither traversal may ABORT on an unreadable input. `find` reports
107
+ # a vanished/denied start point and keeps going, which is what we need. `awk`
108
+ # does NOT: the image ships mawk 1.3.4, which stops at the FIRST file it cannot
109
+ # open and never reads the rest of its argument list. Handing mawk the raw
110
+ # /proc/[0-9]*/maps glob therefore meant that one PID exiting between bash's
111
+ # glob expansion and mawk's serial open (ESRCH), or one EPERM, silently
112
+ # discarded every LATER process's mappings — a fail-OPEN in a safety guard,
113
+ # after which the reaper would delete a tree a live process still had mapped.
114
+ # So `grep` (GNU grep continues past unreadable files, exit 2) does the
115
+ # multi-file read and `awk` only ever sees a single stdin stream.
116
+ #
117
+ # `awk substr` rather than `grep -o`: a maps line is
118
+ # "addr perms offset dev inode <pathname>", so the path runs to end-of-line and
119
+ # MAY CONTAIN SPACES. `grep -oE "$root/[^ ]*"` would truncate
120
+ # "/tmp/my dir/lib.so" to "/tmp/my", which no longer prefix-matches the entry
121
+ # in the caller below — fail-open again, for any /tmp entry with a space.
122
+ _open_paths() {
123
+ local root="$1"
124
+ find /proc/[0-9]*/fd /proc/[0-9]*/cwd /proc/[0-9]*/exe \
125
+ -maxdepth 1 -type l -printf '%l\n' 2>/dev/null \
126
+ | grep -F -- "$root/" 2>/dev/null
127
+ # mmapped files (a running binary, a memory-mapped DB) do not appear as an
128
+ # fd once mapped and the fd is closed, but are absolutely still in use.
129
+ grep -haF -- "$root/" /proc/[0-9]*/maps 2>/dev/null \
130
+ | awk -v r="$root/" '{ p=index($0, r); if (p) print substr($0, p) }'
131
+ }
132
+
133
+ _human() {
134
+ local b="${1:-0}"
135
+ if [ "$b" -ge 1073741824 ] 2>/dev/null; then echo "$((b / 1073741824))G"
136
+ elif [ "$b" -ge 1048576 ] 2>/dev/null; then echo "$((b / 1048576))M"
137
+ elif [ "$b" -ge 1024 ] 2>/dev/null; then echo "$((b / 1024))K"
138
+ else echo "${b}B"; fi
139
+ }
140
+
141
+ # One pass. Returns 0 always; a failure to remove one entry must not abort
142
+ # the pass (the next entry may be the one actually holding the space).
143
+ tmp_reaper_pass() {
144
+ local root="$TMP_REAPER_ROOT"
145
+ _root_ok "$root" || return 1
146
+ # Canonicalise. /proc/*/fd link targets are already fully resolved, so an
147
+ # un-canonicalised root with a symlinked ancestor would never prefix-match
148
+ # them and the open-file check would silently pass everything through —
149
+ # the guard would look present and do nothing.
150
+ root=$(readlink -f -- "$root" 2>/dev/null) || root="$TMP_REAPER_ROOT"
151
+ [ -n "$root" ] || root="$TMP_REAPER_ROOT"
152
+ _root_ok "$root" || return 1
153
+
154
+ local now cutoff
155
+ now=$(date +%s)
156
+ cutoff=$((now - MIN_AGE_SEC))
157
+
158
+ local open_list
159
+ open_list=$(_open_paths "$root")
160
+
161
+ local reaped=0 freed=0 kept_open=0 kept_young=0 kept_pinned=0 failed=0
162
+ local entry base
163
+
164
+ while IFS= read -r -d '' entry; do
165
+ base="${entry##*/}"
166
+
167
+ # 1. Never-reap list.
168
+ local pinned=0 k
169
+ for k in $TMP_REAPER_KEEP; do
170
+ [ "$base" = "$k" ] && { pinned=1; break; }
171
+ done
172
+ if [ "$pinned" -eq 1 ]; then kept_pinned=$((kept_pinned + 1)); continue; fi
173
+
174
+ # 2. Age — nothing ANYWHERE in the subtree may be newer than the cutoff,
175
+ # by mtime or atime. `-print -quit` stops at the first offender.
176
+ local young
177
+ young=$(find "$entry" -xdev \( -newermt "@$cutoff" -o -newerat "@$cutoff" \) \
178
+ -print -quit 2>/dev/null)
179
+ if [ -n "$young" ]; then kept_young=$((kept_young + 1)); continue; fi
180
+
181
+ # 3. Open by a live process (the entry itself, or anything under it).
182
+ local is_open=0 p
183
+ while IFS= read -r p; do
184
+ [ -n "$p" ] || continue
185
+ if [ "$p" = "$entry" ] || [ "${p#"$entry"/}" != "$p" ]; then
186
+ is_open=1; break
187
+ fi
188
+ done <<< "$open_list"
189
+ if [ "$is_open" -eq 1 ]; then kept_open=$((kept_open + 1)); continue; fi
190
+
191
+ # Size and age for the log line, computed BEFORE removal.
192
+ local bytes mtime age_h
193
+ bytes=$(du -sxb -- "$entry" 2>/dev/null | cut -f1)
194
+ case "$bytes" in ''|*[!0-9]*) bytes=0;; esac
195
+ mtime=$(stat -c %Y -- "$entry" 2>/dev/null || echo "$now")
196
+ age_h=$(( (now - mtime) / 3600 ))
197
+
198
+ if rm -rf --one-file-system --preserve-root -- "$entry" 2>/dev/null; then
199
+ reaped=$((reaped + 1))
200
+ freed=$((freed + bytes))
201
+ _log "reaped $entry ($(_human "$bytes"), idle ${age_h}h)"
202
+ else
203
+ failed=$((failed + 1))
204
+ _log "could NOT remove $entry ($(_human "$bytes"), idle ${age_h}h)"
205
+ fi
206
+ done < <(find "$root" -xdev -mindepth 1 -maxdepth 1 -print0 2>/dev/null)
207
+
208
+ _log "pass complete on $root: reaped=$reaped freed=$(_human "$freed")" \
209
+ "kept_young=$kept_young kept_open=$kept_open kept_pinned=$kept_pinned" \
210
+ "failed=$failed min_age=${MIN_AGE_SEC}s"
211
+ return 0
212
+ }
213
+
214
+ tmp_reaper_main() {
215
+ if [ "${SWITCHROOM_TMP_REAPER:-1}" = "0" ]; then
216
+ _log "disabled via SWITCHROOM_TMP_REAPER=0 — not starting"
217
+ return 0
218
+ fi
219
+ if [ "${1:-}" = "--once" ]; then
220
+ tmp_reaper_pass
221
+ return $?
222
+ fi
223
+ _log "starting: root=$TMP_REAPER_ROOT min_age=${MIN_AGE_SEC}s interval=${INTERVAL_SEC}s"
224
+ while true; do
225
+ tmp_reaper_pass
226
+ sleep "$INTERVAL_SEC"
227
+ done
228
+ }
229
+
230
+ # Only auto-run when EXECUTED. Sourcing (the test harness, and any future
231
+ # caller that wants a single pass inline) gets the functions and nothing else.
232
+ if [ "${BASH_SOURCE[0]}" = "$0" ]; then
233
+ tmp_reaper_main "$@"
234
+ fi
@@ -11738,7 +11738,7 @@ var AgentSchema = exports_external.object({
11738
11738
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
11739
11739
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
11740
11740
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
11741
- })).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated worktree for each " + "repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main, backed by a " + "shared bare clone at ~/.switchroom/repos/<slug>.git. The worktree path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
11741
+ })).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated standing tree for " + "each repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main an " + "independent clone (own refs/stash, hardlinked objects) seeded from a shared bare " + "clone at ~/.switchroom/repos/<slug>.git. The tree's path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
11742
11742
  experimental: exports_external.object({
11743
11743
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
11744
11744
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
@@ -11763,7 +11763,7 @@ var init_schema = __esm(() => {
11763
11763
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
11764
11764
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
11765
11765
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
11766
- })).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated worktree for each " + "repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main, backed by a " + "shared bare clone at ~/.switchroom/repos/<slug>.git. The worktree path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
11766
+ })).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated standing tree for " + "each repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main an " + "independent clone (own refs/stash, hardlinked objects) seeded from a shared bare " + "clone at ~/.switchroom/repos/<slug>.git. The tree's path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
11767
11767
  experimental: exports_external.object({
11768
11768
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
11769
11769
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
@@ -19111,7 +19111,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
19111
19111
  return typeof value === "number" && Number.isFinite(value);
19112
19112
  });
19113
19113
  var SHARED_FRAGMENTS_DIR = resolve5(PROFILES_ROOT, "_shared");
19114
- var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
19114
+ var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol", "local-time"];
19115
19115
  for (const name of SHARED_FRAGMENTS) {
19116
19116
  const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
19117
19117
  if (existsSync5(fragPath)) {
@@ -12499,7 +12499,7 @@ var AgentSchema = exports_external.object({
12499
12499
  repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
12500
12500
  url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
12501
12501
  branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
12502
- })).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated worktree for each " + "repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main, backed by a " + "shared bare clone at ~/.switchroom/repos/<slug>.git. The worktree path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
12502
+ })).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated standing tree for " + "each repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main \u2014 an " + "independent clone (own refs/stash, hardlinked objects) seeded from a shared bare " + "clone at ~/.switchroom/repos/<slug>.git. The tree's path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
12503
12503
  experimental: exports_external.object({
12504
12504
  legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
12505
12505
  legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")