switchroom 0.18.9 → 0.18.11
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 +1 -0
- package/dist/auth-broker/index.js +198 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +214 -83
- package/dist/cli/ui/apple-touch-icon.png +0 -0
- package/dist/cli/ui/favicon-32.png +0 -0
- package/dist/cli/ui/favicon.ico +0 -0
- package/dist/host-control/main.js +760 -257
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +3 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +119 -37
- package/profiles/_shared/dev-protocol.md.hbs +42 -0
- package/skills/dev-protocol/SKILL.md +131 -0
- package/telegram-plugin/README.md +2 -1
- package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
- package/telegram-plugin/admin-commands/index.ts +6 -1
- package/telegram-plugin/bridge/bridge.ts +23 -1
- package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
- package/telegram-plugin/chat-lock.ts +13 -0
- package/telegram-plugin/dist/bridge/bridge.js +24 -1
- package/telegram-plugin/dist/gateway/gateway.js +1832 -263
- package/telegram-plugin/dist/server.js +29 -2
- package/telegram-plugin/fallback-card-collapse.ts +131 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
- package/telegram-plugin/gateway/effort-command.ts +47 -3
- package/telegram-plugin/gateway/gateway.ts +1435 -211
- package/telegram-plugin/gateway/model-command.ts +94 -8
- package/telegram-plugin/gateway/pending-session-command.ts +365 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
- package/telegram-plugin/gateway/session-model-file.ts +166 -23
- package/telegram-plugin/gateway/stop-command.ts +56 -0
- package/telegram-plugin/photo-precheck.ts +201 -0
- package/telegram-plugin/quota-watch.ts +141 -2
- package/telegram-plugin/registry/subagents-schema.ts +26 -3
- package/telegram-plugin/registry/subagents.test.ts +67 -0
- package/telegram-plugin/retry-api-call.ts +31 -0
- package/telegram-plugin/subagent-watcher.ts +392 -1
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
- package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
- package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
- package/telegram-plugin/tests/effort-command.test.ts +59 -2
- package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
- package/telegram-plugin/tests/model-command.test.ts +46 -3
- package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
- package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
- package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
- package/telegram-plugin/tests/quota-watch.test.ts +225 -0
- package/telegram-plugin/tests/session-model-file.test.ts +101 -2
- package/telegram-plugin/tests/stop-command.test.ts +234 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
- package/telegram-plugin/welcome-text.ts +4 -3
- package/telegram-plugin/worker-activity-feed.ts +27 -0
|
@@ -4703,6 +4703,7 @@ var init_schema = __esm(() => {
|
|
|
4703
4703
|
uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root)."),
|
|
4704
4704
|
mirror_dir: exports_external.string().optional().describe("Optional host-side directory path. When set, the broker actively " + "writes the consumer's effective-account `.credentials.json` mirror " + "here — in addition to serving creds on demand via `get-credentials`. " + "Use this to eliminate the pull-latency gap: without a mirror the " + "consumer only gets failover creds at its next scheduled re-fetch " + "(up to 30 min). With a mirror the broker pushes failover creds " + "immediately when it detects exhaustion (consumer-quota-sensor tick, " + "or a mark-exhausted RPC on the pinned account). The directory must " + "be accessible to the broker container (bind-mounted from the host) " + "and to the consumer container; the broker writes " + "`<mirror_dir>/.credentials.json` atomically. Chown is attempted to " + "`uid` (default 0) — swallowed when CAP_CHOWN is absent.")
|
|
4705
4705
|
})).optional().describe("Non-agent peers that hold a broker socket (RFC H §4.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation."),
|
|
4706
|
+
proactive_failover_pct: exports_external.number().min(1).max(99).optional().describe("Soft-avoid threshold (percent) for proactive auth failover (#3031). " + "When set (e.g. 95), an account whose fresh 7-day utilization is at/" + "above this value — or whose 5h utilization is at/above min(pct+3, " + "98) — enters the SOFT-AVOID preference tier: the broker prefers a " + "fully-eligible fallback account on the serving path, without ever " + "blocking the account (the hard wall stays 99.5). Enter/exit uses " + "hysteresis (exit below pct-5 or on window reset) so the preference " + "does not flap between probe ticks. Accounts serving past the wall " + "via `allow_overage_accounts` are never soft-avoided. UNSET (the " + "default) disables the tier entirely — behavior is identical to a " + "fleet without this field."),
|
|
4706
4707
|
allow_overage_accounts: exports_external.array(exports_external.string().min(1)).optional().describe("Opt-in list of account labels (bare strings matching `auth.active` / " + "`auth.fallback_order` entries) that may be served PAST the weekly " + "utilization wall when Anthropic overage billing is available for the " + "account (`overageStatus === 'allowed'`). Overage is REAL MONEY — " + "default is empty (no account gets this). An account in this list is " + "only kept eligible when its fresh quota snapshot reports " + "`overageStatus: 'allowed'` AND `overageDisabledReason` is NOT " + "'out_of_credits' (i.e. the overage credit has not been exhausted). " + "As soon as `overageDisabledReason` becomes 'out_of_credits', the " + "account is blocked immediately regardless of this flag. Overage lifts " + "ONLY the utilization wall — it cannot lift an active exhaustion mark " + "written by a real 429 (`mark-exhausted`).")
|
|
4707
4708
|
}).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
|
|
4708
4709
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
@@ -18859,6 +18860,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
18859
18860
|
init_loader();
|
|
18860
18861
|
|
|
18861
18862
|
// src/cli/agent-config.ts
|
|
18863
|
+
init_loader();
|
|
18862
18864
|
init_merge();
|
|
18863
18865
|
|
|
18864
18866
|
// src/vault/broker/acl.ts
|
|
@@ -18886,7 +18888,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
18886
18888
|
return typeof value === "number" && Number.isFinite(value);
|
|
18887
18889
|
});
|
|
18888
18890
|
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
18889
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
|
|
18891
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
18890
18892
|
for (const name of SHARED_FRAGMENTS) {
|
|
18891
18893
|
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
18892
18894
|
if (existsSync4(fragPath)) {
|
|
@@ -4703,6 +4703,7 @@ var init_schema = __esm(() => {
|
|
|
4703
4703
|
uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root)."),
|
|
4704
4704
|
mirror_dir: exports_external.string().optional().describe("Optional host-side directory path. When set, the broker actively " + "writes the consumer's effective-account `.credentials.json` mirror " + "here — in addition to serving creds on demand via `get-credentials`. " + "Use this to eliminate the pull-latency gap: without a mirror the " + "consumer only gets failover creds at its next scheduled re-fetch " + "(up to 30 min). With a mirror the broker pushes failover creds " + "immediately when it detects exhaustion (consumer-quota-sensor tick, " + "or a mark-exhausted RPC on the pinned account). The directory must " + "be accessible to the broker container (bind-mounted from the host) " + "and to the consumer container; the broker writes " + "`<mirror_dir>/.credentials.json` atomically. Chown is attempted to " + "`uid` (default 0) — swallowed when CAP_CHOWN is absent.")
|
|
4705
4705
|
})).optional().describe("Non-agent peers that hold a broker socket (RFC H §4.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation."),
|
|
4706
|
+
proactive_failover_pct: exports_external.number().min(1).max(99).optional().describe("Soft-avoid threshold (percent) for proactive auth failover (#3031). " + "When set (e.g. 95), an account whose fresh 7-day utilization is at/" + "above this value — or whose 5h utilization is at/above min(pct+3, " + "98) — enters the SOFT-AVOID preference tier: the broker prefers a " + "fully-eligible fallback account on the serving path, without ever " + "blocking the account (the hard wall stays 99.5). Enter/exit uses " + "hysteresis (exit below pct-5 or on window reset) so the preference " + "does not flap between probe ticks. Accounts serving past the wall " + "via `allow_overage_accounts` are never soft-avoided. UNSET (the " + "default) disables the tier entirely — behavior is identical to a " + "fleet without this field."),
|
|
4706
4707
|
allow_overage_accounts: exports_external.array(exports_external.string().min(1)).optional().describe("Opt-in list of account labels (bare strings matching `auth.active` / " + "`auth.fallback_order` entries) that may be served PAST the weekly " + "utilization wall when Anthropic overage billing is available for the " + "account (`overageStatus === 'allowed'`). Overage is REAL MONEY — " + "default is empty (no account gets this). An account in this list is " + "only kept eligible when its fresh quota snapshot reports " + "`overageStatus: 'allowed'` AND `overageDisabledReason` is NOT " + "'out_of_credits' (i.e. the overage credit has not been exhausted). " + "As soon as `overageDisabledReason` becomes 'out_of_credits', the " + "account is blocked immediately regardless of this flag. Overage lifts " + "ONLY the utilization wall — it cannot lift an active exhaustion mark " + "written by a real 429 (`mark-exhausted`).")
|
|
4707
4708
|
}).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
|
|
4708
4709
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
@@ -18095,6 +18096,7 @@ import { homedir as homedir2 } from "node:os";
|
|
|
18095
18096
|
init_loader();
|
|
18096
18097
|
|
|
18097
18098
|
// src/cli/agent-config.ts
|
|
18099
|
+
init_loader();
|
|
18098
18100
|
init_merge();
|
|
18099
18101
|
|
|
18100
18102
|
// src/config/google-workspace-acl.ts
|
|
@@ -18346,7 +18348,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
18346
18348
|
return typeof value === "number" && Number.isFinite(value);
|
|
18347
18349
|
});
|
|
18348
18350
|
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
18349
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
|
|
18351
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
18350
18352
|
for (const name of SHARED_FRAGMENTS) {
|
|
18351
18353
|
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
18352
18354
|
if (existsSync4(fragPath)) {
|
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.11",
|
|
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": {
|
|
@@ -1067,21 +1067,21 @@ fi
|
|
|
1067
1067
|
|
|
1068
1068
|
# --- Session model resolution (durable .session-model + .relaunch-model-intent) ---
|
|
1069
1069
|
#
|
|
1070
|
-
# Contract: reference/rfcs/session-model-stickiness.md. A
|
|
1071
|
-
# confirmed `/model X` switch persists as
|
|
1072
|
-
# (one-line JSON written by the gateway)
|
|
1073
|
-
#
|
|
1074
|
-
#
|
|
1070
|
+
# Contract: reference/rfcs/session-model-stickiness.md (#3039 revision). A
|
|
1071
|
+
# positively-confirmed `/model X` switch persists as
|
|
1072
|
+
# `{{agentDir}}/.session-model` (one-line JSON written by the gateway) and is
|
|
1073
|
+
# honored on EVERY boot — deploy, watchdog bounce, raw `docker restart`,
|
|
1074
|
+
# host reboot, crash. It is cleared only by:
|
|
1075
1075
|
#
|
|
1076
|
-
#
|
|
1077
|
-
#
|
|
1078
|
-
#
|
|
1079
|
-
#
|
|
1076
|
+
# - explicit user action (`/model default` deletes it live; a fresh
|
|
1077
|
+
# (<10 min) explicit "revert" `.relaunch-model-intent` reverts at boot)
|
|
1078
|
+
# - invalidation: corrupt/malformed file, or the configured yaml `model:`
|
|
1079
|
+
# changed since the switch
|
|
1080
1080
|
#
|
|
1081
|
-
#
|
|
1082
|
-
#
|
|
1083
|
-
#
|
|
1084
|
-
#
|
|
1081
|
+
# Every clearing path writes `.session-model-alert`, which the gateway
|
|
1082
|
+
# relays to the operator chat once at boot — an override is never dropped
|
|
1083
|
+
# silently. Default (no intent / stale / corrupt intent) is KEEP (operator
|
|
1084
|
+
# decision 2026-07-11, superseding the earlier revert-by-default).
|
|
1085
1085
|
#
|
|
1086
1086
|
# NB {{{modelQ}}} is already shell-single-quoted by the scaffold (it renders as
|
|
1087
1087
|
# a quoted token, e.g. 'claude-sonnet-5'), so it is assigned BARE here — never
|
|
@@ -1117,14 +1117,18 @@ fi
|
|
|
1117
1117
|
# Freshness clock is the EMBEDDED ts (ms), never file mtime — same clock the
|
|
1118
1118
|
# gateway writes with.
|
|
1119
1119
|
_sm_reason=""
|
|
1120
|
+
_sm_revert=""
|
|
1120
1121
|
if [ -f "{{agentDir}}/.relaunch-model-intent" ]; then
|
|
1121
1122
|
_int_raw="$(cat "{{agentDir}}/.relaunch-model-intent" 2>/dev/null || true)"
|
|
1122
1123
|
rm -f "{{agentDir}}/.relaunch-model-intent"
|
|
1123
1124
|
_int="$(printf '%s' "$_int_raw" | sed -n 's/.*"intent"[[:space:]]*:[[:space:]]*"\([a-z]*\)".*/\1/p')"
|
|
1124
1125
|
_int_ts="$(printf '%s' "$_int_raw" | sed -n 's/.*"ts"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p')"
|
|
1125
1126
|
_sm_reason="$(printf '%s' "$_int_raw" | sed -n 's/.*"reason"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1126
|
-
|
|
1127
|
-
|
|
1127
|
+
# #3039: boot default is KEEP. Only a FRESH explicit "revert" intent
|
|
1128
|
+
# (stamped by an explicit user/gateway revert path) clears the override.
|
|
1129
|
+
# A stale or corrupt intent counts as no intent → keep.
|
|
1130
|
+
if [ "$_int" = "revert" ] && [ -n "$_int_ts" ] && [ $(( $(date +%s) * 1000 - _int_ts )) -lt 600000 ]; then
|
|
1131
|
+
_sm_revert="1"
|
|
1128
1132
|
fi
|
|
1129
1133
|
unset _int_raw _int _int_ts
|
|
1130
1134
|
fi
|
|
@@ -1134,13 +1138,13 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1134
1138
|
_sm_model="$(printf '%s' "$_smf" | sed -n 's/.*"model"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1135
1139
|
_sm_cfg="$(printf '%s' "$_smf" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1136
1140
|
_sm_ts="$(printf '%s' "$_smf" | sed -n 's/.*"ts"[[:space:]]*:[[:space:]]*\([0-9]\{1,\}\).*/\1/p')"
|
|
1137
|
-
if [ -
|
|
1138
|
-
#
|
|
1139
|
-
#
|
|
1140
|
-
rm -f "{{agentDir}}/.session-model"
|
|
1141
|
-
_sm_why="${_sm_reason:-
|
|
1141
|
+
if [ -n "$_sm_revert" ]; then
|
|
1142
|
+
# Explicit fresh revert intent — the ONLY boot path that clears a valid
|
|
1143
|
+
# override by design (#3039). Everything else keeps.
|
|
1144
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-kept-notified"
|
|
1145
|
+
_sm_why="${_sm_reason:-explicit revert intent}"
|
|
1142
1146
|
echo "session-model: reverting to configured default '$_EFFECTIVE_MODEL' — $_sm_why (session override '$_sm_model' cleared)" >&2
|
|
1143
|
-
printf 'Session model override `%s` was cleared — this relaunch
|
|
1147
|
+
printf 'Session model override `%s` was cleared as requested (%s) — this relaunch booted the configured default `%s`. Re-issue /model %s to switch back.\n' "$_sm_model" "$_sm_why" "$_EFFECTIVE_MODEL" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1144
1148
|
unset _sm_why
|
|
1145
1149
|
# Shape gate — kept BYTE-IDENTICAL with MODEL_ARG_RE in
|
|
1146
1150
|
# telegram-plugin/gateway/model-command.ts. `/` is allowed for
|
|
@@ -1151,20 +1155,18 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1151
1155
|
# passes if ANY line matches — a multiline value must never reach
|
|
1152
1156
|
# `claude --model` (parity with parseSessionModel's single-string check).
|
|
1153
1157
|
elif [ "$(printf '%s' "$_sm_model" | wc -c)" -eq 0 ] || [ "$(printf '%s' "$_sm_model" | wc -l)" -ne 0 ] || ! printf '%s' "$_sm_model" | grep -Eq '^[A-Za-z0-9][]A-Za-z0-9._[/-]{0,99}$'; then
|
|
1154
|
-
|
|
1158
|
+
# Invalid carrier (#3039): fall back to the configured default AND tell
|
|
1159
|
+
# the operator once — never a silent stderr-only drop.
|
|
1160
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-kept-notified"
|
|
1155
1161
|
echo "session-model: ignoring malformed .session-model (failed shape gate) — using configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1162
|
+
printf 'Your saved session model override could not be read (invalid or corrupt), so the agent booted on its configured default `%s`. Re-issue /model <name> if you want a different model.\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1156
1163
|
elif [ "$_sm_cfg" != "$_EFFECTIVE_MODEL" ]; then
|
|
1157
1164
|
# switchroom.yaml `model:` changed since the switch → the override is
|
|
1158
|
-
# against a default that no longer exists
|
|
1159
|
-
|
|
1165
|
+
# against a default that no longer exists (it may name a retired model).
|
|
1166
|
+
# Invalidate + announce once (#3039 invalid-carrier fallback).
|
|
1167
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-kept-notified"
|
|
1160
1168
|
echo "session-model: configured default changed ('$_sm_cfg' → '$_EFFECTIVE_MODEL') — clearing session override '$_sm_model'" >&2
|
|
1161
1169
|
printf 'The configured default model changed (`%s` → `%s`), so your session override to `%s` was cleared — the agent booted on the new configured default. Re-issue /model %s if you still want it.\n' "$_sm_cfg" "$_EFFECTIVE_MODEL" "$_sm_model" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1162
|
-
elif [ -n "$_sm_ts" ] && [ $(( $(date +%s) * 1000 - _sm_ts )) -gt 604800000 ]; then
|
|
1163
|
-
# 7-day staleness bound (belt-and-braces vs version-rollback resurrection
|
|
1164
|
-
# and long-forgotten overrides).
|
|
1165
|
-
rm -f "{{agentDir}}/.session-model"
|
|
1166
|
-
echo "session-model: session override '$_sm_model' is older than 7 days — expiring it, using configured default '$_EFFECTIVE_MODEL'" >&2
|
|
1167
|
-
printf 'Session model override `%s` expired (older than 7 days) — the agent booted on its configured default `%s`. Re-issue /model %s if you still want it.\n' "$_sm_model" "$_EFFECTIVE_MODEL" "$_sm_model" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1168
1170
|
else
|
|
1169
1171
|
case "$_sm_model" in
|
|
1170
1172
|
sr-*)
|
|
@@ -1183,13 +1185,93 @@ if [ -f "{{agentDir}}/.session-model" ]; then
|
|
|
1183
1185
|
;;
|
|
1184
1186
|
esac
|
|
1185
1187
|
if [ "$_EFFECTIVE_MODEL" = "$_sm_model" ]; then
|
|
1186
|
-
echo "session-model: keeping session override '$_sm_model' across this
|
|
1187
|
-
|
|
1188
|
+
echo "session-model: keeping session override '$_sm_model' across this relaunch${_sm_reason:+ ($_sm_reason)}" >&2
|
|
1189
|
+
# #3042 item 4: notify the chat ONCE per kept value — a watchdog bounce
|
|
1190
|
+
# loop must not storm the operator with identical "kept" alerts. The
|
|
1191
|
+
# sentinel is cleared on every path that clears the override.
|
|
1192
|
+
if [ "$(cat "{{agentDir}}/.session-model-kept-notified" 2>/dev/null || true)" != "$_sm_model" ]; then
|
|
1193
|
+
printf 'Session model override `%s` kept across this relaunch%s. It persists across restarts and deploys; /model default clears it.\n' "$_sm_model" "${_sm_reason:+ ($_sm_reason)}" > "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1194
|
+
printf '%s\n' "$_sm_model" > "{{agentDir}}/.session-model-kept-notified" 2>/dev/null || true
|
|
1195
|
+
fi
|
|
1188
1196
|
fi
|
|
1189
1197
|
fi
|
|
1190
1198
|
unset _smf _sm_model _sm_cfg _sm_ts
|
|
1191
1199
|
fi
|
|
1192
|
-
unset _sm_keep _sm_reason
|
|
1200
|
+
unset _sm_keep _sm_reason _sm_revert
|
|
1201
|
+
|
|
1202
|
+
# --- Override crashloop self-heal (#3042 review blocker 2b) ---
|
|
1203
|
+
#
|
|
1204
|
+
# Under keep-by-default an invalid-but-shape-valid persisted model (a typo
|
|
1205
|
+
# persisted at shutdown, or a confirmed model later retired) would make every
|
|
1206
|
+
# boot exec `claude --model <bad>` and die with the gateway dead — recovery
|
|
1207
|
+
# would need a host shell. Detect the loop from inside: when an override is
|
|
1208
|
+
# ACTIVE, stamp a boot-attempt counter just before exec; a healthy run makes
|
|
1209
|
+
# the next boot arrive much later (stamp stale → counter resets), while a
|
|
1210
|
+
# fast crashloop re-enters here within seconds (docker backoff caps ≈1 min).
|
|
1211
|
+
# Three consecutive fast boots with the same override → clear the carrier,
|
|
1212
|
+
# boot the configured default, and alert the chat once.
|
|
1213
|
+
_SM_CFG_RECORDED="$(cat "{{agentDir}}/.configured-default-model" 2>/dev/null | tr -d '[:space:]' || true)"
|
|
1214
|
+
if [ -n "$_SM_CFG_RECORDED" ] && [ "$_EFFECTIVE_MODEL" != "$_SM_CFG_RECORDED" ] && [ -f "{{agentDir}}/.session-model" ]; then
|
|
1215
|
+
_bl_now="$(date +%s)"
|
|
1216
|
+
_bl_cnt=0
|
|
1217
|
+
_bl_prev=0
|
|
1218
|
+
if [ -f "{{agentDir}}/.session-model-boot-attempts" ]; then
|
|
1219
|
+
read -r _bl_cnt _bl_prev < "{{agentDir}}/.session-model-boot-attempts" 2>/dev/null || true
|
|
1220
|
+
fi
|
|
1221
|
+
case "$_bl_cnt" in (''|*[!0-9]*) _bl_cnt=0;; esac
|
|
1222
|
+
case "$_bl_prev" in (''|*[!0-9]*) _bl_prev=0;; esac
|
|
1223
|
+
if [ $(( _bl_now - _bl_prev )) -lt 150 ]; then _bl_cnt=$(( _bl_cnt + 1 )); else _bl_cnt=1; fi
|
|
1224
|
+
if [ "$_bl_cnt" -ge 3 ]; then
|
|
1225
|
+
echo "session-model: override '$_EFFECTIVE_MODEL' appears to be crashlooping the session ($_bl_cnt fast boots) — clearing it, booting configured default '$_SM_CFG_RECORDED'" >&2
|
|
1226
|
+
printf 'Session model override `%s` was cleared automatically: the agent failed to stay up %s boots in a row with it active (the model may be invalid or retired). Booting the configured default `%s`. Re-issue /model <name> if you want a different model.\n' "$_EFFECTIVE_MODEL" "$_bl_cnt" "$_SM_CFG_RECORDED" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1227
|
+
rm -f "{{agentDir}}/.session-model" "{{agentDir}}/.session-model-boot-attempts" "{{agentDir}}/.session-model-kept-notified"
|
|
1228
|
+
_EFFECTIVE_MODEL="$_SM_CFG_RECORDED"
|
|
1229
|
+
else
|
|
1230
|
+
printf '%s %s\n' "$_bl_cnt" "$_bl_now" > "{{agentDir}}/.session-model-boot-attempts" 2>/dev/null || true
|
|
1231
|
+
fi
|
|
1232
|
+
unset _bl_now _bl_cnt _bl_prev
|
|
1233
|
+
else
|
|
1234
|
+
# No active override this boot — a stale counter must not bite a future one.
|
|
1235
|
+
rm -f "{{agentDir}}/.session-model-boot-attempts"
|
|
1236
|
+
fi
|
|
1237
|
+
unset _SM_CFG_RECORDED
|
|
1238
|
+
|
|
1239
|
+
# --- Session effort resolution (durable .session-effort, #3039) ---
|
|
1240
|
+
#
|
|
1241
|
+
# The `/effort` sibling of the block above. A positively-confirmed `/effort`
|
|
1242
|
+
# apply persists `{{agentDir}}/.session-effort` (one-line JSON
|
|
1243
|
+
# {"level","configuredDefaultAtWrite","ts"} written by the gateway). It is
|
|
1244
|
+
# honored on every boot and cleared only by `/effort default` (live delete)
|
|
1245
|
+
# or invalidation here (corrupt file / configured `thinking_effort:` changed)
|
|
1246
|
+
# — each invalidation appends to `.session-model-alert` so the gateway's
|
|
1247
|
+
# boot relay tells the operator once.
|
|
1248
|
+
_EFFECTIVE_EFFORT={{#if thinkingEffort}}'{{thinkingEffort}}'{{else}}''{{/if}}
|
|
1249
|
+
if [ -f "{{agentDir}}/.session-effort" ]; then
|
|
1250
|
+
_sef="$(cat "{{agentDir}}/.session-effort" 2>/dev/null || true)"
|
|
1251
|
+
_se_level="$(printf '%s' "$_sef" | sed -n 's/.*"level"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1252
|
+
_se_cfg="$(printf '%s' "$_sef" | sed -n 's/.*"configuredDefaultAtWrite"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')"
|
|
1253
|
+
# Allowlist gate — kept in sync with EFFORT_LEVELS in
|
|
1254
|
+
# telegram-plugin/gateway/effort-command.ts. The level is passed verbatim
|
|
1255
|
+
# to `claude --effort`.
|
|
1256
|
+
if [ "$(printf '%s' "$_se_level" | wc -l)" -ne 0 ] || ! printf '%s' "$_se_level" | grep -Eq '^(low|medium|high|xhigh|max)$'; then
|
|
1257
|
+
rm -f "{{agentDir}}/.session-effort"
|
|
1258
|
+
echo "session-effort: ignoring malformed .session-effort (failed allowlist gate) — using configured default '${_EFFECTIVE_EFFORT:-<unset>}'" >&2
|
|
1259
|
+
printf 'Your saved session effort override could not be read (invalid or corrupt), so the agent booted on its configured default effort. Re-issue /effort <level> if you want a different one.\n' >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1260
|
+
elif [ "$_se_cfg" != "$_EFFECTIVE_EFFORT" ]; then
|
|
1261
|
+
rm -f "{{agentDir}}/.session-effort"
|
|
1262
|
+
echo "session-effort: configured thinking_effort changed ('$_se_cfg' → '${_EFFECTIVE_EFFORT:-<unset>}') — clearing session override '$_se_level'" >&2
|
|
1263
|
+
printf 'The configured default effort changed (`%s` → `%s`), so your session effort override `%s` was cleared — the agent booted on the new configured default. Re-issue /effort %s if you still want it.\n' "${_se_cfg:-<unset>}" "${_EFFECTIVE_EFFORT:-<unset>}" "$_se_level" "$_se_level" >> "{{agentDir}}/.session-model-alert" 2>/dev/null || true
|
|
1264
|
+
else
|
|
1265
|
+
_EFFECTIVE_EFFORT="$_se_level"
|
|
1266
|
+
echo "session-effort: keeping session effort override '$_se_level' across this relaunch" >&2
|
|
1267
|
+
fi
|
|
1268
|
+
unset _sef _se_level _se_cfg
|
|
1269
|
+
fi
|
|
1270
|
+
if [ -n "$_EFFECTIVE_EFFORT" ]; then
|
|
1271
|
+
_EFFORT_ARG="--effort $_EFFECTIVE_EFFORT"
|
|
1272
|
+
else
|
|
1273
|
+
_EFFORT_ARG=""
|
|
1274
|
+
fi
|
|
1193
1275
|
|
|
1194
1276
|
# sr-* passthrough→router repoint — ONE post-resolution gate covering BOTH the
|
|
1195
1277
|
# /model override path AND the configured-default path (`model: sr-*` in
|
|
@@ -1222,14 +1304,14 @@ printf '%s\n' "$_EFFECTIVE_MODEL" > "{{agentDir}}/.active-session-model" 2>/dev/
|
|
|
1222
1304
|
|
|
1223
1305
|
{{#if useSwitchroomPlugin}}
|
|
1224
1306
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
1225
|
-
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL"
|
|
1307
|
+
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL" $_EFFORT_ARG{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
1226
1308
|
else
|
|
1227
|
-
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL"
|
|
1309
|
+
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL" $_EFFORT_ARG{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
1228
1310
|
fi
|
|
1229
1311
|
{{else}}
|
|
1230
1312
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
1231
|
-
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL"
|
|
1313
|
+
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL" $_EFFORT_ARG{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
1232
1314
|
else
|
|
1233
|
-
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL"
|
|
1315
|
+
exec claude $CONTINUE_FLAG --channels plugin:telegram@claude-plugins-official --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL" $_EFFORT_ARG{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}}{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
1234
1316
|
fi
|
|
1235
1317
|
{{/if}}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
## Development Protocol
|
|
2
|
+
|
|
3
|
+
How development work gets done here — orient, clarify, align, ship, communicate. These are procedural rules for any substantive coding, infra, or debugging task. For the long-form playbook (design reports, adversarial review structure, re-review verdicts), load the bundled `dev-protocol` skill before starting substantive development work.
|
|
4
|
+
|
|
5
|
+
### Orient — ground before you build
|
|
6
|
+
|
|
7
|
+
- **Validate, don't assume.** Read the actual code, config, and system state before forming a theory. Never assert a fact you haven't checked this turn.
|
|
8
|
+
- **Root-cause in real source.** Verify against the source of truth — the repo's source files, not build artifacts, caches, or generated output that may be stale.
|
|
9
|
+
- **Contradicting evidence wins.** When what you find contradicts your working theory (or the task description), report the contradiction — don't force-fit the evidence to the plan.
|
|
10
|
+
- **Cite your ground.** Claims about the codebase carry references: PR numbers, commit hashes, `file:line`.
|
|
11
|
+
|
|
12
|
+
### Clarify vs proceed
|
|
13
|
+
|
|
14
|
+
- **Infer first.** Mine the codebase, history, and prior context before asking. Most "questions" are answerable by reading.
|
|
15
|
+
- **If genuinely unsure, ask ONE question at a time** — the single question that unblocks the most work — not a questionnaire.
|
|
16
|
+
- **Clarify during planning; act autonomously during execution.** Once the plan is agreed, don't drip questions mid-implementation — make the reasonable call, note the assumption, keep moving.
|
|
17
|
+
|
|
18
|
+
### Design-align on larger tasks
|
|
19
|
+
|
|
20
|
+
For tasks that are architecturally significant, cross-cutting, or ambiguous in approach:
|
|
21
|
+
|
|
22
|
+
- **Design report before implementation.** Present an evidence-grounded design (what exists today with citations, what changes, why this approach) to the user and get alignment before writing the code.
|
|
23
|
+
- **Red-team your own plan.** Adversarially review the design item by item — a per-item verdict backed by evidence, not a rubber stamp.
|
|
24
|
+
- **Stage delivery.** Ship as focused, single-concern PRs rather than one omnibus change.
|
|
25
|
+
|
|
26
|
+
### Pipeline — how a change ships
|
|
27
|
+
|
|
28
|
+
- **Branch off fresh main.** Always pull before branching.
|
|
29
|
+
- **Scoped tests + lint locally; CI is the full-suite authority.** Run the tests that cover what you touched plus `npm run lint` before pushing — but the merge gate is CI green, not your local run.
|
|
30
|
+
- **Adversarial review of the diff.** Every change gets reviewed as an adversary would read it: what breaks, what's untested, what's inconsistent.
|
|
31
|
+
- **Fix ALL findings — including lows.** A "low" you skip is a bug you shipped. Then **re-review the fix** before calling it done.
|
|
32
|
+
- **Merge only on CI green.** No exceptions, no "it's probably fine".
|
|
33
|
+
- **Durable fixes over hack patches.** Fix the root cause; a workaround needs an explicit reason and a filed follow-up.
|
|
34
|
+
- **Deterministic mechanisms over model-dependent behavior.** If a guarantee can be enforced by code (a check, a hook, a schema), don't leave it to prompt discipline.
|
|
35
|
+
- **Tests assert outcomes, not just code paths.** A test that exercises the code but wouldn't fail on the bug is not a test.
|
|
36
|
+
|
|
37
|
+
### Communicate while you work
|
|
38
|
+
|
|
39
|
+
- **Consolidated messages.** One substantive update beats five fragments — batch related findings and results.
|
|
40
|
+
- **Always-visible progress.** Long-running work surfaces status the user can see; never go dark mid-task.
|
|
41
|
+
- **No foreground watches over 30 seconds.** Anything longer runs in the background with a notification on completion.
|
|
42
|
+
- **Max 15 parallel sub-agents.** Fan out for genuinely parallel work, but cap the swarm.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dev-protocol
|
|
3
|
+
description: >
|
|
4
|
+
Use when starting substantive development work — a code change, refactor,
|
|
5
|
+
bug fix, infra change, or multi-step debugging task in any repo. Load BEFORE
|
|
6
|
+
writing code: it is the fleet development protocol (orient/ground, clarify
|
|
7
|
+
vs proceed, design-align on larger tasks, the branch→test→review→CI
|
|
8
|
+
pipeline, communication rules). Also use when deciding whether a task needs
|
|
9
|
+
a design report first, when dispatching an adversarial review of a diff, or
|
|
10
|
+
when writing a re-review verdict. Do NOT use for: quick lookups, answering
|
|
11
|
+
questions about code without changing it, or pure conversation.
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Development Protocol — the playbook
|
|
15
|
+
|
|
16
|
+
The always-loaded CLAUDE.md "Development Protocol" section is the summary.
|
|
17
|
+
This skill is the long-form procedure. Work through the five phases in order;
|
|
18
|
+
they are checkpoints, not vibes.
|
|
19
|
+
|
|
20
|
+
## 1. Orient — ground before you build
|
|
21
|
+
|
|
22
|
+
Before forming any theory or plan:
|
|
23
|
+
|
|
24
|
+
- **Read the real source.** The repo's source files at the current HEAD — not
|
|
25
|
+
build artifacts (`dist/`, generated files, caches), not your memory of the
|
|
26
|
+
code, not the task description's paraphrase. If a claim matters, open the
|
|
27
|
+
file.
|
|
28
|
+
- **Verify the root cause, not the first plausible cause.** Reproduce or trace
|
|
29
|
+
the failure to a specific mechanism before fixing. "This line looks wrong"
|
|
30
|
+
is a lead; the fix ships only when you can say *why* it produced the
|
|
31
|
+
observed symptom.
|
|
32
|
+
- **Report contradicting evidence.** If what you find contradicts the task
|
|
33
|
+
description, the ticket, or your own working theory — say so explicitly and
|
|
34
|
+
stop to re-plan. Never force-fit evidence to the plan you already had.
|
|
35
|
+
- **Cite everything.** Claims about the codebase carry `file:line`, commit
|
|
36
|
+
hashes, or PR numbers. "The scaffold appends fragments at
|
|
37
|
+
`src/agents/scaffold.ts:4113`" is a claim; "the scaffold appends fragments
|
|
38
|
+
somewhere" is not.
|
|
39
|
+
|
|
40
|
+
## 2. Clarify vs proceed
|
|
41
|
+
|
|
42
|
+
- **Infer first.** Most questions are answerable from the codebase, git
|
|
43
|
+
history, existing tests, or docs. Exhaust those before asking.
|
|
44
|
+
- **One question at a time.** If genuinely unsure after inferring, ask the
|
|
45
|
+
single question whose answer unblocks the most work. Phrase it as a
|
|
46
|
+
decision with a default: state what you found, the 2–3 viable options, which
|
|
47
|
+
you'd pick and why, and ask for confirmation — e.g. *"The config loader
|
|
48
|
+
supports both YAML and JSON overlays; the task says 'config file' without
|
|
49
|
+
specifying. I'd extend the YAML path since all existing overlays are YAML
|
|
50
|
+
(src/config/merge.ts:88) — confirm, or should JSON be covered too?"* Never
|
|
51
|
+
send a questionnaire.
|
|
52
|
+
- **Phase discipline.** Clarify during *planning*. Once the plan is agreed,
|
|
53
|
+
execute autonomously: make the reasonable call on small ambiguities, record
|
|
54
|
+
the assumption in your report, and keep moving. Mid-execution questions are
|
|
55
|
+
reserved for discoveries that invalidate the plan.
|
|
56
|
+
|
|
57
|
+
## 3. Design-align on larger tasks
|
|
58
|
+
|
|
59
|
+
**Classify the task first.** Treat it as "larger" (design-align before
|
|
60
|
+
implementing) when ANY of these hold:
|
|
61
|
+
|
|
62
|
+
- It changes a public interface, schema, config shape, or on-disk format.
|
|
63
|
+
- It cuts across 3+ modules or touches a load-bearing invariant.
|
|
64
|
+
- Two or more genuinely different approaches exist and the choice is
|
|
65
|
+
expensive to reverse.
|
|
66
|
+
- The task description is a goal ("make X reliable") rather than a change
|
|
67
|
+
("add flag Y").
|
|
68
|
+
- It will land as more than one PR.
|
|
69
|
+
|
|
70
|
+
Small, single-concern, obvious-approach changes skip straight to phase 4.
|
|
71
|
+
|
|
72
|
+
For larger tasks:
|
|
73
|
+
|
|
74
|
+
1. **Design report before code.** Send the user an evidence-grounded report:
|
|
75
|
+
what exists today (with citations), what will change, the chosen approach
|
|
76
|
+
and its rejected alternatives, and the PR staging plan. Get alignment
|
|
77
|
+
before implementation.
|
|
78
|
+
2. **Red-team your own plan adversarially.** Review the design item by item.
|
|
79
|
+
Each item gets a verdict — `SOUND`, `RISK`, or `WRONG` — backed by
|
|
80
|
+
evidence (a file you read, a test you ran, a documented behavior), not
|
|
81
|
+
intuition. Fix every `WRONG` and address every `RISK` before starting.
|
|
82
|
+
3. **Stage delivery as focused single-concern PRs.** One concern per PR:
|
|
83
|
+
reviewable in one sitting, revertable in one command. Never bundle a
|
|
84
|
+
refactor with a behavior change.
|
|
85
|
+
|
|
86
|
+
## 4. Pipeline — how a change ships
|
|
87
|
+
|
|
88
|
+
1. **Branch off fresh main.** `git fetch && git checkout -b <branch> origin/main`.
|
|
89
|
+
2. **Implement with durable fixes.** Fix root causes. A workaround is
|
|
90
|
+
acceptable only with an explicit reason stated and a follow-up filed.
|
|
91
|
+
Prefer deterministic mechanisms (a check, a hook, a schema, a lint gate)
|
|
92
|
+
over model-dependent behavior — if code can enforce the guarantee, don't
|
|
93
|
+
leave it to prompt discipline.
|
|
94
|
+
3. **Tests assert outcomes.** Every test must fail if the bug it guards
|
|
95
|
+
returns. A test that merely exercises the code path without asserting the
|
|
96
|
+
observable outcome is not a test.
|
|
97
|
+
4. **Scoped tests + lint locally.** Run the test files covering what you
|
|
98
|
+
touched, plus the repo's lint gate. Local runs are a fast filter; **CI is
|
|
99
|
+
the full-suite authority** — never claim done off a local run alone.
|
|
100
|
+
5. **Adversarial review of the diff.** Dispatch a reviewer (sub-agent or
|
|
101
|
+
fresh pass) with this structure:
|
|
102
|
+
- Input: the full diff, the task statement, and the design report if one
|
|
103
|
+
exists.
|
|
104
|
+
- Charge: *find reasons this change is wrong* — correctness, missed edge
|
|
105
|
+
cases, untested behavior, inconsistency with surrounding code, docs
|
|
106
|
+
drift, security/data-loss risk.
|
|
107
|
+
- Output: a findings list, each with severity (high/medium/low), the
|
|
108
|
+
evidence (`file:line`), and a concrete fix.
|
|
109
|
+
6. **Fix ALL findings — including lows.** A low you skip is a bug you
|
|
110
|
+
shipped. If a finding is genuinely invalid, rebut it with evidence in
|
|
111
|
+
writing; silence is not a rebuttal.
|
|
112
|
+
7. **Re-review the fix.** The re-review verdict must contain, per original
|
|
113
|
+
finding: the finding ID, what changed (`file:line` of the fix), whether it
|
|
114
|
+
fully resolves the finding (`RESOLVED` / `PARTIAL` / `REBUTTED` with
|
|
115
|
+
evidence), and whether the fix introduced anything new. A bare "fixed" is
|
|
116
|
+
not a verdict.
|
|
117
|
+
8. **Merge only on CI green.** No exceptions. A red or flaky CI run is a
|
|
118
|
+
blocker to investigate, not to override.
|
|
119
|
+
|
|
120
|
+
## 5. Communicate while you work
|
|
121
|
+
|
|
122
|
+
- **Consolidated messages.** Batch related findings and results into one
|
|
123
|
+
substantive update; never send five fragments where one message serves.
|
|
124
|
+
- **Always-visible progress.** Long-running work surfaces status the user can
|
|
125
|
+
see (progress card, interim edit, explicit "still running: X"). Never go
|
|
126
|
+
dark mid-task.
|
|
127
|
+
- **No foreground watches over 30 seconds.** Anything longer — builds, CI
|
|
128
|
+
waits, deploys — runs in the background with a notification on completion.
|
|
129
|
+
Don't block a turn polling.
|
|
130
|
+
- **Max 15 parallel sub-agents.** Fan out for genuinely parallel work
|
|
131
|
+
(independent reviews, independent modules), but cap the swarm at 15.
|
|
@@ -276,7 +276,8 @@ Each plugin instance is bound to one agent (via `SWITCHROOM_AGENT_NAME` set by `
|
|
|
276
276
|
|---------|-------------|
|
|
277
277
|
| `/agents` | List all agents and their status |
|
|
278
278
|
| `/agentstart [name]` | Start an agent (default: this agent) |
|
|
279
|
-
| `/
|
|
279
|
+
| `/agentstop [name]` | Stop an agent's container (default: this agent) |
|
|
280
|
+
| `/stop` | Cancel this agent's in-flight turn (bare `stop` as a message works too) |
|
|
280
281
|
| `/restart [name\|all]` | Restart an agent (default: this agent; pass `all` for every agent) |
|
|
281
282
|
| `/auth` | Show auth/token status |
|
|
282
283
|
| `/topics` | Show topic-to-agent mappings |
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
2
5
|
import {
|
|
3
6
|
dispatchAdminCommand,
|
|
4
7
|
parseCommandName,
|
|
@@ -41,12 +44,47 @@ describe('parseCommandName', () => {
|
|
|
41
44
|
|
|
42
45
|
describe('ADMIN_COMMAND_NAMES', () => {
|
|
43
46
|
it('contains the fleet-management admin commands', () => {
|
|
44
|
-
const required = ['agents', 'logs', 'restart', 'update', 'reconcile', '
|
|
47
|
+
const required = ['agents', 'logs', 'restart', 'update', 'reconcile', 'agentstop', 'agentstart', 'grant', 'dangerous', 'permissions', 'vault', 'audit']
|
|
45
48
|
for (const cmd of required) {
|
|
46
49
|
expect(ADMIN_COMMAND_NAMES.has(cmd)).toBe(true)
|
|
47
50
|
}
|
|
48
51
|
})
|
|
49
52
|
|
|
53
|
+
it('replaced container-stop /stop with /agentstop (#3020 / #1394)', () => {
|
|
54
|
+
// 'stop' now cancels the in-flight turn on EVERY agent (like /interrupt),
|
|
55
|
+
// so it must NOT be admin-gated; container stop moved to 'agentstop',
|
|
56
|
+
// which MUST be admin-gated or any forum member could stop containers
|
|
57
|
+
// via non-admin agents (the #1394 hole).
|
|
58
|
+
expect(ADMIN_COMMAND_NAMES.has('stop')).toBe(false)
|
|
59
|
+
expect(ADMIN_COMMAND_NAMES.has('agentstop')).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('stays in sync with the tier-2 command list in docs/architecture.md', () => {
|
|
63
|
+
// The doc's "Fleet-management commands" line is the operator-facing
|
|
64
|
+
// contract; ADMIN_COMMAND_NAMES is the enforcing set. Drift between the
|
|
65
|
+
// two either advertises an ungated verb or hides a gated one.
|
|
66
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
67
|
+
const doc = readFileSync(
|
|
68
|
+
resolve(__dirname, '..', '..', 'docs', 'architecture.md'),
|
|
69
|
+
'utf8',
|
|
70
|
+
)
|
|
71
|
+
const tier2Line = doc
|
|
72
|
+
.split('\n')
|
|
73
|
+
.find(l => l.startsWith('2. **Fleet-management commands**'))
|
|
74
|
+
expect(tier2Line, 'tier-2 line missing from docs/architecture.md').toBeTruthy()
|
|
75
|
+
const documented = new Set(
|
|
76
|
+
[...tier2Line!.matchAll(/`\/([a-z]+)(?:\s+<[^>]+>)?`/g)].map(m => m[1]!),
|
|
77
|
+
)
|
|
78
|
+
// Every documented tier-2 verb is enforced…
|
|
79
|
+
for (const cmd of documented) {
|
|
80
|
+
expect(ADMIN_COMMAND_NAMES.has(cmd), `doc lists /${cmd} but ADMIN_COMMAND_NAMES lacks it`).toBe(true)
|
|
81
|
+
}
|
|
82
|
+
// …and every enforced verb is documented.
|
|
83
|
+
for (const cmd of ADMIN_COMMAND_NAMES) {
|
|
84
|
+
expect(documented.has(cmd), `ADMIN_COMMAND_NAMES has '${cmd}' but the doc tier-2 line lacks /${cmd}`).toBe(true)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
50
88
|
it('does not contain per-agent auth ops (must work without model)', () => {
|
|
51
89
|
// /auth, /reauth, /authfallback are handled by the gateway directly so
|
|
52
90
|
// the user can re-authenticate even when the model is rate-limited or the
|
|
@@ -251,7 +289,7 @@ describe('classifyAdminGate', () => {
|
|
|
251
289
|
})
|
|
252
290
|
})
|
|
253
291
|
it('blocks /agents, /update, /vault, /permissions', () => {
|
|
254
|
-
for (const c of ['agents', 'update', 'vault', 'permissions', '
|
|
292
|
+
for (const c of ['agents', 'update', 'vault', 'permissions', 'agentstop', 'agentstart', 'reconcile', 'dangerous', 'memory', 'topics']) {
|
|
255
293
|
const r = classifyAdminGate(`/${c}`, me)
|
|
256
294
|
expect(r).toEqual({ action: 'block', reason: 'admin-required', cmd: c })
|
|
257
295
|
}
|
|
@@ -49,7 +49,12 @@ export const ADMIN_COMMAND_NAMES = new Set<string>([
|
|
|
49
49
|
'agents',
|
|
50
50
|
'logs',
|
|
51
51
|
'restart',
|
|
52
|
-
|
|
52
|
+
// #3020: container stop is /agentstop (pairs with /agentstart). Plain
|
|
53
|
+
// 'stop' is deliberately ABSENT — it cancels the in-flight turn on every
|
|
54
|
+
// agent (like /interrupt), so admin-gating it would break the kill switch
|
|
55
|
+
// on non-admin agents; and leaving container-stop off this list would let
|
|
56
|
+
// any forum member stop containers via non-admin agents (#1394).
|
|
57
|
+
'agentstop',
|
|
53
58
|
'agentstart',
|
|
54
59
|
'update',
|
|
55
60
|
'reconcile',
|