switchroom 0.18.3 → 0.18.6
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/dist/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +2 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +119 -74
- package/dist/host-control/main.js +3 -2
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +37 -8
- package/package.json +1 -1
- package/skills/switchroom-release/SKILL.md +78 -0
- package/telegram-plugin/auth-snapshot-format.ts +15 -1
- package/telegram-plugin/dist/gateway/gateway.js +922 -459
- package/telegram-plugin/gateway/gateway.ts +260 -21
- package/telegram-plugin/gateway/resolve-person.ts +304 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
- package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
- package/telegram-plugin/operator-events.ts +21 -0
- package/telegram-plugin/pending-work-progress.ts +42 -7
- package/telegram-plugin/quota-bar-format.ts +360 -0
- package/telegram-plugin/subagent-watcher.ts +27 -9
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +21 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/operator-events.test.ts +16 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
- package/telegram-plugin/tests/quota-bar-format.test.ts +444 -0
- package/telegram-plugin/tests/resolve-person.test.ts +290 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +53 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +138 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +61 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +97 -0
- package/telegram-plugin/worker-activity-feed.ts +220 -15
- package/profiles/default/CLAUDE.md +0 -116
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
|
@@ -21959,7 +21959,8 @@ var CronConfigSchema = exports_external.object({
|
|
|
21959
21959
|
var UserSchema = exports_external.object({
|
|
21960
21960
|
name: exports_external.string().optional().describe("Display name for the user."),
|
|
21961
21961
|
telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
|
|
21962
|
-
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
21962
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
21963
|
+
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
21963
21964
|
});
|
|
21964
21965
|
var SwitchroomConfigSchema = exports_external.object({
|
|
21965
21966
|
switchroom: exports_external.object({
|
|
@@ -28736,7 +28737,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:f
|
|
|
28736
28737
|
import { dirname as dirname4, join as join6 } from "node:path";
|
|
28737
28738
|
|
|
28738
28739
|
// src/build-info.ts
|
|
28739
|
-
var VERSION = "0.18.
|
|
28740
|
+
var VERSION = "0.18.6";
|
|
28740
28741
|
|
|
28741
28742
|
// src/cli/resolve-version.ts
|
|
28742
28743
|
function readPackageVersion() {
|
|
@@ -4676,7 +4676,8 @@ var init_schema = __esm(() => {
|
|
|
4676
4676
|
UserSchema = exports_external.object({
|
|
4677
4677
|
name: exports_external.string().optional().describe("Display name for the user."),
|
|
4678
4678
|
telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
|
|
4679
|
-
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
4679
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
4680
|
+
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
4680
4681
|
});
|
|
4681
4682
|
SwitchroomConfigSchema = exports_external.object({
|
|
4682
4683
|
switchroom: exports_external.object({
|
|
@@ -4676,7 +4676,8 @@ var init_schema = __esm(() => {
|
|
|
4676
4676
|
UserSchema = exports_external.object({
|
|
4677
4677
|
name: exports_external.string().optional().describe("Display name for the user."),
|
|
4678
4678
|
telegram_ids: exports_external.array(exports_external.string()).min(1).describe("Telegram username(s) and/or numeric user id(s) identifying this user " + "(a leading @ is optional). Matched against the message sender for " + "per-speaker memory routing."),
|
|
4679
|
-
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`).")
|
|
4679
|
+
profile_bank: exports_external.string().describe("Hindsight bank holding this user's memory profile (author via " + "`switchroom memory profile add <bank> ...`)."),
|
|
4680
|
+
person_id: exports_external.string().optional().describe("Free-text display name projected into the inbound `<channel>` " + 'tag\'s `user` attribute (e.g. "Lisa") so an agent can greet by ' + "name instead of only seeing the raw Telegram id/username. NOT a " + "stable identity system — just a label. Resolution is boot-time-only " + "(no hot-reload; a config change needs an agent restart) and " + "chat-scoped (a resolved name is only shown in a chat/group the " + "person is actually a member of, per that chat's access.json " + "membership — never broadcast into every chat the agent operates " + "in). Keep this broadly safe to show, the same discipline as " + "picking a `profile_bank` name: there is no automated enforcement " + "that a `person_id` stays safe if a group's membership changes " + "later — see docs/configuration.md.")
|
|
4680
4681
|
});
|
|
4681
4682
|
SwitchroomConfigSchema = exports_external.object({
|
|
4682
4683
|
switchroom: exports_external.object({
|
|
@@ -19569,7 +19570,7 @@ function allocateAgentUid(name) {
|
|
|
19569
19570
|
var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
|
|
19570
19571
|
|
|
19571
19572
|
// src/vault/broker/server.ts
|
|
19572
|
-
import { dirname as
|
|
19573
|
+
import { dirname as dirname5, resolve as resolve8, basename as basename4 } from "node:path";
|
|
19573
19574
|
import * as os3 from "node:os";
|
|
19574
19575
|
import * as path3 from "node:path";
|
|
19575
19576
|
|
|
@@ -19957,10 +19958,38 @@ function rotateAuditLog(logPath, maxFiles) {
|
|
|
19957
19958
|
return;
|
|
19958
19959
|
}
|
|
19959
19960
|
}
|
|
19961
|
+
const snapshotPath = `${logPath}.1`;
|
|
19960
19962
|
try {
|
|
19961
|
-
fs.
|
|
19963
|
+
fs.copyFileSync(logPath, snapshotPath);
|
|
19962
19964
|
} catch (err) {
|
|
19963
|
-
process.stderr.write(`[vault-audit] ERROR: could not
|
|
19965
|
+
process.stderr.write(`[vault-audit] ERROR: could not snapshot active audit log ${logPath} → ${snapshotPath}: ${err.message}
|
|
19966
|
+
`);
|
|
19967
|
+
return;
|
|
19968
|
+
}
|
|
19969
|
+
try {
|
|
19970
|
+
const fd = fs.openSync(snapshotPath, "r");
|
|
19971
|
+
try {
|
|
19972
|
+
fs.fsyncSync(fd);
|
|
19973
|
+
} finally {
|
|
19974
|
+
fs.closeSync(fd);
|
|
19975
|
+
}
|
|
19976
|
+
} catch (err) {
|
|
19977
|
+
process.stderr.write(`[vault-audit] ERROR: could not fsync audit snapshot ${snapshotPath}; leaving active log intact to avoid data loss: ${err.message}
|
|
19978
|
+
`);
|
|
19979
|
+
return;
|
|
19980
|
+
}
|
|
19981
|
+
try {
|
|
19982
|
+
const dirFd = fs.openSync(path.dirname(snapshotPath), "r");
|
|
19983
|
+
try {
|
|
19984
|
+
fs.fsyncSync(dirFd);
|
|
19985
|
+
} finally {
|
|
19986
|
+
fs.closeSync(dirFd);
|
|
19987
|
+
}
|
|
19988
|
+
} catch {}
|
|
19989
|
+
try {
|
|
19990
|
+
fs.truncateSync(logPath, 0);
|
|
19991
|
+
} catch (err) {
|
|
19992
|
+
process.stderr.write(`[vault-audit] ERROR: could not truncate active audit log ${logPath}: ${err.message}
|
|
19964
19993
|
`);
|
|
19965
19994
|
}
|
|
19966
19995
|
}
|
|
@@ -22369,7 +22398,7 @@ class VaultBroker {
|
|
|
22369
22398
|
this.passphrase = this.testOpts._testPassphrase;
|
|
22370
22399
|
}
|
|
22371
22400
|
process.umask(63);
|
|
22372
|
-
const parentDir =
|
|
22401
|
+
const parentDir = dirname5(this.socketPath);
|
|
22373
22402
|
mkdirSync6(parentDir, { recursive: true, mode: 448 });
|
|
22374
22403
|
try {
|
|
22375
22404
|
chmodSync4(parentDir, 448);
|
|
@@ -23850,15 +23879,15 @@ class VaultBroker {
|
|
|
23850
23879
|
}
|
|
23851
23880
|
}
|
|
23852
23881
|
function detectVaultLayoutDrift(vaultPath) {
|
|
23853
|
-
const dir =
|
|
23882
|
+
const dir = dirname5(vaultPath);
|
|
23854
23883
|
if (basename4(dir) !== "vault")
|
|
23855
23884
|
return;
|
|
23856
23885
|
if (basename4(vaultPath) !== "vault.enc")
|
|
23857
23886
|
return;
|
|
23858
|
-
const switchroomDir =
|
|
23887
|
+
const switchroomDir = dirname5(dir);
|
|
23859
23888
|
if (basename4(switchroomDir) !== ".switchroom")
|
|
23860
23889
|
return;
|
|
23861
|
-
const home2 =
|
|
23890
|
+
const home2 = dirname5(switchroomDir);
|
|
23862
23891
|
const result = inspectVaultLayout(home2);
|
|
23863
23892
|
if (result.kind === "divergent") {
|
|
23864
23893
|
throw new VaultError(`Vault layout divergence detected at boot: ${result.details.oldPath} and ${result.details.newPath} are both regular files with different content. An older switchroom CLI may have written to the legacy path after migration ran. Run \`switchroom apply\` from the host to surface the recovery recipe (state E refusal with literal \`mv\` commands). See docs/operators/state-e-recovery.md.`);
|
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.
|
|
4
|
+
"version": "0.18.6",
|
|
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": {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: switchroom-release
|
|
3
|
+
description: "Cut and ship a switchroom release end-to-end: CHANGELOG consolidation, tag, npm publish, image build, and the fleet rollout gate. Use when the user says 'cut a release', 'ship a release', 'release vX', 'publish a new version', 'roll out the latest', or otherwise wants the merged work on main to go live for the fleet. This is the ONLY skill that authorizes a fleet rollout, and it enforces the npm-publish + image-build gates that were historically skipped (v0.18.4/v0.18.5 shipped to the fleet but never hit npm). Do NOT use for adding agents (switchroom-manage), diagnostics (switchroom-health), or a plain `switchroom update` on one agent (switchroom-cli)."
|
|
4
|
+
allowed-tools: Bash(git *) Bash(gh *) Bash(npm view *) Bash(npm pack *) Bash(docker manifest inspect *) Bash(docker buildx imagetools inspect *)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Switchroom release
|
|
8
|
+
|
|
9
|
+
Cut a release of `switchroom/switchroom` and get it live on the fleet. This is a **gated, ordered checklist** — do not skip steps, do not reorder. The whole point of this skill is that two steps that used to be skipped silently (npm publish, image-build verification) are now **hard gates before rollout**.
|
|
10
|
+
|
|
11
|
+
## What a release actually is
|
|
12
|
+
|
|
13
|
+
- A release = a `vX.Y.Z` **git tag** on `main` (the merge commit of the CHANGELOG PR).
|
|
14
|
+
- The tag is the version source of truth (`scripts/build.mjs:resolveVersion()`). `package.json` `version` is a **stale placeholder by design** — never bump it in a commit (the `//version` comment + #2733 discipline). The uncommitted pack-time bump happens in CI now, not by hand.
|
|
15
|
+
- Cutting the tag fires TWO workflows in parallel on tag push: `docker-images` (builds + pushes the 6 ghcr images) and `npm-publish` (builds, packs, verifies, publishes to npm). **Rollout is gated on BOTH going green.**
|
|
16
|
+
|
|
17
|
+
## Before you start — pre-flight (verify, don't assume)
|
|
18
|
+
|
|
19
|
+
1. `git fetch origin`, confirm `main` is at the commit you want released.
|
|
20
|
+
2. `gh pr list --state open` — confirm no PR meant for this release is still open. Ask the operator if unsure.
|
|
21
|
+
3. Confirm CI on `main` is green (`gh run list --branch main --limit 3`).
|
|
22
|
+
4. Read `CHANGELOG.md` — the `## Unreleased` section is the staging area for this release's notes. If it's empty, there's nothing to release.
|
|
23
|
+
5. Pick the next version: read the latest tag (`git tag --list 'v*' --sort=-v:refname | head -1`) and bump the patch (or minor if the operator asks). Confirm with the operator which.
|
|
24
|
+
|
|
25
|
+
## Step 1 — Consolidate the changelog (the release commit is CHANGELOG-only)
|
|
26
|
+
|
|
27
|
+
- Move the `## Unreleased` entries under a new `## vX.Y.Z — <one-line summary>` heading.
|
|
28
|
+
- The release commit touches **CHANGELOG.md only**. Do NOT bump `package.json` (placeholder discipline).
|
|
29
|
+
- Branch protection blocks direct push to `main`, so: create a `release/vX.Y.Z` branch, push it, open a `chore: release vX.Y.Z` PR (base `main`), arm auto-merge (squash, delete-branch) on green CI.
|
|
30
|
+
|
|
31
|
+
## Step 2 — Cut the tag (on the merge commit, not the PR branch)
|
|
32
|
+
|
|
33
|
+
Once the changelog PR is merged:
|
|
34
|
+
- `git fetch origin && git checkout main && git pull --ff-only`
|
|
35
|
+
- Tag the merge commit and create the GitHub Release:
|
|
36
|
+
- `gh release create vX.Y.Z -R switchroom/switchroom --target main --title 'vX.Y.Z — <summary>' --notes-file <notes-file>`
|
|
37
|
+
- **Notes extraction gotcha (historical):** the naive `awk '/^## vX/,/^## v/' CHANGELOG` range collapses to a single line. Use a start-flag awk: `awk 'f{print} /^## vX\.Y\.Z/{print; f=1} f && /^## v/ && !/^## vX\.Y\.Z/{exit}'` — or extract the section to a temp file by line range.
|
|
38
|
+
- **`gh release create` has been silently dropped in past runs.** After running it, verify: `gh release view vX.Y.Z` must return the release. If it didn't create, re-run.
|
|
39
|
+
|
|
40
|
+
## Step 3 — Wait for BOTH tag-push workflows (hard gates)
|
|
41
|
+
|
|
42
|
+
The tag push triggers `docker-images` AND `npm-publish` in parallel. **Do not proceed to rollout until both are green AND verified.**
|
|
43
|
+
|
|
44
|
+
### Gate A — npm publish (`npm-publish.yml`)
|
|
45
|
+
- `gh run list --workflow=npm-publish.yml --limit 1` — wait for it to reach `completed` / `success`.
|
|
46
|
+
- Verify the publish is live: `npm view switchroom version` must return `X.Y.Z` (not the old version). Retry a few times — npm registry propagation can lag a few seconds.
|
|
47
|
+
- If this workflow fails: **the release is not published.** Do NOT roll. Diagnose (NPM_TOKEN unset? npm 5xx? empty dist?). Re-run via `gh workflow run npm-publish.yml --ref vX.Y.Z` after fixing.
|
|
48
|
+
|
|
49
|
+
### Gate B — docker images (`docker-images.yml`)
|
|
50
|
+
- `gh run list --workflow=docker-images.yml --limit 1` — wait for `completed` / `success`.
|
|
51
|
+
- Verify all 6 images are published: `docker manifest inspect ghcr.io/switchroom/<image>:vX.Y.Z` for `agent`, `auth-broker`, `kernel`, `broker`, `web`, `hostd`. Each must resolve.
|
|
52
|
+
- If any image is missing: do NOT roll — the rollout canary version-assert fails on an unpublished tag. Wait + re-check.
|
|
53
|
+
|
|
54
|
+
**Only when Gate A AND Gate B are green + verified** do you proceed.
|
|
55
|
+
|
|
56
|
+
## Step 4 — Fleet rollout (operator-gated, canary-first)
|
|
57
|
+
|
|
58
|
+
- Fire the rollout via the hostd rollout path (`mcp__hostd__rollout`), which pops an **approval card**. Do NOT roll without the operator tapping approve.
|
|
59
|
+
- Canary discipline: roll the release-critical canary agent first (the test-harness agent, per CLAUDE.md > Release canary discipline), monitor its logs + a smoke check, then stagger the rest per-agent with a `--version` assertion each (guards the `:latest` pull-race).
|
|
60
|
+
- For a release that changes agent runtime behavior (a CLI pin bump, a behavioral template change), offer the operator a canary-first path (one agent → monitor → sweep) vs all-at-once; let them choose.
|
|
61
|
+
|
|
62
|
+
## What you must NOT do
|
|
63
|
+
|
|
64
|
+
- **Never bump `package.json` `version` in a commit.** It's a stale placeholder; the tag is the source of truth and `npm-publish.yml` does the uncommitted pack-time bump.
|
|
65
|
+
- **Never run `npm publish` by hand from the agent container.** You can't reach the operator's npm auth, and the workflow is the reliable path. If the workflow is broken, fix the workflow — don't side-step it.
|
|
66
|
+
- **Never roll the fleet before Gate A (npm) AND Gate B (images) are both green + verified.** A release that's on the fleet but not on npm is the exact regression this skill exists to prevent.
|
|
67
|
+
- **Never push directly to `main`.** The CHANGELOG PR goes through auto-merge on green.
|
|
68
|
+
- **Never force-push `main` or bypass hooks (`--no-verify`).**
|
|
69
|
+
|
|
70
|
+
## If something goes wrong
|
|
71
|
+
|
|
72
|
+
- **npm-publish failed but the tag is already pushed:** fix + `gh workflow run npm-publish.yml --ref vX.Y.Z`. Do NOT roll until it's green + `npm view` confirms.
|
|
73
|
+
- **Images failed but npm succeeded:** the npm package is live but the fleet can't roll yet. Fix the image workflow / re-run. (npm being ahead of images is fine — the CLI is published for npm consumers; the fleet waits on images.)
|
|
74
|
+
- **Rollout started before publish verified (the old bug):** abort the rollout, publish, then re-roll. Do not let a half-published release sit on the fleet.
|
|
75
|
+
|
|
76
|
+
## Operator one-time setup (tell them once, not every release)
|
|
77
|
+
|
|
78
|
+
The `npm-publish.yml` workflow needs an `NPM_TOKEN` repo secret (automation-scoped npm access token with publish rights on `switchroom`). Set it once in repo settings → Secrets and variables → Actions → `NPM_TOKEN`. Without it, Gate A fails loudly on the first release — that's the design (loud > silent).
|
|
@@ -273,6 +273,16 @@ export interface SnapshotRenderOpts {
|
|
|
273
273
|
* Takes precedence over `liveProbedAtMs`.
|
|
274
274
|
*/
|
|
275
275
|
staleCachedAtMs?: number;
|
|
276
|
+
/**
|
|
277
|
+
* True when the live probe returned no usable data for ANY account (probe
|
|
278
|
+
* threw / timed out / returned zero rows) AND nothing was served from
|
|
279
|
+
* cache either. The footer renders `⚠ probe failed — no live data`
|
|
280
|
+
* instead of a false bare `_Live_`. Subscription-honesty: a "Live" footer
|
|
281
|
+
* next to "no data" rows is the exact lie this closes. `staleCachedAtMs`
|
|
282
|
+
* (cache-served data) takes precedence over this; this takes precedence
|
|
283
|
+
* over the bare-`Live` fallback.
|
|
284
|
+
*/
|
|
285
|
+
probeFailed?: boolean;
|
|
276
286
|
/**
|
|
277
287
|
* Demo mode (the `/usage demo` / `/auth demo` suffix). When true, every
|
|
278
288
|
* account label is run through `maskEmail` before rendering so a screen
|
|
@@ -521,11 +531,15 @@ export function renderAuthSnapshotFormat2(
|
|
|
521
531
|
lines.push(`_${recommendation(snapshots, now, opts.demo ?? false)}_`);
|
|
522
532
|
// #2495 Change 2 — a failed probe-on-open renders an explicit "cached Nm
|
|
523
533
|
// ago" warning, never a false live stamp. The degraded variant takes
|
|
524
|
-
// precedence over the live stamp.
|
|
534
|
+
// precedence over the live stamp. A TOTAL probe failure (no rows, no
|
|
535
|
+
// cache) renders an explicit "probe failed" marker — without it the
|
|
536
|
+
// bare-else rendered `_Live_` next to no-data rows (the honesty gap).
|
|
525
537
|
if (opts.staleCachedAtMs != null) {
|
|
526
538
|
lines.push(`_⚠ cached ${formatAgeStamp(opts.staleCachedAtMs, now)}_`);
|
|
527
539
|
} else if (opts.liveProbedAtMs != null) {
|
|
528
540
|
lines.push(`_Live · refreshed ${formatAgeStamp(opts.liveProbedAtMs, now)}_`);
|
|
541
|
+
} else if (opts.probeFailed) {
|
|
542
|
+
lines.push('_⚠ probe failed — no live data_');
|
|
529
543
|
} else {
|
|
530
544
|
lines.push('_Live_');
|
|
531
545
|
}
|