switchroom 0.11.1 → 0.12.0
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.
- package/README.md +7 -6
- package/dist/agent-scheduler/index.js +216 -97
- package/dist/auth-broker/index.js +175 -96
- package/dist/cli/drive-write-pretool.mjs +26 -11
- package/dist/cli/switchroom.js +45153 -42663
- package/dist/cli/ui/index.html +1281 -0
- package/dist/host-control/main.js +3628 -309
- package/dist/vault/approvals/kernel-server.js +207 -98
- package/dist/vault/broker/server.js +218 -97
- package/examples/personal-google-workspace-mcp/README.md +8 -3
- package/examples/switchroom.yaml +91 -42
- package/package.json +2 -2
- package/profiles/_base/start.sh.hbs +76 -36
- package/profiles/default/CLAUDE.md.hbs +4 -2
- package/skills/file-bug/SKILL.md +6 -4
- package/skills/switchroom-cli/SKILL.md +20 -4
- package/skills/switchroom-install/SKILL.md +3 -3
- package/telegram-plugin/auth-snapshot-format.ts +4 -4
- package/telegram-plugin/card-format.ts +3 -3
- package/telegram-plugin/dist/bridge/bridge.js +112 -112
- package/telegram-plugin/dist/gateway/gateway.js +795 -410
- package/telegram-plugin/dist/server.js +162 -161
- package/telegram-plugin/format.ts +71 -0
- package/telegram-plugin/gateway/approval-card.test.ts +18 -18
- package/telegram-plugin/gateway/approval-card.ts +1 -1
- package/telegram-plugin/gateway/auth-command.ts +2 -2
- package/telegram-plugin/gateway/boot-card.ts +40 -3
- package/telegram-plugin/gateway/boot-probes.ts +71 -27
- package/telegram-plugin/gateway/diff-preview-card.test.ts +15 -15
- package/telegram-plugin/gateway/diff-preview-card.ts +1 -1
- package/telegram-plugin/gateway/drive-write-approval.test.ts +2 -2
- package/telegram-plugin/gateway/gateway.ts +193 -22
- package/telegram-plugin/gateway/update-announce.ts +167 -0
- package/telegram-plugin/quota-check.ts +0 -195
- package/telegram-plugin/retry-api-call.ts +24 -0
- package/telegram-plugin/server.ts +8 -5
- package/telegram-plugin/tests/auth-add-flow.test.ts +31 -2
- package/telegram-plugin/tests/boot-probes.test.ts +53 -0
- package/telegram-plugin/tests/bot-runtime.test.ts +23 -1
- package/telegram-plugin/tests/quota-check.test.ts +0 -409
- package/telegram-plugin/tests/retry-api-call.test.ts +76 -0
- package/telegram-plugin/tests/telegram-format.test.ts +84 -1
- package/telegram-plugin/tests/update-announce.test.ts +154 -0
- package/telegram-plugin/welcome-text.ts +1 -8
- package/profiles/default/CLAUDE.md +0 -192
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-313.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-313.pyc +0 -0
- package/telegram-plugin/first-paint.ts +0 -225
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/telegram-plugin/server.js +0 -41795
- package/telegram-plugin/tests/html-balanced.ts +0 -63
- package/telegram-plugin/tests/snapshot-serializer.ts +0 -79
- package/telegram-plugin/tool-error-filter.ts +0 -89
|
@@ -6,9 +6,14 @@ tools to **your own Claude Code session on the host** (not to switchroom
|
|
|
6
6
|
agents).
|
|
7
7
|
|
|
8
8
|
It is intentionally **separate from the agent-side feature.** Agents get
|
|
9
|
-
Workspace access via `switchroom auth google connect
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Workspace access via `switchroom auth google connect` →
|
|
10
|
+
`switchroom auth google account add` (RFC G §4.5, shipped) — see
|
|
11
|
+
[`docs/google-workspace.md`](../../docs/google-workspace.md) for the
|
|
12
|
+
fleet setup. **Do not reuse this example's OAuth client for the
|
|
13
|
+
fleet:** different trust posture (approval-kernel-mediated vs.
|
|
14
|
+
single-identity), and switchroom expects its own client. This example
|
|
15
|
+
is only for the operator's pair-design loop with their own host-side
|
|
16
|
+
`claude`.
|
|
12
17
|
|
|
13
18
|
> **Why two paths?** Agents run inside switchroom containers with
|
|
14
19
|
> approval-kernel-mediated tool access; the per-agent OAuth posture is
|
package/examples/switchroom.yaml
CHANGED
|
@@ -5,8 +5,22 @@
|
|
|
5
5
|
# 2. profiles: → named presets agents opt into via `extends:`
|
|
6
6
|
# 3. agents: → per-agent overrides (only express differences)
|
|
7
7
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
8
|
+
# ┌─ ONE TELEGRAM BOT PER AGENT — NON-NEGOTIABLE ───────────────────┐
|
|
9
|
+
# │ Every agent MUST have its own Telegram bot + its own token. │
|
|
10
|
+
# │ Two agents sharing a token both long-poll getUpdates and │
|
|
11
|
+
# │ Telegram 409-Conflicts them in a loop — neither one replies. │
|
|
12
|
+
# │ │
|
|
13
|
+
# │ For each agent: create a separate @BotFather bot (`/newbot`), │
|
|
14
|
+
# │ vault its token under its own key │
|
|
15
|
+
# │ (`switchroom vault set telegram-<agent>-bot-token`), and give │
|
|
16
|
+
# │ the agent its own `bot_token: "vault:telegram-<agent>-bot- │
|
|
17
|
+
# │ token"`. This file ships ONE active agent (`assistant`); every │
|
|
18
|
+
# │ extra agent below is commented out WITH its own bot_token — │
|
|
19
|
+
# │ uncomment one only after you've minted its bot + vaulted its │
|
|
20
|
+
# │ token, then `switchroom apply` (no need to re-run setup). │
|
|
21
|
+
# └─────────────────────────────────────────────────────────────────┘
|
|
22
|
+
#
|
|
23
|
+
# See docs/botfather-walkthrough.md for the ~3-min-per-bot steps.
|
|
10
24
|
|
|
11
25
|
switchroom:
|
|
12
26
|
version: 1
|
|
@@ -14,6 +28,12 @@ switchroom:
|
|
|
14
28
|
skills_dir: ~/.switchroom/skills # shared skill pool (symlinked per agent)
|
|
15
29
|
|
|
16
30
|
telegram:
|
|
31
|
+
# Single-agent fallback ONLY. This is the bot the one shipped agent
|
|
32
|
+
# (`assistant`) uses — `switchroom setup` stores your first
|
|
33
|
+
# BotFather token in the vault under `telegram-bot-token`. The
|
|
34
|
+
# moment you run more than one agent, this global token is NOT
|
|
35
|
+
# enough: give EACH agent its own `bot_token:` (see the agents
|
|
36
|
+
# section). Never let two agents resolve to the same token.
|
|
17
37
|
bot_token: "vault:telegram-bot-token"
|
|
18
38
|
# DM-only sentinel; v0.7+ defaults to per-agent DM-pair topology.
|
|
19
39
|
# Legacy forum-mode installs keep a real chat id here.
|
|
@@ -155,52 +175,80 @@ profiles:
|
|
|
155
175
|
|
|
156
176
|
# --- Agents ---
|
|
157
177
|
# Minimal per-agent declarations. Everything else inherited.
|
|
178
|
+
#
|
|
179
|
+
# This file ships exactly ONE active agent so a fresh `switchroom
|
|
180
|
+
# setup` → `apply` → `up` brings up a single, working bot (the
|
|
181
|
+
# `assistant` below uses the single global telegram.bot_token that
|
|
182
|
+
# setup vaulted). Every other agent is a commented-out TEMPLATE.
|
|
183
|
+
#
|
|
184
|
+
# To add any agent: (1) @BotFather `/newbot` → a NEW bot just for it;
|
|
185
|
+
# (2) `switchroom vault set telegram-<agent>-bot-token` (paste that
|
|
186
|
+
# bot's token); (3) uncomment its block below — note each already
|
|
187
|
+
# carries its own `bot_token: "vault:telegram-<agent>-bot-token"`;
|
|
188
|
+
# (4) `switchroom apply`. Do NOT re-run `switchroom setup` for this —
|
|
189
|
+
# `apply` reads the vaulted per-agent token directly.
|
|
190
|
+
#
|
|
191
|
+
# Sharing one token across agents is the single most common
|
|
192
|
+
# multi-agent install failure: both bots long-poll getUpdates and
|
|
193
|
+
# Telegram 409-Conflicts them forever. One bot per agent, always.
|
|
158
194
|
agents:
|
|
159
|
-
coach:
|
|
160
|
-
topic_name: "Fitness"
|
|
161
|
-
topic_emoji: "🏋️"
|
|
162
|
-
extends: advisor # inherits from inline profile above
|
|
163
|
-
soul:
|
|
164
|
-
name: Coach
|
|
165
|
-
style: motivational, direct # overrides advisor.soul.style
|
|
166
|
-
memory:
|
|
167
|
-
collection: fitness
|
|
168
|
-
schedule:
|
|
169
|
-
- cron: "0 8 * * *"
|
|
170
|
-
prompt: "Good morning check-in: ask about sleep, energy, and plans for today"
|
|
171
|
-
- cron: "0 20 * * 0"
|
|
172
|
-
prompt: "Weekly review: summarize this week's activity and progress"
|
|
173
|
-
|
|
174
|
-
dev:
|
|
175
|
-
topic_name: "Code"
|
|
176
|
-
topic_emoji: "💻"
|
|
177
|
-
extends: coder # inherits from inline profile above
|
|
178
|
-
model: claude-opus-4-7 # override defaults.model for this agent
|
|
179
|
-
memory:
|
|
180
|
-
collection: coding
|
|
181
|
-
cli_args: ["--effort", "high"] # escape hatch: extra exec claude flags
|
|
182
|
-
|
|
183
195
|
assistant:
|
|
184
196
|
topic_name: "General"
|
|
185
197
|
topic_emoji: "💬"
|
|
186
198
|
memory:
|
|
187
199
|
collection: general
|
|
200
|
+
# Uses the global telegram.bot_token above (the one `switchroom
|
|
201
|
+
# setup` vaulted as `telegram-bot-token`). This is the ONLY agent
|
|
202
|
+
# allowed to rely on the global token — because it's the only one
|
|
203
|
+
# shipped active. Give every agent you add its own bot_token.
|
|
188
204
|
# No `extends:` → uses the "default" filesystem profile (profiles/default/)
|
|
189
205
|
# No tool/model overrides → inherits everything from defaults:
|
|
190
206
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
207
|
+
# ── Additional agents — each needs its OWN BotFather bot ──────────
|
|
208
|
+
# Before uncommenting any block: create its bot, then
|
|
209
|
+
# switchroom vault set telegram-coach-bot-token # (etc.)
|
|
210
|
+
# The `bot_token:` line in each block points at that per-agent key.
|
|
211
|
+
|
|
212
|
+
# coach:
|
|
213
|
+
# topic_name: "Fitness"
|
|
214
|
+
# topic_emoji: "🏋️"
|
|
215
|
+
# bot_token: "vault:telegram-coach-bot-token" # its own bot
|
|
216
|
+
# extends: advisor # inherits from inline profile above
|
|
217
|
+
# soul:
|
|
218
|
+
# name: Coach
|
|
219
|
+
# style: motivational, direct # overrides advisor.soul.style
|
|
220
|
+
# memory:
|
|
221
|
+
# collection: fitness
|
|
222
|
+
# schedule:
|
|
223
|
+
# - cron: "0 8 * * *"
|
|
224
|
+
# prompt: "Good morning check-in: ask about sleep, energy, and plans for today"
|
|
225
|
+
# - cron: "0 20 * * 0"
|
|
226
|
+
# prompt: "Weekly review: summarize this week's activity and progress"
|
|
227
|
+
|
|
228
|
+
# dev:
|
|
229
|
+
# topic_name: "Code"
|
|
230
|
+
# topic_emoji: "💻"
|
|
231
|
+
# bot_token: "vault:telegram-dev-bot-token" # its own bot
|
|
232
|
+
# extends: coder # inherits from inline profile above
|
|
233
|
+
# model: claude-opus-4-7 # override defaults.model for this agent
|
|
234
|
+
# memory:
|
|
235
|
+
# collection: coding
|
|
236
|
+
# cli_args: ["--effort", "high"] # escape hatch: extra exec claude flags
|
|
237
|
+
|
|
238
|
+
# exec:
|
|
239
|
+
# topic_name: "Executive"
|
|
240
|
+
# topic_emoji: "📋"
|
|
241
|
+
# bot_token: "vault:telegram-exec-bot-token" # its own bot
|
|
242
|
+
# extends: advisor
|
|
243
|
+
# soul:
|
|
244
|
+
# name: Friday
|
|
245
|
+
# style: efficient, proactive, anticipates needs
|
|
246
|
+
# skills: [daily-briefing, meeting-prep]
|
|
247
|
+
# memory:
|
|
248
|
+
# collection: executive
|
|
249
|
+
# schedule:
|
|
250
|
+
# - cron: "0 7 * * 1-5"
|
|
251
|
+
# prompt: "Daily briefing: summarize today's calendar, pending tasks, and priorities"
|
|
204
252
|
|
|
205
253
|
# Example admin agent — its gateway intercepts fleet-management slash
|
|
206
254
|
# commands (/agents, /restart, /update, /logs, etc.) and runs them
|
|
@@ -209,12 +257,13 @@ agents:
|
|
|
209
257
|
# on every agent regardless of admin status. See the three-tier
|
|
210
258
|
# command model in docs/architecture.md.
|
|
211
259
|
#
|
|
212
|
-
#
|
|
213
|
-
#
|
|
260
|
+
# Like every agent it needs its OWN bot — create a separate BotFather
|
|
261
|
+
# bot and `switchroom vault set telegram-admin-bot-token` before
|
|
262
|
+
# uncommenting.
|
|
214
263
|
# admin:
|
|
215
264
|
# topic_name: "Admin"
|
|
216
265
|
# topic_emoji: "🛠️"
|
|
217
|
-
# bot_token: "vault:telegram-admin-bot-token"
|
|
266
|
+
# bot_token: "vault:telegram-admin-bot-token" # its own bot
|
|
218
267
|
# admin: true
|
|
219
268
|
# system_prompt_append: |
|
|
220
269
|
# You are the fleet admin agent. Always respond concisely.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"dev": "bun bin/switchroom.ts",
|
|
22
22
|
"build": "node scripts/build.mjs",
|
|
23
23
|
"build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
|
|
24
|
+
"pretest": "npm run build",
|
|
24
25
|
"test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts",
|
|
25
26
|
"test:vitest": "vitest run",
|
|
26
27
|
"test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts",
|
|
@@ -53,7 +54,6 @@
|
|
|
53
54
|
"@types/bun": "^1.3.11",
|
|
54
55
|
"@types/node": "^22.0.0",
|
|
55
56
|
"@vitest/coverage-v8": "3.2.4",
|
|
56
|
-
"buildkite-test-collector": "^1.9.5",
|
|
57
57
|
"typescript": "^5.7.0",
|
|
58
58
|
"vitest": "^3.2.4"
|
|
59
59
|
},
|
|
@@ -32,66 +32,88 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
32
32
|
# same path the rest of start.sh + the MCP sidecar expects.
|
|
33
33
|
export TELEGRAM_STATE_DIR="{{agentDir}}/telegram"
|
|
34
34
|
|
|
35
|
-
# Tiny in-process supervisor: runs cmd in a respawn loop
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
35
|
+
# Tiny in-process supervisor: runs cmd in a respawn loop with
|
|
36
|
+
# exponential backoff (1→2→4…→60s cap) and NEVER permanently gives
|
|
37
|
+
# up. Rationale (RFC J / install-validation 2026-05-17): the
|
|
38
|
+
# gateway's hardest dependency is the vault-broker, which gets
|
|
39
|
+
# recreated+relocked by a routine `switchroom apply`. The old
|
|
40
|
+
# "10 restarts in 60s → give up forever" turned that transient
|
|
41
|
+
# outage into a dead agent until a human recreated the container —
|
|
42
|
+
# a direct violation of the always-on outcome. Backoff bounds CPU
|
|
43
|
+
# during an outage; indefinite retry means the agent self-heals
|
|
44
|
+
# within one backoff cycle the moment the broker is back. The ONLY
|
|
45
|
+
# non-retry path is EX_CONFIG=78 (genuine permanent misconfig).
|
|
46
|
+
# The sidecar's own structured logging is written
|
|
39
47
|
# directly to its log file; this wrapper only handles process
|
|
40
48
|
# lifecycle. Ampersand-backgrounded by callers below.
|
|
41
49
|
_switchroom_supervise() {
|
|
42
50
|
local _name="$1"; local _logfile="$2"; shift 2
|
|
43
|
-
local
|
|
44
|
-
local
|
|
51
|
+
local _cap=60
|
|
52
|
+
local _delay=1
|
|
53
|
+
local _attempt=0
|
|
45
54
|
while true; do
|
|
55
|
+
local _start=$SECONDS
|
|
46
56
|
"$@" >> "$_logfile" 2>&1
|
|
47
57
|
local _exit=$?
|
|
48
|
-
local
|
|
58
|
+
local _ran=$((SECONDS - _start))
|
|
49
59
|
# Exit 78 = sysexits EX_CONFIG, the "permanent config error, do
|
|
50
60
|
# not restart" sentinel. The gateway uses this on a 401 from
|
|
51
61
|
# Telegram (#1076 — revoked / wrong-typed bot token). Restarting
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
62
|
+
# just re-hits the same 401, so we quarantine and stop — the
|
|
63
|
+
# host CLI (`switchroom doctor`, `switchroom agent restart`)
|
|
64
|
+
# reads the marker at <TELEGRAM_STATE_DIR>/quarantine.json and
|
|
65
|
+
# surfaces it. This is the ONLY non-retry path: a transient
|
|
66
|
+
# dependency (vault-broker locked/recreating — RFC J) must never
|
|
67
|
+
# be terminal for an always-on agent.
|
|
58
68
|
if [ $_exit -eq 78 ]; then
|
|
59
69
|
echo "[supervise] $_name exit 78 (EX_CONFIG) — quarantined, not restarting. Operator action required." >> "$_logfile"
|
|
60
70
|
return 0
|
|
61
71
|
fi
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
# A run that stayed up for at least the backoff cap means the
|
|
73
|
+
# dependency was healthy and this exit is a fresh transient
|
|
74
|
+
# blip — reset backoff so recovery latency is minimal. Only
|
|
75
|
+
# consecutive fast failures escalate the delay.
|
|
76
|
+
if [ $_ran -ge $_cap ]; then
|
|
77
|
+
_delay=1
|
|
78
|
+
_attempt=0
|
|
65
79
|
fi
|
|
66
|
-
|
|
67
|
-
echo "[supervise] $_name exited (status=$_exit,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
fi
|
|
72
|
-
sleep 1
|
|
80
|
+
_attempt=$((_attempt + 1))
|
|
81
|
+
echo "[supervise] $_name exited (status=$_exit, ran=${_ran}s, attempt=$_attempt) — retrying in ${_delay}s (transient deps self-heal; never gives up)" >> "$_logfile"
|
|
82
|
+
sleep $_delay
|
|
83
|
+
_delay=$((_delay * 2))
|
|
84
|
+
[ $_delay -gt $_cap ] && _delay=$_cap
|
|
73
85
|
done
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
# 1) Gateway daemon — the long-running Telegram bot client.
|
|
89
|
+
# Honors channels.telegram.enabled (PR A schema, PR C wiring).
|
|
90
|
+
# When the operator sets enabled:false, skip the gateway sidecar
|
|
91
|
+
# entirely so the agent boots without bot-token requirements —
|
|
92
|
+
# used by the CI smoke-test harness and any offline-dev setup.
|
|
93
|
+
# Default behavior preserved: when the var is unset/empty, treat
|
|
94
|
+
# as enabled (no operator action required for existing agents).
|
|
77
95
|
# Polls Telegram, writes gateway.sock for the in-claude MCP
|
|
78
96
|
# sidecar to bridge through. Mirrors the v0.6 sibling
|
|
79
97
|
# switchroom-<name>-gateway.service unit. Talks to the broker
|
|
80
98
|
# over SWITCHROOM_VAULT_BROKER_SOCK (set by compose) for the bot
|
|
81
99
|
# token. Failure modes: vault locked → gateway boots, fails to
|
|
82
|
-
# fetch token, exits non-zero, supervisor
|
|
83
|
-
#
|
|
84
|
-
#
|
|
100
|
+
# fetch token, exits non-zero, supervisor backs off and keeps
|
|
101
|
+
# retrying — it recovers on its own the moment the broker
|
|
102
|
+
# unlocks (no human, no container recreate); bot token invalid
|
|
103
|
+
# → 401 → gateway exits 78 → quarantined (operator action).
|
|
85
104
|
_gateway_bundle=/opt/switchroom/telegram-plugin/dist/gateway/gateway.js
|
|
86
|
-
if
|
|
105
|
+
_telegram_enabled={{#if telegramEnabledFlag}}{{telegramEnabledFlag}}{{else}}true{{/if}}
|
|
106
|
+
if [ "$_telegram_enabled" = "true" ] && [ -f "$_gateway_bundle" ] && command -v bun >/dev/null 2>&1; then
|
|
87
107
|
_switchroom_supervise gateway /var/log/switchroom/gateway-supervisor.log \
|
|
88
108
|
bun "$_gateway_bundle" &
|
|
109
|
+
elif [ "$_telegram_enabled" != "true" ]; then
|
|
110
|
+
echo "[start.sh] channels.telegram.enabled=false — skipping gateway sidecar" >&2
|
|
89
111
|
fi
|
|
90
112
|
|
|
91
113
|
# 2) autoaccept-poll — first-run TUI prompt dispatcher. Single-shot
|
|
92
114
|
# by design (exits cleanly after idle-timeout once prompts have
|
|
93
|
-
# fired); supervisor's
|
|
94
|
-
#
|
|
115
|
+
# fired); the supervisor's exponential backoff keeps a flaky
|
|
116
|
+
# autoaccept from busy-looping.
|
|
95
117
|
if [ -f /opt/switchroom/autoaccept-poll.js ] && command -v bun >/dev/null 2>&1; then
|
|
96
118
|
_switchroom_supervise autoaccept /var/log/switchroom/autoaccept.log \
|
|
97
119
|
bun /opt/switchroom/autoaccept-poll.js "{{name}}" &
|
|
@@ -375,8 +397,8 @@ fi
|
|
|
375
397
|
# --- Wake audit sentinel ---
|
|
376
398
|
#
|
|
377
399
|
# Every boot drops a `.wake-audit-pending` sentinel into the telegram
|
|
378
|
-
# state dir. The agent's
|
|
379
|
-
#
|
|
400
|
+
# state dir. The agent's wake-audit protocol
|
|
401
|
+
# (`skills/switchroom-runtime/SKILL.md`) instructs it to detect this file at the start of
|
|
380
402
|
# its first turn after boot, run a three-signal check (owed reply /
|
|
381
403
|
# orphan sub-agents / open todos), surface findings to the user, and
|
|
382
404
|
# `rm -f` the file. This complements the SWITCHROOM_PENDING_TURN env
|
|
@@ -394,7 +416,7 @@ fi
|
|
|
394
416
|
# level dedup (so the agent doesn't re-fire the same "owed reply"
|
|
395
417
|
# audit twice on the same user message after a respawn) lives in the
|
|
396
418
|
# agent's audit logic via `.wake-audit-last-completed`, not here. See
|
|
397
|
-
# the "Conversation-aware dedup" block in
|
|
419
|
+
# the "Conversation-aware dedup" block in skills/switchroom-runtime/SKILL.md.
|
|
398
420
|
mkdir -p "$TELEGRAM_STATE_DIR" 2>/dev/null || true
|
|
399
421
|
: > "$TELEGRAM_STATE_DIR/.wake-audit-pending" 2>/dev/null || true
|
|
400
422
|
|
|
@@ -514,16 +536,34 @@ if [ -x "{{repoRoot}}/bin/boot-self-test.sh" ]; then
|
|
|
514
536
|
fi
|
|
515
537
|
{{/if}}
|
|
516
538
|
|
|
539
|
+
# --- Security-hooks plugin integrity check (sec WS8-F1 / #1416) ---
|
|
540
|
+
# The image-baked, agent-unstrippable tool-safety plugin is loaded via
|
|
541
|
+
# --plugin-dir below. If the image copy is missing/empty (bad build,
|
|
542
|
+
# tampering, downgrade) we DON'T fail boot — Claude Code unions
|
|
543
|
+
# plugin-dir hooks with the settings.json copy, which still protects —
|
|
544
|
+
# but we surface it loudly to stderr so the gap is visible instead of
|
|
545
|
+
# silently relying on the strippable fallback.
|
|
546
|
+
SR_SECPLUGIN="{{securityPluginDir}}"
|
|
547
|
+
for f in \
|
|
548
|
+
"$SR_SECPLUGIN/.claude-plugin/plugin.json" \
|
|
549
|
+
"$SR_SECPLUGIN/hooks/hooks.json" \
|
|
550
|
+
"$SR_SECPLUGIN/hooks/secret-guard-pretool.mjs" \
|
|
551
|
+
"$SR_SECPLUGIN/hooks/drive-write-pretool.mjs"; do
|
|
552
|
+
if [ ! -s "$f" ]; then
|
|
553
|
+
echo "WARNING sec WS8-F1 (#1416): security-hooks plugin artifact missing or empty: $f — tool-safety is running on the settings.json fallback only (strippable). Rebuild/redeploy the agent image." >&2
|
|
554
|
+
fi
|
|
555
|
+
done
|
|
556
|
+
|
|
517
557
|
{{#if useSwitchroomPlugin}}
|
|
518
558
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
519
|
-
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
559
|
+
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
520
560
|
else
|
|
521
|
-
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
561
|
+
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
522
562
|
fi
|
|
523
563
|
{{else}}
|
|
524
564
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
525
|
-
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
565
|
+
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
526
566
|
else
|
|
527
|
-
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
567
|
+
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}}{{#if modelQ}} --model {{{modelQ}}}{{/if}}{{#if thinkingEffort}} --effort {{thinkingEffort}}{{/if}}{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
528
568
|
fi
|
|
529
569
|
{{/if}}
|
|
@@ -122,6 +122,8 @@ A config-summary greeting card is sent automatically by the SessionStart hook
|
|
|
122
122
|
## Admin surface
|
|
123
123
|
|
|
124
124
|
You're `admin: true`. Fleet operations live on the `hostd` MCP server: `agent_restart` / `agent_start` / `agent_stop` (lifecycle of any peer), `agent_logs` (peer container logs), `agent_exec` (read-only inspection inside any peer — argv[0] must be on the safe-command allowlist), `update_check` / `update_apply`. Treat these like a root shell on the host: confirm intent before destructive actions, refuse if unsure who's asking.
|
|
125
|
+
|
|
126
|
+
Only `update_check` (a read-only dry-run) runs immediately. Every mutating / host verb — `update_apply`, `agent_exec`, `agent_restart` / `agent_start` / `agent_stop`, `agent_logs` — pauses for an **operator approval card in Telegram before it executes**: a human must tap approve. This is deliberate (you are a prompt-injectable process; the human in the loop is the safety boundary, not your own judgement). Expect the call to block until approved or denied; if denied, don't retry — relay the denial and stop.
|
|
125
127
|
{{else}}
|
|
126
128
|
## Admin operations
|
|
127
129
|
|
|
@@ -137,7 +139,7 @@ Use your available tools when appropriate. If you lack the right tool for a task
|
|
|
137
139
|
|
|
138
140
|
{{#if schedule}}
|
|
139
141
|
## Scheduled Tasks
|
|
140
|
-
You have scheduled tasks configured.
|
|
142
|
+
You have scheduled tasks configured. At fire time an in-container scheduler sidecar injects a synthesized inbound turn into **your running session** — a scheduled task arrives as an ordinary turn tagged `<channel source="cron">`, using your normal session, context, and model, and it shows up in your transcript and Hindsight memory like any other turn (it is *not* an isolated one-shot `claude -p` process). They survive reboots via the container restart policy plus an at-least-once boot replay.
|
|
141
143
|
|
|
142
|
-
You don't need to manage them. If the user asks about scheduled tasks, explain that they
|
|
144
|
+
You don't need to manage them. If the user asks about scheduled tasks, explain that they fire into your session automatically and are configured in switchroom.yaml under `schedule:`.
|
|
143
145
|
{{/if}}
|
package/skills/file-bug/SKILL.md
CHANGED
|
@@ -74,11 +74,13 @@ Switchroom's standard log map (resolve `<agent>` from the user or from `SWITCHRO
|
|
|
74
74
|
|---|---|---|
|
|
75
75
|
| Gateway events | `~/.switchroom/agents/<agent>/telegram/gateway.log` | Inbound/outbound messages, IPC, progress card, watcher, classifier output |
|
|
76
76
|
| Claude stdout/stderr | `~/.switchroom/agents/<agent>/service.log` | The agent's own session output, tool calls, errors |
|
|
77
|
-
|
|
|
78
|
-
| Cron
|
|
79
|
-
| Vault broker | `
|
|
77
|
+
| Container lifecycle | `docker logs switchroom-<agent>` | Boot/restart/crash, exit codes |
|
|
78
|
+
| Cron firings | `docker logs switchroom-<agent>` (lines prefixed `agent-scheduler:`) | Scheduled-task firings (in-container sidecar since Phase 4) |
|
|
79
|
+
| Vault broker | `docker logs switchroom-vault-broker` | Audit log, ACL gates |
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
(v0.7+ agents run in Docker — there is no systemd/`journalctl` in-container; logs are `docker logs`. Only a legacy non-docker install would use `journalctl --user -u switchroom-…`.)
|
|
82
|
+
|
|
83
|
+
For each relevant source: extract the slice that brackets the symptom window. Use `docker logs --since 10m switchroom-<agent>` or pipe through `awk '/<start-ts>/,/<end-ts>/'`. Do **not** paste raw multi-MB dumps; cap each excerpt at the lines that actually matter and signpost what was clipped.
|
|
82
84
|
|
|
83
85
|
If the gateway.log doesn't have what you need, check whether `progress-card.log`, `bridge.log`, or `subagent-watcher.log` are configured separately on this agent (some setups split).
|
|
84
86
|
|
|
@@ -234,16 +234,32 @@ List cron jobs and scheduled tasks.
|
|
|
234
234
|
|
|
235
235
|
### Step 1 — Show live timers
|
|
236
236
|
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
Since Phase 4 (#893) cron runs **in-container** as the `agent-scheduler`
|
|
238
|
+
sidecar inside each agent — the old `switchroom-<agent>-scheduler` /
|
|
239
|
+
`switchroom-cron` singleton container no longer exists. Inspect fired
|
|
240
|
+
jobs in the agent's own log; scheduler lines are prefixed
|
|
241
|
+
`agent-scheduler:`:
|
|
239
242
|
|
|
240
243
|
```bash
|
|
241
|
-
docker
|
|
244
|
+
docker logs --tail 100 switchroom-<agent> 2>&1 | grep agent-scheduler:
|
|
242
245
|
```
|
|
243
246
|
|
|
244
247
|
### Step 2 — Show declared schedule entries
|
|
245
248
|
|
|
246
|
-
From `switchroom.yaml`, the `schedule:` array under each agent specifies `cron` + `prompt`
|
|
249
|
+
From `switchroom.yaml`, the `schedule:` array under each agent specifies `cron` + `prompt`. (A `model:` field may appear but is **ignored** — since the v0.8 cron-fold-in the fire runs in the agent's existing session and uses the agent's configured model, not a per-task one. Don't tell the user a per-task model is honoured.) Cron expressions are evaluated in the agent's resolved timezone (the `switchroom.timezone` / per-agent `timezone` cascade), not hard-coded UTC. Read the relevant agent block and enumerate the entries with their next-fire times in that zone.
|
|
250
|
+
|
|
251
|
+
### Step 3 — A schedule change is NOT live until the agent restarts
|
|
252
|
+
|
|
253
|
+
The in-container `agent-scheduler` reads its entries **once at boot**. Editing the `schedule:` array in `switchroom.yaml`, or adding/removing an entry via the agent-config `schedule add` / `schedule remove` tools, writes the change to disk but does **not** register it in the running scheduler. The same is true for `skill_install` and `.mcp.json` changes — claude loads skills and MCP servers at process start.
|
|
254
|
+
|
|
255
|
+
So whenever you (or the user) change a schedule, skill, or MCP config:
|
|
256
|
+
|
|
257
|
+
- The `schedule add` / `skill_install` tool result includes `restart_required: true` and a `restart_hint`. **Surface it.** Tell the user plainly that the change is on disk but won't take effect until `switchroom agent restart <name>` (or `switchroom restart <name>` for the reconcile+restart path).
|
|
258
|
+
- Never report a just-added schedule or skill as already active. It is staged, not live.
|
|
259
|
+
|
|
260
|
+
### Step 4 — Missed runs while the agent was offline
|
|
261
|
+
|
|
262
|
+
If the user asks whether scheduled runs were missed during downtime: the scheduler replays fires missed within the last ~30 min on boot, but runs older than that window are **not** re-run (cron is not a queue). It is not silent about this — on boot it emits a one-time notice listing every schedule that had a skipped run, delivered as a normal turn and recorded in `agent-scheduler:` log lines / `/state/agent/scheduler.jsonl`. Check those to answer honestly; never claim a run happened if the skip notice says it was dropped.
|
|
247
263
|
|
|
248
264
|
---
|
|
249
265
|
|
|
@@ -107,7 +107,7 @@ This is the default. One OAuth flow per Anthropic account, then every agent in t
|
|
|
107
107
|
## What not to do
|
|
108
108
|
|
|
109
109
|
- **Do not** run `switchroom setup` non-interactively or pipe input to it — it's designed for a human.
|
|
110
|
-
- **Do not** edit `~/.switchroom/vault
|
|
111
|
-
- **Do not** run `docker build` on the operator's host. The
|
|
112
|
-
- **Do not** suggest the legacy `switchroom up` / `switchroom init`
|
|
110
|
+
- **Do not** edit the vault (`~/.switchroom/vault/`) or any file under `~/.switchroom/` directly. Use the CLI.
|
|
111
|
+
- **Do not** run `docker build` on the operator's host. The fleet images are published on GHCR; `switchroom apply` writes a compose file that pulls them.
|
|
112
|
+
- **Do not** suggest the legacy `switchroom up` / `switchroom init` verbs — they were removed. NOTE: `switchroom update` is **current and canonical** — it is the one-shot upgrade path (pull images + apply + recreate + doctor); recommend it for "how do I update". A fresh install/redeploy is `switchroom apply && docker compose pull && docker compose up -d`.
|
|
113
113
|
- **Do not** reinstall over an existing install without asking. If the user wants a clean slate, have them run `switchroom uninstall` first (or confirm they want to blow away `~/.switchroom/`).
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Format 2 — health-grouped /auth snapshot + causal auto-fallback
|
|
3
3
|
* announcement. Pure functions; the gateway handles the live-API probe
|
|
4
|
-
* (via `
|
|
4
|
+
* (via the broker `probe-quota` op, #1336) and the broker `listState`,
|
|
5
5
|
* then hands shaped data to these formatters.
|
|
6
6
|
*
|
|
7
7
|
* JTBD this module serves:
|
|
@@ -588,9 +588,9 @@ function escapeHtml(s: string): string {
|
|
|
588
588
|
* results (same length, same order), return the AccountSnapshot[] the
|
|
589
589
|
* formatters need.
|
|
590
590
|
*
|
|
591
|
-
* The gateway calls this after
|
|
592
|
-
*
|
|
593
|
-
*
|
|
591
|
+
* The gateway calls this after probing quota via the broker
|
|
592
|
+
* `probe-quota` op (#1336) — both arrays are caller-provided, this
|
|
593
|
+
* is just a zip + classify.
|
|
594
594
|
*/
|
|
595
595
|
export function buildSnapshotsFromState(
|
|
596
596
|
state: ListStateData,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared formatters for Telegram status cards.
|
|
3
3
|
*
|
|
4
|
-
* Both the main progress card (`
|
|
5
|
-
* card (`subagent-watcher.ts`) emit HTML to
|
|
6
|
-
* each
|
|
4
|
+
* Both the main progress card (rendered via `stream-reply-handler.ts`)
|
|
5
|
+
* and the pinned worker card (`subagent-watcher.ts`) emit HTML to
|
|
6
|
+
* Telegram; before issue #94 each had its own private copies with subtly
|
|
7
7
|
* different conventions:
|
|
8
8
|
*
|
|
9
9
|
* - `formatDuration(500)` → progress-card returned `500ms`, watcher
|