switchroom 0.19.47 → 0.20.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.
Files changed (50) hide show
  1. package/dist/agent-scheduler/index.js +22 -1
  2. package/dist/auth-broker/index.js +26 -2
  3. package/dist/buzz-gateway/index.js +9207 -0
  4. package/dist/cli/notion-write-pretool.mjs +22 -1
  5. package/dist/cli/switchroom.js +91 -9
  6. package/dist/host-control/main.js +27 -3
  7. package/dist/vault/approvals/kernel-server.js +26 -2
  8. package/dist/vault/broker/server.js +26 -2
  9. package/package.json +4 -3
  10. package/profiles/_base/start.sh.hbs +78 -1
  11. package/profiles/default/CLAUDE.md.hbs +1 -1
  12. package/skills/dev-protocol/SKILL.md +30 -1
  13. package/skills/switchroom-architecture/SKILL.md +5 -0
  14. package/skills/switchroom-cli/SKILL.md +1 -1
  15. package/telegram-plugin/dist/bridge/bridge.js +7 -4
  16. package/telegram-plugin/dist/gateway/gateway.js +1156 -247
  17. package/telegram-plugin/dist/server.js +7 -4
  18. package/telegram-plugin/gateway/boot-briefing-builder.ts +458 -0
  19. package/telegram-plugin/gateway/boot-briefing-wiring.ts +170 -0
  20. package/telegram-plugin/gateway/buzz-mirror.ts +329 -0
  21. package/telegram-plugin/gateway/buzz-type-guards.ts +34 -0
  22. package/telegram-plugin/gateway/channel-route.ts +272 -0
  23. package/telegram-plugin/gateway/gateway.ts +73 -81
  24. package/telegram-plugin/gateway/inbound-spool.ts +33 -1
  25. package/telegram-plugin/gateway/ipc-protocol.ts +81 -2
  26. package/telegram-plugin/gateway/ipc-server.ts +197 -2
  27. package/telegram-plugin/gateway/outbound-send-path.ts +37 -1
  28. package/telegram-plugin/gateway/pending-turn-env.ts +61 -0
  29. package/telegram-plugin/gateway/stream-render.ts +21 -0
  30. package/telegram-plugin/gateway/subagent-handback-marker.ts +12 -0
  31. package/telegram-plugin/gateway/user-failure-notices.ts +172 -0
  32. package/telegram-plugin/history.ts +15 -0
  33. package/telegram-plugin/llm-error-present.ts +9 -4
  34. package/telegram-plugin/model-unavailable.ts +4 -0
  35. package/telegram-plugin/operator-events.fixtures.json +12 -12
  36. package/telegram-plugin/operator-events.ts +81 -9
  37. package/telegram-plugin/session-tail.ts +7 -1
  38. package/telegram-plugin/tests/boot-briefing-builder.test.ts +604 -0
  39. package/telegram-plugin/tests/buzz-mirror.test.ts +242 -0
  40. package/telegram-plugin/tests/buzz-origin-stamp-gate.test.ts +159 -0
  41. package/telegram-plugin/tests/channel-route.test.ts +306 -0
  42. package/telegram-plugin/tests/inbound-spool.test.ts +47 -0
  43. package/telegram-plugin/tests/ipc-server-buzz-dedup.test.ts +124 -0
  44. package/telegram-plugin/tests/ipc-server-buzz-peer.test.ts +269 -0
  45. package/telegram-plugin/tests/operator-events-session-tail.test.ts +63 -0
  46. package/telegram-plugin/tests/operator-events.test.ts +71 -7
  47. package/telegram-plugin/tests/user-failure-notices.test.ts +165 -0
  48. package/telegram-plugin/voice-normalize-text.ts +5 -0
  49. package/vendor/hindsight-memory/scripts/directive_verify.py +4 -0
  50. package/vendor/hindsight-memory/scripts/recall.py +7 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.19.47",
4
+ "version": "0.20.0",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -24,9 +24,9 @@
24
24
  "build": "node scripts/build.mjs",
25
25
  "build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
26
26
  "pretest": "npm run build",
27
- "test": "vitest run && bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.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/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.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/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
27
+ "test": "vitest run && bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.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/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.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/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
28
28
  "test:vitest": "vitest run",
29
- "test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.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-scope-persist.test.ts src/vault/broker/server-tokenless-scope.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/server-admin-only-keys.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/litellm/provision-apply-e2e.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/approval-origin.test.ts src/vault/approvals/self-approval-bypass.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/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.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/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.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/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
29
+ "test:bun": "bun test telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts src/vault/grants.test.ts src/vault/grants-db.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-scope-persist.test.ts src/vault/broker/server-tokenless-scope.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/server-admin-only-keys.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/litellm/provision-apply-e2e.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/approval-origin.test.ts src/vault/approvals/self-approval-bypass.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/boot-briefing-builder.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.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/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.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/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
31
  "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts",
32
32
  "lint:tsc": "tsc --noEmit",
@@ -63,6 +63,7 @@
63
63
  "commander": "^13.1.0",
64
64
  "grammy": "^1.44",
65
65
  "handlebars": "^4.7.8",
66
+ "nostr-tools": "^2.24.1",
66
67
  "posthog-node": "^5.29.2",
67
68
  "yaml": "^2.7.0",
68
69
  "zod": "^3.24.0"
@@ -57,6 +57,48 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
57
57
  # `claude` --continue path, not the gateway), so this needs its own hoist.
58
58
  export SWITCHROOM_BOOT_RESUME="{{#if bootResumeMode}}{{{bootResumeMode}}}{{else}}in-flight{{/if}}"
59
59
 
60
+ # Boot-briefing transport flag (session_continuity.briefing; default
61
+ # 'legacy'). When 'gateway', the gateway daemon assembles the fresh-session
62
+ # reorientation briefing at boot from the durable history.db and injects it
63
+ # as a spooled synthetic inbound (<channel source="boot_briefing">) instead
64
+ # of the handoff-briefing.sh --append-system-prompt path. MUST be exported
65
+ # here, before the gateway fork below (same env-fork landmine as
66
+ # SWITCHROOM_BOOT_RESUME). The gateway also needs the resume MODE to
67
+ # suppress the briefing when 'continue'/'auto' may replay the transcript —
68
+ # hoist that too (the inner-pass SWITCHROOM_RESUME_MODE assignment below
69
+ # renders the same template value, so the two can never disagree).
70
+ export SWITCHROOM_SESSION_BRIEFING="{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}"
71
+ export SWITCHROOM_RESUME_MODE="{{#if resumeMode}}{{{resumeMode}}}{{else}}handoff{{/if}}"
72
+
73
+ # Force-fresh suppression signal for the gateway boot briefing. The gateway
74
+ # suppresses the briefing when this boot is a /reset·/new fresh start —
75
+ # re-feeding the just-reset conversation into the fresh session would defeat
76
+ # the reset. It MUST NOT rely on existsSync(.force-fresh-session) at its own
77
+ # module-eval time: this OUTER pass forks the gateway with `&` (below) then
78
+ # execs the inner tmux pass, whose CONTINUE_FLAG block `rm`s the marker (see
79
+ # ~line 1262). The fork and the `rm` race with no ordering, and under boot
80
+ # CPU contention the `rm` can win before the gateway bundle evaluates its
81
+ # briefing check — the marker vanishes and the reset session gets re-fed the
82
+ # conversation it just reset. Snapshot the marker into env HERE, before the
83
+ # fork, so the gateway reads a value fixed at fork time and immune to the
84
+ # later `rm`. The inner pass still consumes the marker for the claude
85
+ # --continue / session-mode logic; this only mirrors it for the fork race.
86
+ if [ -f "{{agentDir}}/.force-fresh-session" ]; then
87
+ export SWITCHROOM_FORCE_FRESH=1
88
+ fi
89
+
90
+ # Session-generation stamp for the gateway boot briefing (#4242). Set ONCE
91
+ # here, per real boot, before the gateway fork below. `_switchroom_supervise`
92
+ # respawns the gateway `bun` process in a `while` loop inside this same shell
93
+ # (it does NOT re-run start.sh), so every crash-respawn inherits this exact
94
+ # value, while the next real container boot re-derives a fresh one. The
95
+ # gateway persists it the first time it briefs and skips re-minting when the
96
+ # persisted stamp matches — that distinguishes "supervisor respawned me into
97
+ # a still-live Claude session" (skip) from "genuine new boot" (brief). The
98
+ # epoch second keeps it monotonic across boots; $$ (this outer pass's PID)
99
+ # plus ${RANDOM} guarantee uniqueness even for two boots within one second.
100
+ export SWITCHROOM_GATEWAY_BOOT_ID="$(date +%s)-$$-${RANDOM}"
101
+
60
102
  # Gateway-consumed env MUST be exported HERE, before the gateway fork
61
103
  # below. The gateway daemon reads channels.telegram.* knobs (and any
62
104
  # agent env) from process.env at startup — e.g. SWITCHROOM_TG_STREAM_
@@ -455,6 +497,34 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
455
497
  bun /opt/switchroom/agent-scheduler/index.js &
456
498
  fi
457
499
 
500
+ # 3b) Buzz co-channel inbound sidecar (Phase 1, channels.buzz).
501
+ # A supervised sibling that opens a WebSocket Nostr subscription to a
502
+ # closed Buzz relay, NIP-42-authenticates, and injects allowlisted
503
+ # messages onto the gateway socket as synthesized turns
504
+ # (meta.source="buzz") — exactly like the cron sidecar above. INBOUND
505
+ # ONLY in Phase 1; replies land on Telegram (the authoritative surface).
506
+ #
507
+ # Default OFF and deterministic: the block is present for every agent
508
+ # (start.sh stays byte-identical fleet-wide) but forks the sidecar ONLY
509
+ # when BUZZ_ENABLED=1 is set in the container env. NOTE: nothing in this
510
+ # branch sets BUZZ_ENABLED (or the other BUZZ_* vars) — projecting an
511
+ # ENABLED channels.buzz block into container env is DEFERRED to the
512
+ # deploy-wiring phase. Until then the env is unset for every agent and
513
+ # this block is a no-op, so the channel is dark by construction. The
514
+ # sidecar itself also fails closed if its env is incomplete, and fetches
515
+ # the nsec in-process from the vault broker (never an env var, never
516
+ # logged).
517
+ if [ "$BUZZ_ENABLED" = "1" ] \
518
+ && [ -f /opt/switchroom/buzz-gateway/index.js ] \
519
+ && command -v bun >/dev/null 2>&1; then
520
+ # --oneshot-ok: the sidecar exits 0 when the channel resolves not-live
521
+ # (e.g. mirror:off, or a not-live env). A clean exit 0 = "nothing to do",
522
+ # so DON'T respawn it every 60s (that is the documented churn pattern
523
+ # above). A non-zero crash while live still respawns.
524
+ _switchroom_supervise buzz-gateway /var/log/switchroom/buzz-gateway.log --oneshot-ok \
525
+ bun /opt/switchroom/buzz-gateway/index.js &
526
+ fi
527
+
458
528
  {{#if cronSessionEnabled}}
459
529
  # 4) cheap cron SESSION (Tier 1, reference/rfcs/cheap-cron-sessions.md §2.2).
460
530
  # A SECOND interactive claude (no -p) dedicated to context:fresh cron
@@ -1353,7 +1423,14 @@ if [ "$_HANDOFF_STALE" = "1" ]; then
1353
1423
  # daily memory into a compact context brief. Requires python3 (stdlib
1354
1424
  # sqlite3) for the Telegram history section; other sections only need
1355
1425
  # curl + jq (both optional — each source degrades gracefully).
1356
- if [ "$SWITCHROOM_RESUME_MODE" = "handoff" ] && command -v handoff-briefing.sh >/dev/null 2>&1; then
1426
+ #
1427
+ # SKIPPED when session_continuity.briefing='gateway': the gateway daemon
1428
+ # then owns the reorientation briefing (built from the durable history.db
1429
+ # and injected as a spooled <channel source="boot_briefing"> inbound), so
1430
+ # running the shell assembler here would double-inject the same recent
1431
+ # conversation. The Stop-hook .handoff.md path below is left untouched in
1432
+ # this stage of the migration.
1433
+ if [ "$SWITCHROOM_RESUME_MODE" = "handoff" ] && [ "{{#if sessionBriefingMode}}{{{sessionBriefingMode}}}{{else}}legacy{{/if}}" != "gateway" ] && command -v handoff-briefing.sh >/dev/null 2>&1; then
1357
1434
  export AGENT_DIR="{{agentDir}}"
1358
1435
  timeout 5 handoff-briefing.sh 2>/dev/null || true
1359
1436
  fi
@@ -50,7 +50,7 @@ Creating, updating, or refreshing a mental model is **operator-approved** — do
50
50
  ### Directives (replaces feedback rules)
51
51
  Hard rules the agent must follow during reflect — guardrails that are always applied.
52
52
 
53
- - `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file.
53
+ - `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file. If the rule can be enforced deterministically — a settings.json hook, a permission rule, a skill/script edit, or a config change — prefer that (instead of, or in addition to, the directive) and say which you did; reserve directives for judgment rules code can't enforce.
54
54
 
55
55
  (Read-only inspection tools like `list_memories`, `list_mental_models`, `get_mental_model`, `list_directives` are available under the `mcp__hindsight__*` namespace if you ever need them, but you rarely should — Hindsight's own auto-recall surfaces what matters and the operator handles bank curation out-of-band. The mental-model WRITE tools — `create`/`update`/`refresh`/`delete_mental_model` — are gated: propose via `mcp__switchroom-telegram__mental_model_propose`, don't call them directly.)
56
56
 
@@ -18,6 +18,32 @@ judgement criteria. This skill carries the parts that are *this fleet's
18
18
  specific opinion* — the ones you would get wrong by defaulting to generic
19
19
  good practice, because our answer differs from the obvious one.
20
20
 
21
+ ## 0. The design contract binds first (repos that declare one)
22
+
23
+ Switchroom declares its design contract in `reference/` (`reference/README.md`
24
+ is the map; the repo CLAUDE.md "Design contract" section binds it to every
25
+ PR). When the repo you are changing carries one, the whole protocol below
26
+ runs *inside* that contract:
27
+
28
+ - **Before non-trivial work, place the change in the contract:** which of the
29
+ four outcomes it advances, and which job spec it satisfies —
30
+ `reference/product-spec.md` owns the job index; survey specs cheaply with
31
+ `head -7 reference/jobs/*.md`. A change that maps to no outcome and no job
32
+ is a scoping question, not a coding task.
33
+ - **The ship gate is the verdict rule, not just CI green:** a change ships
34
+ only when it (a) advances one of the four outcomes, (b) satisfies its job
35
+ spec — proven by that job's outcome UAT, (c) passes all three principle
36
+ checks in `reference/principles.md` (docs / defaults / consistency; a "no"
37
+ is a redesign, not a ship-and-patch), and (d) crosses no invariant in
38
+ `reference/invariants.md`. Cite the job spec in the PR body.
39
+ - **Design lives in `reference/`, not `docs/`.** A design decision worth
40
+ recording is an RFC or design record in `reference/rfcs/` (`serves:` a job
41
+ or `backs:` an invariant), never a new doc under `docs/`.
42
+
43
+ This does not add ceremony to small changes: §1's fast path stands, and on a
44
+ single-concern change the verdict check is a one-line sanity pass, not a
45
+ report.
46
+
21
47
  ## 1. Is this a "larger" task? (decides whether you design-align first)
22
48
 
23
49
  Treat it as larger — design report before implementing — when ANY of these hold:
@@ -37,7 +63,10 @@ Design-aligning a one-liner is its own failure mode.
37
63
 
38
64
  The report states what exists today **with citations**, what will change, the
39
65
  chosen approach, the alternatives you rejected and why, and the PR staging
40
- plan. Get alignment before implementing.
66
+ plan. In a contract-carrying repo (§0) it opens with the verdict-rule
67
+ mapping: the outcome advanced, the job spec satisfied, and any principle
68
+ check or invariant the design brushes against. Get alignment before
69
+ implementing.
41
70
 
42
71
  Then red-team your own plan item by item. Each item gets a verdict —
43
72
  `SOUND`, `RISK`, or `WRONG` — backed by evidence you can point at (a file you
@@ -59,3 +59,8 @@ Switchroom is a multi-agent orchestrator built on Claude Code. It manages multip
59
59
  - [cascade.md](cascade.md) — three-layer config cascade semantics
60
60
  - [sub-agents.md](sub-agents.md) — delegation patterns and model routing
61
61
  - [telegram.md](telegram.md) — enhanced Telegram plugin features
62
+
63
+ **Why it's built this way** lives in the repo's `reference/` directory — the
64
+ design contract (`reference/README.md` is the map: vision, principles,
65
+ invariants, product spec, job specs, RFCs). For "why does switchroom do X"
66
+ questions, that is the source of truth; `docs/` is usage/operation only.
@@ -290,7 +290,7 @@ config-plus-recreate operation with three known traps. **Follow the runbook:
290
290
 
291
291
  ## Telegram plugin reference — "what MCP tools", "how does reply work"
292
292
 
293
- The `switchroom-telegram` plugin is an enhanced fork of the official Telegram MCP plugin and is the default for all switchroom agents. It exposes **9 MCP tools** (all prefixed `mcp__switchroom-telegram__`):
293
+ The `switchroom-telegram` plugin is an enhanced fork of the official Telegram MCP plugin and is the default for all switchroom agents. It exposes **20+ MCP tools** (all prefixed `mcp__switchroom-telegram__`; the authoritative list is the tool declarations in `telegram-plugin/bridge/bridge.ts`). The core messaging set:
294
294
 
295
295
  | Tool | Purpose |
296
296
  |---|---|
@@ -23121,12 +23121,12 @@ function classifyClaudeError(raw) {
23121
23121
  try {
23122
23122
  return classifyInner(raw);
23123
23123
  } catch {
23124
- return "unknown-4xx";
23124
+ return "unknown-5xx";
23125
23125
  }
23126
23126
  }
23127
23127
  function classifyInner(raw) {
23128
23128
  if (raw == null)
23129
- return "unknown-4xx";
23129
+ return "unknown-5xx";
23130
23130
  const obj = typeof raw === "object" ? raw : {};
23131
23131
  const errorType = extractString(obj, "error_type") ?? extractString(obj, "type") ?? extractString(getNestedObj(obj, "error"), "type") ?? "";
23132
23132
  const errorCode = extractString(obj, "code") ?? extractString(getNestedObj(obj, "error"), "code") ?? "";
@@ -23171,13 +23171,16 @@ ${message}`;
23171
23171
  if (errorType === "agent-restarted-unexpectedly" || errorCode === "agent-restarted-unexpectedly") {
23172
23172
  return "agent-restarted-unexpectedly";
23173
23173
  }
23174
+ if ((status == null || status >= 500) && (errorType === "server_error" || errorCode === "server_error" || sdkCode === "server_error" || errorType === "api_error" || errorCode === "api_error" || sdkCode === "api_error")) {
23175
+ return "transport-transient";
23176
+ }
23174
23177
  if (status != null) {
23175
23178
  if (status >= 400 && status < 500)
23176
23179
  return "unknown-4xx";
23177
23180
  if (status >= 500 && status < 600)
23178
23181
  return "unknown-5xx";
23179
23182
  }
23180
- return "unknown-4xx";
23183
+ return "unknown-5xx";
23181
23184
  }
23182
23185
  function extractString(obj, key) {
23183
23186
  const v = obj[key];
@@ -23708,7 +23711,7 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
23708
23711
  const raw = embeddedError ?? obj;
23709
23712
  const kind = classifyClaudeError(embeddedError ?? obj);
23710
23713
  const detail = extractDetailMessage(embeddedError) ?? extractDetailMessage(obj) ?? String(type ?? "");
23711
- const transient = kind === "rate-limited";
23714
+ const transient = kind === "rate-limited" || kind === "transport-transient";
23712
23715
  const retry = extractRetryState(obj);
23713
23716
  const terminal = !transient ? true : retry.retryAttempt != null && retry.maxRetries != null ? retry.retryAttempt >= retry.maxRetries : isErrorLine;
23714
23717
  return { kind, raw, detail, transient, terminal };