switchroom 0.18.31 → 0.18.32

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 (65) hide show
  1. package/dist/agent-scheduler/index.js +4 -2
  2. package/dist/auth-broker/index.js +4 -2
  3. package/dist/cli/notion-write-pretool.mjs +4 -2
  4. package/dist/cli/switchroom.js +693 -253
  5. package/dist/host-control/main.js +5 -3
  6. package/dist/vault/approvals/kernel-server.js +4 -2
  7. package/dist/vault/broker/server.js +4 -2
  8. package/package.json +3 -2
  9. package/profiles/_base/start.sh.hbs +43 -5
  10. package/telegram-plugin/dist/gateway/gateway.js +25868 -25038
  11. package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
  12. package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
  13. package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
  14. package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
  15. package/telegram-plugin/gateway/gateway.ts +7195 -8837
  16. package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
  17. package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
  18. package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
  19. package/telegram-plugin/gateway/obligation-store.ts +37 -1
  20. package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
  21. package/telegram-plugin/pending-user-notice.ts +59 -13
  22. package/telegram-plugin/subagent-watcher.ts +111 -28
  23. package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
  24. package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
  25. package/telegram-plugin/tests/buffer-gate-broadened.test.ts +16 -6
  26. package/telegram-plugin/tests/button-tap-turn-gated.test.ts +3 -3
  27. package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
  28. package/telegram-plugin/tests/emission-authority-facade.test.ts +29 -19
  29. package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
  30. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +18 -7
  31. package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +249 -0
  32. package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
  33. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
  34. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +303 -0
  35. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -3
  36. package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
  37. package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
  38. package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
  39. package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
  40. package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +4 -4
  41. package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
  42. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +9 -3
  43. package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
  44. package/telegram-plugin/tests/obligation-store.test.ts +43 -0
  45. package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +2 -1
  46. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
  47. package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
  48. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
  49. package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +3 -2
  50. package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
  51. package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +291 -0
  52. package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
  53. package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
  54. package/telegram-plugin/tests/turn-flush-safety.test.ts +8 -6
  55. package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +112 -0
  56. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
  57. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +126 -0
  58. package/telegram-plugin/tool-activity-summary.ts +27 -3
  59. package/telegram-plugin/worker-activity-feed.ts +24 -0
  60. package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
  61. package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
  62. package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
  63. package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
  64. package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
  65. package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
@@ -19265,7 +19265,8 @@ var MemoryBackendConfigSchema = exports_external.object({
19265
19265
  llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
19266
19266
  api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
19267
19267
  docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
19268
- url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
19268
+ url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
19269
+ mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
19269
19270
  }).optional()
19270
19271
  });
19271
19272
  var VaultConfigSchema = exports_external.object({
@@ -19308,7 +19309,8 @@ var HostControlConfigSchema = exports_external.object({
19308
19309
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
19309
19310
  });
19310
19311
  var WebServiceConfigSchema = exports_external.object({
19311
- managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
19312
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers."),
19313
+ port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
19312
19314
  });
19313
19315
  var FleetHealthConfigSchema = exports_external.object({
19314
19316
  owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
@@ -26645,7 +26647,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
26645
26647
  import { dirname as dirname4, join as join7 } from "node:path";
26646
26648
 
26647
26649
  // src/build-info.ts
26648
- var VERSION = "0.18.31";
26650
+ var VERSION = "0.18.32";
26649
26651
 
26650
26652
  // src/cli/resolve-version.ts
26651
26653
  function readPackageVersion() {
@@ -4685,7 +4685,8 @@ var init_schema = __esm(() => {
4685
4685
  llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
4686
4686
  api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
4687
4687
  docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
4688
- url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
4688
+ url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
4689
+ mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
4689
4690
  }).optional()
4690
4691
  });
4691
4692
  VaultConfigSchema = exports_external.object({
@@ -4728,7 +4729,8 @@ var init_schema = __esm(() => {
4728
4729
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
4729
4730
  });
4730
4731
  WebServiceConfigSchema = exports_external.object({
4731
- managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
4732
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers."),
4733
+ port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
4732
4734
  });
4733
4735
  FleetHealthConfigSchema = exports_external.object({
4734
4736
  owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
@@ -4685,7 +4685,8 @@ var init_schema = __esm(() => {
4685
4685
  llm_model: exports_external.string().optional().describe("LiteLLM model name for Hindsight's LLM ops (retain/reflect/" + "consolidation) when the top-level `litellm.enabled` carve-out is " + "on. Defaults to a cheap OpenRouter model (routed via LiteLLM's " + "model-mapped path, not the Anthropic OAuth pass-through) to keep " + "background memory-op cost off the Claude subscription quota. Set " + "to a `claude-*` model name to route it back through the OAuth " + "pass-through instead. Has no effect when litellm is disabled."),
4686
4686
  api_key: exports_external.string().optional().describe("API key or vault reference for embedding provider"),
4687
4687
  docker_service: exports_external.boolean().default(true).describe("Whether to include Hindsight in docker-compose"),
4688
- url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/.")
4688
+ url: exports_external.string().url("Hindsight URL must be a valid URL (no shell-special characters)").optional().describe("Hindsight MCP endpoint URL (e.g., http://localhost:18888/mcp/). Defaults to http://localhost:8888/mcp/."),
4689
+ mcp_transport: exports_external.enum(["shim", "http"]).optional().describe("How agents reach Hindsight's MCP surface. Default (unset or " + "'shim'): the lazy-connect stdio proxy `switchroom " + "hindsight-mcp-shim`, which always registers at session start " + "even when the hindsight container is down and reconnects " + "per-call. 'http' is the escape hatch that restores the legacy " + "direct Streamable-HTTP entry (session-fatal if the backend is " + "down when the agent session boots).")
4689
4690
  }).optional()
4690
4691
  });
4691
4692
  VaultConfigSchema = exports_external.object({
@@ -4728,7 +4729,8 @@ var init_schema = __esm(() => {
4728
4729
  auto_release_check: AutoReleaseCheckSchema.default({}).describe("Pull-based release-triggered fleet restart (#1743). hostd polls " + "the remote release tag on a fixed interval and applies + " + "restarts the fleet (graceful) when a new release is detected. " + "Opt-in: default enabled=false.")
4729
4730
  });
4730
4731
  WebServiceConfigSchema = exports_external.object({
4731
- managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers.")
4732
+ managed: exports_external.boolean().default(false).describe("Whether `switchroom update` refreshes the web-service container " + "(dashboard + GitHub-webhook receiver) via `switchroom webd " + "install`. Default: false — existing installs run the web server " + "as the legacy `switchroom-web.service` systemd unit and must not " + "be surprised by a container takeover of host loopback 127.0.0.1:" + "8080 mid-update. Set true ONLY after cutting over to the " + "container (stop+disable the systemd unit, `switchroom webd " + "install`). The container runs in its own compose project " + "(`switchroom-web`), separate from the agent fleet, with " + "network_mode: host so it keeps owning loopback:8080 for the " + "cloudflared tunnel + tailscale serve consumers."),
4733
+ port: exports_external.number().int().min(1).max(65535).default(8080).describe("Host loopback port the web server listens on (127.0.0.1:<port>). " + "Default: 8080. Set this when another service owns 8080 on the " + "host (e.g. UniFi OS Server) — `switchroom webd install` and the " + "`switchroom update` refresh-web step both honor it, so a custom " + "port survives updates instead of being silently reverted to " + "8080 by the regenerated compose file. Remember to point the " + "host-side consumers (cloudflared tunnel, `tailscale serve`) at " + "the same port.")
4732
4734
  });
4733
4735
  FleetHealthConfigSchema = exports_external.object({
4734
4736
  owner_agent: exports_external.string().regex(/^[a-z0-9][a-z0-9_-]{0,50}$/, {
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.18.31",
4
+ "version": "0.18.32",
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": {
@@ -28,7 +28,7 @@
28
28
  "test:vitest": "vitest run",
29
29
  "test:bun": "bun test 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-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/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/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
- "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-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs",
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-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs",
32
32
  "lint:tsc": "tsc --noEmit",
33
33
  "lint:plugin-references": "node scripts/check-plugin-references.mjs",
34
34
  "lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
@@ -36,6 +36,7 @@
36
36
  "lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
37
37
  "lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
38
38
  "lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
39
+ "lint:gateway-line-ratchet": "node scripts/check-gateway-line-ratchet.mjs",
39
40
  "prepublishOnly": "npm run build && npm run lint && npm test"
40
41
  },
41
42
  "dependencies": {
@@ -602,16 +602,54 @@ export HINDSIGHT_SENDER_BANKS_JSON={{{hindsightSenderBanksJsonQ}}}
602
602
  {{#if hindsightTopicFilterMode}}
603
603
  export HINDSIGHT_TOPIC_FILTER_MODE={{hindsightTopicFilterMode}}
604
604
  {{/if}}
605
- # Wait for Hindsight API to be reachable before launching Claude, otherwise
606
- # the MCP server connection fails at startup with "1 MCP server failed".
605
+ # Give Claude Code a longer MCP server-startup budget (ms). The default can
606
+ # race a Hindsight restart window; the boot gate below usually absorbs it,
607
+ # but this keeps the in-session connect attempt itself patient too.
608
+ # Documented Claude Code env var (MCP server startup timeout).
609
+ export MCP_TIMEOUT="${MCP_TIMEOUT:-30000}"
610
+ # Wait for Hindsight MCP to be READY before launching Claude, otherwise the
611
+ # MCP server connection fails at startup with "1 MCP server failed" and the
612
+ # session boots toolless (incident 2026-07-18: clerk booted toolless despite
613
+ # a 30s bare-GET gate — the endpoint returned HTTP 200 mid-restart while a
614
+ # real MCP initialize still failed). So: probe with an actual JSON-RPC
615
+ # initialize POST and require a JSON-RPC result, not just HTTP 200.
616
+ # Bounded at 120s of real wall-clock time (each probe costs up to ~3s of
617
+ # curl timeout + 1s sleep, so an iteration count would overshoot — measure
618
+ # elapsed seconds against the deadline instead; HINDSIGHT_WAIT holds real
619
+ # elapsed seconds). On timeout we log a warning and boot anyway (degraded
620
+ # beats never-boots — the plugin reconnects once Hindsight recovers).
607
621
  HINDSIGHT_WAIT=0
608
- while [ $HINDSIGHT_WAIT -lt 30 ]; do
609
- if curl -sf -o /dev/null --max-time 2 {{{hindsightApiBaseUrlQ}}}/mcp/; then
622
+ HINDSIGHT_READY=0
623
+ _hs_start=$(date +%s)
624
+ while [ $HINDSIGHT_WAIT -lt 120 ]; do
625
+ _hs_resp=$(curl -sf --max-time 3 -i \
626
+ -X POST {{{hindsightApiBaseUrlQ}}}/mcp/ \
627
+ -H "Content-Type: application/json" \
628
+ -H "Accept: application/json, text/event-stream" \
629
+ -d '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"switchroom-boot-gate","version":"1.0"}}}' \
630
+ 2>/dev/null)
631
+ # Match on serverInfo — only present in a successful initialize result;
632
+ # a JSON-RPC *error* body could legitimately contain the substring "result".
633
+ if printf '%s\n' "$_hs_resp" | grep -q '"serverInfo"'; then
634
+ HINDSIGHT_READY=1
635
+ # Best-effort cleanup of the probe's server-side MCP session (one would
636
+ # leak per boot otherwise). Header may be absent on stateless servers.
637
+ _hs_sid=$(printf '%s\n' "$_hs_resp" | tr -d '\r' | sed -n 's/^[Mm][Cc][Pp]-[Ss]ession-[Ii][Dd]: *//p' | head -n 1)
638
+ if [ -n "$_hs_sid" ]; then
639
+ curl -s -o /dev/null --max-time 2 -X DELETE {{{hindsightApiBaseUrlQ}}}/mcp/ \
640
+ -H "Mcp-Session-Id: $_hs_sid" 2>/dev/null || true
641
+ fi
642
+ HINDSIGHT_WAIT=$(( $(date +%s) - _hs_start ))
610
643
  break
611
644
  fi
612
645
  sleep 1
613
- HINDSIGHT_WAIT=$((HINDSIGHT_WAIT + 1))
646
+ HINDSIGHT_WAIT=$(( $(date +%s) - _hs_start ))
614
647
  done
648
+ if [ "$HINDSIGHT_READY" = "1" ]; then
649
+ echo "start.sh: hindsight boot gate — MCP initialize OK after ${HINDSIGHT_WAIT}s" >&2
650
+ else
651
+ echo "start.sh: WARNING hindsight boot gate TIMED OUT after ${HINDSIGHT_WAIT}s — MCP initialize never returned a JSON-RPC result; launching claude anyway (memory tools may be unavailable this session)" >&2
652
+ fi
615
653
  {{/if}}
616
654
  {{#if userEnvQuoted}}
617
655
  # User-declared env vars from switchroom.yaml (agent + defaults merged).