switchroom 0.15.2 → 0.15.4
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/bin/turn-pacing-hook.sh +112 -0
- package/bin/workspace-dynamic-hook.sh +105 -15
- package/bin/workspace-stable-hook.sh +2 -2
- package/dist/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +75 -12
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +1596 -1515
- package/dist/host-control/main.js +2 -1
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +2 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +35 -2
- package/profiles/default/CLAUDE.md.hbs +13 -4
- package/telegram-plugin/dist/gateway/gateway.js +533 -33
- package/telegram-plugin/gateway/gateway.ts +152 -14
- package/telegram-plugin/gateway/inbound-spool.ts +107 -16
- package/telegram-plugin/gateway/model-command.ts +261 -7
- package/telegram-plugin/tests/inbound-spool.test.ts +101 -0
- package/telegram-plugin/tests/model-command.test.ts +179 -0
- package/telegram-plugin/tests/welcome-text.test.ts +11 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-command-dm.test.ts +93 -0
- package/telegram-plugin/welcome-text.ts +16 -1
- package/profiles/default/workspace/HEARTBEAT.md.hbs +0 -40
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UAT — `/model` Telegram command (PR #2259, shipped v0.15.2).
|
|
3
|
+
*
|
|
4
|
+
* Serves: `reference/vision.md` outcome 2 (you hold the leash) — the
|
|
5
|
+
* operator can see and switch the agent's Claude model from Telegram
|
|
6
|
+
* without SSH. Session-scoped switch via claude's own `/model <name>`
|
|
7
|
+
* REPL verb injected into the tmux pane.
|
|
8
|
+
*
|
|
9
|
+
* Three assertions against a real agent over real Telegram:
|
|
10
|
+
*
|
|
11
|
+
* 1. Bare `/model` → shows the configured model (never opens claude's
|
|
12
|
+
* interactive picker — the reply must come from the gateway, fast,
|
|
13
|
+
* containing "Configured:").
|
|
14
|
+
* 2. `/model <valid-name>` → switch is injected; reply relays claude's
|
|
15
|
+
* response and carries the session-only persistence note.
|
|
16
|
+
* 3. `/model bogus-name` → still a reply (claude's inline error is
|
|
17
|
+
* relayed, or the empty-capture explanation) — never silence.
|
|
18
|
+
*
|
|
19
|
+
* The switch test sets the model to the SAME value the agent already
|
|
20
|
+
* runs (sonnet) so the canary doesn't leave the harness agent on a
|
|
21
|
+
* different model afterwards.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from "vitest";
|
|
25
|
+
import { spinUp } from "../harness.js";
|
|
26
|
+
|
|
27
|
+
const AGENT = "test-harness";
|
|
28
|
+
const REPLY_TIMEOUT_MS = 30_000;
|
|
29
|
+
|
|
30
|
+
describe("uat: /model command — show, switch, bad-name", () => {
|
|
31
|
+
it(
|
|
32
|
+
"bare /model shows the model dashboard (menu v2) or static fallback (v1)",
|
|
33
|
+
async () => {
|
|
34
|
+
const sc = await spinUp({ agent: AGENT });
|
|
35
|
+
try {
|
|
36
|
+
await sc.sendDM("/model");
|
|
37
|
+
// v2 (picker-driven menu): "Now: <model>"; v1 / fallback path:
|
|
38
|
+
// "Configured: <model>". Either proves the gateway handled the
|
|
39
|
+
// command rather than forwarding it to claude as plain text.
|
|
40
|
+
const shape = /Now:|Configured:/i;
|
|
41
|
+
const reply = await sc.expectMessage(shape, {
|
|
42
|
+
from: "bot",
|
|
43
|
+
timeout: REPLY_TIMEOUT_MS,
|
|
44
|
+
});
|
|
45
|
+
expect(reply.text).toMatch(shape);
|
|
46
|
+
// Persistence caveat present on both shapes
|
|
47
|
+
expect(reply.text).toMatch(/switchroom\.yaml/i);
|
|
48
|
+
} finally {
|
|
49
|
+
await sc.tearDown();
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
60_000,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
it(
|
|
56
|
+
"/model sonnet switches the live session (same-value, no net change)",
|
|
57
|
+
async () => {
|
|
58
|
+
const sc = await spinUp({ agent: AGENT });
|
|
59
|
+
try {
|
|
60
|
+
await sc.sendDM("/model sonnet");
|
|
61
|
+
// Accept either a relayed claude response or the explicit
|
|
62
|
+
// empty-capture explanation — both prove the command routed
|
|
63
|
+
// through the gateway handler (and neither is silence).
|
|
64
|
+
const reply = await sc.expectMessage(
|
|
65
|
+
/\/model sonnet|no response captured|Session-only/i,
|
|
66
|
+
{ from: "bot", timeout: REPLY_TIMEOUT_MS },
|
|
67
|
+
);
|
|
68
|
+
expect(reply.text.length).toBeGreaterThan(0);
|
|
69
|
+
} finally {
|
|
70
|
+
await sc.tearDown();
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
60_000,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
it(
|
|
77
|
+
"/model bogus-name still gets a reply (error relayed, never silence)",
|
|
78
|
+
async () => {
|
|
79
|
+
const sc = await spinUp({ agent: AGENT });
|
|
80
|
+
try {
|
|
81
|
+
await sc.sendDM("/model bogus-model-name-xyz");
|
|
82
|
+
const reply = await sc.expectMessage(/\S/, {
|
|
83
|
+
from: "bot",
|
|
84
|
+
timeout: REPLY_TIMEOUT_MS,
|
|
85
|
+
});
|
|
86
|
+
expect(reply.text.length).toBeGreaterThan(0);
|
|
87
|
+
} finally {
|
|
88
|
+
await sc.tearDown();
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
60_000,
|
|
92
|
+
);
|
|
93
|
+
});
|
|
@@ -66,6 +66,14 @@ export type StatusProbeRow = {
|
|
|
66
66
|
export type AgentMetadata = {
|
|
67
67
|
agentName: string;
|
|
68
68
|
model: string | null;
|
|
69
|
+
/**
|
|
70
|
+
* Live session-model override set via the `/model` picker (session-only,
|
|
71
|
+
* resets on restart). When present it's what the agent is ACTUALLY running
|
|
72
|
+
* right now, distinct from `model` (the persistent configured model). Null
|
|
73
|
+
* when no session switch is active — then `/status` just shows `model`.
|
|
74
|
+
* Surfaced so `/status` and `/model` never silently disagree.
|
|
75
|
+
*/
|
|
76
|
+
sessionModel?: string | null;
|
|
69
77
|
extendsProfile: string | null;
|
|
70
78
|
topicName: string | null;
|
|
71
79
|
topicEmoji: string | null;
|
|
@@ -122,7 +130,14 @@ export function formatAgentLine(meta: AgentMetadata): string {
|
|
|
122
130
|
const topic = meta.topicName
|
|
123
131
|
? ` · topic: ${escapeHtml([meta.topicEmoji, meta.topicName].filter(Boolean).join(" "))}`
|
|
124
132
|
: "";
|
|
125
|
-
|
|
133
|
+
// A live `/model` session switch overrides what's running. Show it next to
|
|
134
|
+
// the configured model so the two surfaces agree (the override resets on
|
|
135
|
+
// restart, when the session reverts to the configured model).
|
|
136
|
+
const session =
|
|
137
|
+
meta.sessionModel && meta.sessionModel.length > 0
|
|
138
|
+
? ` · live session: <code>${escapeHtml(meta.sessionModel)}</code>`
|
|
139
|
+
: "";
|
|
140
|
+
return `<b>${escapeHtml(meta.agentName)}</b> · model: <code>${escapeHtml(m)}</code>${session}${topic}`;
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
/**
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# HEARTBEAT.md — Proactive Check-Ins
|
|
2
|
-
|
|
3
|
-
This file is read on every turn (it's a dynamic workspace file). Edit it
|
|
4
|
-
to tell yourself what to look for when someone (or something) prompts you
|
|
5
|
-
with a bare "heartbeat" — a cron firing, a quiet-period nudge, or a
|
|
6
|
-
scheduled check-in.
|
|
7
|
-
|
|
8
|
-
## When this fires
|
|
9
|
-
|
|
10
|
-
A heartbeat arrives as a user-role message with no real payload — often
|
|
11
|
-
just "HEARTBEAT" or "heartbeat check". When that happens:
|
|
12
|
-
|
|
13
|
-
1. Run through the bullets below in order.
|
|
14
|
-
2. If anything needs action, respond normally (and take the action).
|
|
15
|
-
3. If nothing needs action, respond with exactly `HEARTBEAT_OK` on its
|
|
16
|
-
own line. The plugin suppresses that as a silent reply — no Telegram
|
|
17
|
-
message gets sent, and the user isn't notified.
|
|
18
|
-
|
|
19
|
-
## Things to check (customize per-agent)
|
|
20
|
-
|
|
21
|
-
- **New emails / messages:** is there anything in the inbox or
|
|
22
|
-
connected channels that looks actionable?
|
|
23
|
-
- **Upcoming calendar events:** anything in the next ~2 hours the user
|
|
24
|
-
should be reminded of?
|
|
25
|
-
- **Long-running tasks:** any background work you kicked off earlier
|
|
26
|
-
that might have completed?
|
|
27
|
-
- **Today's plan:** anything in `memory/YYYY-MM-DD.md` (today's
|
|
28
|
-
daily note, auto-loaded into context by the dynamic workspace hook)
|
|
29
|
-
that hasn't been touched?
|
|
30
|
-
|
|
31
|
-
## Guidelines
|
|
32
|
-
|
|
33
|
-
- **Respect quiet hours.** If it's late (local time 22:00–08:00),
|
|
34
|
-
default to `HEARTBEAT_OK` unless something is genuinely urgent.
|
|
35
|
-
- **Don't spam.** If you messaged the user in the last 30 minutes,
|
|
36
|
-
`HEARTBEAT_OK` unless there's something new to add.
|
|
37
|
-
- **Stay terse.** A heartbeat-initiated message should be one or two
|
|
38
|
-
lines, not a paragraph.
|
|
39
|
-
|
|
40
|
-
Edit this file to narrow or broaden the check set for this agent.
|