switchroom 0.13.9 → 0.13.10
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/cli/switchroom.js +38 -14
- package/dist/host-control/main.js +222 -7
- package/examples/switchroom.yaml +25 -7
- package/package.json +1 -1
- package/profiles/_shared/telegram-style.md.hbs +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +514 -143
- package/telegram-plugin/gateway/config-approval-handler.test.ts +246 -0
- package/telegram-plugin/gateway/config-approval-handler.ts +284 -0
- package/telegram-plugin/gateway/gateway.ts +206 -21
- package/telegram-plugin/gateway/ipc-protocol.ts +72 -2
- package/telegram-plugin/gateway/ipc-server.ts +101 -0
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +103 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +69 -0
- package/telegram-plugin/subagent-watcher.ts +39 -0
- package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +105 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +61 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +67 -1
- package/telegram-plugin/uat/scenarios/jtbd-subagent-handback-dm.test.ts +95 -0
- package/profiles/default/CLAUDE.md +0 -193
package/dist/cli/switchroom.js
CHANGED
|
@@ -47314,8 +47314,8 @@ var {
|
|
|
47314
47314
|
} = import__.default;
|
|
47315
47315
|
|
|
47316
47316
|
// src/build-info.ts
|
|
47317
|
-
var VERSION = "0.13.
|
|
47318
|
-
var COMMIT_SHA = "
|
|
47317
|
+
var VERSION = "0.13.10";
|
|
47318
|
+
var COMMIT_SHA = "e0fd6617";
|
|
47319
47319
|
|
|
47320
47320
|
// src/cli/agent.ts
|
|
47321
47321
|
init_source();
|
|
@@ -48782,7 +48782,10 @@ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
|
|
|
48782
48782
|
\`disable_notification: true\`.
|
|
48783
48783
|
4. **Hand back delegations with synthesis.** When a sub-agent / worker
|
|
48784
48784
|
returns, re-enter in YOUR voice \u2014 what it found, and what you are
|
|
48785
|
-
doing next. Never let its raw report stand as your reply.
|
|
48785
|
+
doing next. Never let its raw report stand as your reply. A
|
|
48786
|
+
*background* worker finishes after your turn ends; its result
|
|
48787
|
+
arrives as a fresh \`<channel source="subagent_handback">\` turn \u2014
|
|
48788
|
+
treat that turn as the cue to do exactly this.
|
|
48786
48789
|
5. **Deliver the answer** as a final \`reply\`.
|
|
48787
48790
|
|
|
48788
48791
|
The one thing to avoid is *spam*: a reply on every tool call, on a
|
|
@@ -49665,7 +49668,7 @@ function reconcileAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchr
|
|
|
49665
49668
|
const hindsightRecallCacheTtlSecs = agentConfig.memory?.recall?.cache_ttl_secs;
|
|
49666
49669
|
const hindsightRecallMinOverlap = agentConfig.memory?.recall?.min_overlap;
|
|
49667
49670
|
const startShPath = join8(agentDir, "start.sh");
|
|
49668
|
-
{
|
|
49671
|
+
if (!options.skipProfileTemplates) {
|
|
49669
49672
|
const basePath = getBaseProfilePath();
|
|
49670
49673
|
const startShContext = {
|
|
49671
49674
|
name,
|
|
@@ -49728,7 +49731,10 @@ messaging a capable colleague \u2014 not a tool emitting output. Five beats:
|
|
|
49728
49731
|
\`disable_notification: true\`.
|
|
49729
49732
|
4. **Hand back delegations with synthesis.** When a sub-agent / worker
|
|
49730
49733
|
returns, re-enter in YOUR voice \u2014 what it found, and what you are
|
|
49731
|
-
doing next. Never let its raw report stand as your reply.
|
|
49734
|
+
doing next. Never let its raw report stand as your reply. A
|
|
49735
|
+
*background* worker finishes after your turn ends; its result
|
|
49736
|
+
arrives as a fresh \`<channel source="subagent_handback">\` turn \u2014
|
|
49737
|
+
treat that turn as the cue to do exactly this.
|
|
49732
49738
|
5. **Deliver the answer** as a final \`reply\`.
|
|
49733
49739
|
|
|
49734
49740
|
The one thing to avoid is *spam*: a reply on every tool call, on a
|
|
@@ -49801,7 +49807,7 @@ Don't wait for a slash command. Don't ask permission. Memory work is table stake
|
|
|
49801
49807
|
changes.push(startShPath);
|
|
49802
49808
|
}
|
|
49803
49809
|
}
|
|
49804
|
-
if (!options.preserveClaudeMd) {
|
|
49810
|
+
if (!options.preserveClaudeMd && !options.skipProfileTemplates) {
|
|
49805
49811
|
const profilePath = getProfilePath(agentConfig.extends ?? DEFAULT_PROFILE);
|
|
49806
49812
|
const claudeMdSrc = join8(profilePath, "CLAUDE.md.hbs");
|
|
49807
49813
|
const claudeMdDest = join8(agentDir, "CLAUDE.md");
|
|
@@ -73400,13 +73406,31 @@ defaults:
|
|
|
73400
73406
|
PreToolUse:
|
|
73401
73407
|
- command: "/opt/switchroom-audit.sh"
|
|
73402
73408
|
timeout: 5
|
|
73403
|
-
# Bundled skills that ship with switchroom
|
|
73404
|
-
#
|
|
73405
|
-
#
|
|
73406
|
-
#
|
|
73407
|
-
#
|
|
73408
|
-
#
|
|
73409
|
-
|
|
73409
|
+
# Bundled skills that ship with switchroom \u2014 general-purpose capabilities
|
|
73410
|
+
# every agent gets by default. Per-agent \`skills:\` is unioned with this
|
|
73411
|
+
# list; don't repeat shared skills in each agent. Remove an entry here to
|
|
73412
|
+
# disable it fleet-wide.
|
|
73413
|
+
#
|
|
73414
|
+
# docx / pdf / pptx / xlsx \u2014 Office + PDF document handling (read,
|
|
73415
|
+
# extract, generate). Pairs with the
|
|
73416
|
+
# pandoc/imagemagick bins in the base image.
|
|
73417
|
+
# webapp-testing \u2014 Playwright-driven UI checks (browsers are
|
|
73418
|
+
# baked into the agent image at
|
|
73419
|
+
# /opt/playwright/browsers).
|
|
73420
|
+
# mcp-builder \u2014 scaffold + iterate on MCP servers.
|
|
73421
|
+
# skill-creator \u2014 author new Claude Code skills.
|
|
73422
|
+
# file-bug \u2014 structured issue filing into the operator's
|
|
73423
|
+
# tracker.
|
|
73424
|
+
# humanizer \u2014 strips AI-writing patterns from replies
|
|
73425
|
+
# before they reach Telegram (29 patterns
|
|
73426
|
+
# from Wikipedia's "Signs of AI writing").
|
|
73427
|
+
# humanizer-calibrate \u2014 builds a personal voice template from your
|
|
73428
|
+
# message history; feeds humanizer.
|
|
73429
|
+
#
|
|
73430
|
+
# Job-specific / dev-only skills (buildkite-*, telegram-test-harness,
|
|
73431
|
+
# token-helpers, switchroom-*) are intentionally NOT in this default
|
|
73432
|
+
# list \u2014 opt in per-agent where they apply.
|
|
73433
|
+
skills: [docx, pdf, pptx, xlsx, webapp-testing, mcp-builder, skill-creator, file-bug, humanizer, humanizer-calibrate]
|
|
73410
73434
|
# Optional: point the humanizer at a voice template generated by
|
|
73411
73435
|
# \`/humanizer-calibrate\`. Without this, falls back to generic rules.
|
|
73412
73436
|
# humanizer_voice_file: ~/.switchroom/voice.md
|
|
@@ -75458,7 +75482,7 @@ function reconcileAgentCronOnly(agent) {
|
|
|
75458
75482
|
return { ok: false, error: `agent "${agent}" not in switchroom.yaml` };
|
|
75459
75483
|
}
|
|
75460
75484
|
const agentsDir = resolveAgentsDir(config);
|
|
75461
|
-
const result = reconcileAgent(agent, agentConfig, agentsDir, config.telegram, config, undefined, {});
|
|
75485
|
+
const result = reconcileAgent(agent, agentConfig, agentsDir, config.telegram, config, undefined, { skipProfileTemplates: true });
|
|
75462
75486
|
const changes = [...result.changes];
|
|
75463
75487
|
const nonCron = changes.filter((p) => classifyChangeKind(p) !== "cron");
|
|
75464
75488
|
if (nonCron.length > 0) {
|
|
@@ -9666,6 +9666,8 @@ var init_secretlint_source = __esm(() => {
|
|
|
9666
9666
|
|
|
9667
9667
|
// src/host-control/main.ts
|
|
9668
9668
|
import { homedir as homedir3 } from "node:os";
|
|
9669
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
9670
|
+
import { join as join4, resolve as resolve6 } from "node:path";
|
|
9669
9671
|
|
|
9670
9672
|
// src/config/loader.ts
|
|
9671
9673
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
|
|
@@ -14702,9 +14704,11 @@ import {
|
|
|
14702
14704
|
readFileSync as readFileSync4,
|
|
14703
14705
|
writeFileSync as writeFileSync2,
|
|
14704
14706
|
renameSync,
|
|
14705
|
-
mkdirSync
|
|
14707
|
+
mkdirSync,
|
|
14708
|
+
unlinkSync
|
|
14706
14709
|
} from "node:fs";
|
|
14707
14710
|
import { join as join3, dirname as dirname2, resolve as resolve5 } from "node:path";
|
|
14711
|
+
import { randomUUID, randomBytes } from "node:crypto";
|
|
14708
14712
|
|
|
14709
14713
|
// src/host-control/protocol.ts
|
|
14710
14714
|
var MAX_FRAME_BYTES = 64 * 1024;
|
|
@@ -15634,7 +15638,7 @@ function validateConfigEdit(opts) {
|
|
|
15634
15638
|
const leakErr = secretLeakGuard(beforeData, parsedAfter.data);
|
|
15635
15639
|
if (leakErr)
|
|
15636
15640
|
return leakErr;
|
|
15637
|
-
return { ok: true };
|
|
15641
|
+
return { ok: true, postApplyContent: applied.after };
|
|
15638
15642
|
}
|
|
15639
15643
|
|
|
15640
15644
|
// src/host-control/server.ts
|
|
@@ -15688,6 +15692,15 @@ function readCachedInstallType(bindRoot) {
|
|
|
15688
15692
|
} catch {}
|
|
15689
15693
|
return payload;
|
|
15690
15694
|
}
|
|
15695
|
+
var CONFIG_APPROVAL_TIMEOUT_MS = 10 * 60 * 1000;
|
|
15696
|
+
function defaultApprovalId() {
|
|
15697
|
+
return randomBytes(4).toString("hex");
|
|
15698
|
+
}
|
|
15699
|
+
function unlinkSyncBestEffort(path2) {
|
|
15700
|
+
try {
|
|
15701
|
+
unlinkSync(path2);
|
|
15702
|
+
} catch {}
|
|
15703
|
+
}
|
|
15691
15704
|
var STATUS_RETENTION_MS = 10 * 60 * 1000;
|
|
15692
15705
|
var STATUS_MAX_ENTRIES = 256;
|
|
15693
15706
|
var TAIL_BYTES = 4096;
|
|
@@ -15916,7 +15929,7 @@ class HostdServer {
|
|
|
15916
15929
|
resp = await this.handleAgentSmoke(req, started);
|
|
15917
15930
|
break;
|
|
15918
15931
|
case "config_propose_edit":
|
|
15919
|
-
resp = this.handleConfigProposeEdit(req, started);
|
|
15932
|
+
resp = await this.handleConfigProposeEdit(req, caller, started);
|
|
15920
15933
|
break;
|
|
15921
15934
|
}
|
|
15922
15935
|
} catch (err) {
|
|
@@ -16213,7 +16226,7 @@ class HostdServer {
|
|
|
16213
16226
|
stderr_tail: tail(res.stderr)
|
|
16214
16227
|
};
|
|
16215
16228
|
}
|
|
16216
|
-
handleConfigProposeEdit(req, started) {
|
|
16229
|
+
async handleConfigProposeEdit(req, caller, started) {
|
|
16217
16230
|
const enabled = this.opts.config.hostd?.config_edit_enabled === true;
|
|
16218
16231
|
if (!enabled) {
|
|
16219
16232
|
return errorResponse(req.request_id, "E_CONFIG_EDIT_DISABLED: config_propose_edit is disabled; " + "operator must set hostd.config_edit_enabled=true in " + "switchroom.yaml to opt in", Date.now() - started);
|
|
@@ -16227,7 +16240,96 @@ class HostdServer {
|
|
|
16227
16240
|
if (!verdict.ok) {
|
|
16228
16241
|
return errorResponse(req.request_id, `${verdict.code}: ${verdict.detail}`, Date.now() - started);
|
|
16229
16242
|
}
|
|
16230
|
-
|
|
16243
|
+
if (!this.opts.approvalGateway) {
|
|
16244
|
+
return errorResponse(req.request_id, "E_NO_APPROVAL_GATEWAY: validation passed but hostd was " + "started without an approval-gateway wiring; the operator " + "build is missing the telegram-plugin link", Date.now() - started);
|
|
16245
|
+
}
|
|
16246
|
+
const callerName = caller.kind === "agent" ? caller.name : "operator";
|
|
16247
|
+
const approvalId = (this.opts.generateApprovalId ?? defaultApprovalId)();
|
|
16248
|
+
const approval = await this.opts.approvalGateway.requestApproval({
|
|
16249
|
+
requestId: approvalId,
|
|
16250
|
+
agentName: callerName,
|
|
16251
|
+
reason: req.args.reason,
|
|
16252
|
+
unifiedDiff: req.args.unified_diff,
|
|
16253
|
+
timeoutMs: CONFIG_APPROVAL_TIMEOUT_MS
|
|
16254
|
+
});
|
|
16255
|
+
if (approval.verdict === "deny") {
|
|
16256
|
+
return errorResponse(req.request_id, `E_DENIED: operator denied config_propose_edit (approval_id=${approvalId})`, Date.now() - started);
|
|
16257
|
+
}
|
|
16258
|
+
if (approval.verdict === "timeout") {
|
|
16259
|
+
return errorResponse(req.request_id, `E_APPROVAL_TIMEOUT: operator approval card expired without a tap (approval_id=${approvalId})`, Date.now() - started);
|
|
16260
|
+
}
|
|
16261
|
+
const release = await this.acquireConfigApplyLock();
|
|
16262
|
+
try {
|
|
16263
|
+
let snapshot;
|
|
16264
|
+
try {
|
|
16265
|
+
snapshot = readFileSync4(configPath, "utf-8");
|
|
16266
|
+
} catch (err) {
|
|
16267
|
+
await approval.finalize({
|
|
16268
|
+
outcome: "reconcile_failed_rolled_back",
|
|
16269
|
+
detail: `pre-write snapshot read failed: ${err.message}`
|
|
16270
|
+
});
|
|
16271
|
+
return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: snapshot read failed: ${err.message}`, Date.now() - started);
|
|
16272
|
+
}
|
|
16273
|
+
const postApply = verdict.postApplyContent;
|
|
16274
|
+
const tmp = configPath + ".tmp";
|
|
16275
|
+
try {
|
|
16276
|
+
writeFileSync2(tmp, postApply);
|
|
16277
|
+
renameSync(tmp, configPath);
|
|
16278
|
+
} catch (err) {
|
|
16279
|
+
unlinkSyncBestEffort(tmp);
|
|
16280
|
+
await approval.finalize({
|
|
16281
|
+
outcome: "reconcile_failed_rolled_back",
|
|
16282
|
+
detail: `atomic write failed: ${err.message}`
|
|
16283
|
+
});
|
|
16284
|
+
return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: write failed: ${err.message}`, Date.now() - started);
|
|
16285
|
+
}
|
|
16286
|
+
const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(["apply"]));
|
|
16287
|
+
const recRes = await runner({ requestId: approvalId });
|
|
16288
|
+
if (recRes.exit_code === 0) {
|
|
16289
|
+
await approval.finalize({ outcome: "applied" });
|
|
16290
|
+
return {
|
|
16291
|
+
v: 1,
|
|
16292
|
+
request_id: req.request_id,
|
|
16293
|
+
result: "completed",
|
|
16294
|
+
exit_code: 0,
|
|
16295
|
+
duration_ms: Date.now() - started,
|
|
16296
|
+
stdout_tail: tail(recRes.stdout),
|
|
16297
|
+
stderr_tail: tail(recRes.stderr)
|
|
16298
|
+
};
|
|
16299
|
+
}
|
|
16300
|
+
let rollbackDetail = "";
|
|
16301
|
+
try {
|
|
16302
|
+
writeFileSync2(tmp, snapshot);
|
|
16303
|
+
renameSync(tmp, configPath);
|
|
16304
|
+
} catch (err) {
|
|
16305
|
+
rollbackDetail = `snapshot restore failed: ${err.message}`;
|
|
16306
|
+
await approval.finalize({
|
|
16307
|
+
outcome: "reconcile_failed_rolled_back",
|
|
16308
|
+
detail: rollbackDetail
|
|
16309
|
+
});
|
|
16310
|
+
return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: ${rollbackDetail}`, Date.now() - started);
|
|
16311
|
+
}
|
|
16312
|
+
const recRes2 = await runner({ requestId: approvalId });
|
|
16313
|
+
const recoveryNote = recRes2.exit_code === 0 ? "rolled back successfully" : `rolled back but recovery reconcile also failed (exit ${recRes2.exit_code})`;
|
|
16314
|
+
await approval.finalize({
|
|
16315
|
+
outcome: "reconcile_failed_rolled_back",
|
|
16316
|
+
detail: recoveryNote
|
|
16317
|
+
});
|
|
16318
|
+
return errorResponse(req.request_id, `E_RECONCILE_FAILED_ROLLED_BACK: reconcile exit ${recRes.exit_code}; ${recoveryNote}`, Date.now() - started);
|
|
16319
|
+
} finally {
|
|
16320
|
+
release();
|
|
16321
|
+
}
|
|
16322
|
+
}
|
|
16323
|
+
configApplyLock = Promise.resolve();
|
|
16324
|
+
async acquireConfigApplyLock() {
|
|
16325
|
+
let release;
|
|
16326
|
+
const next = new Promise((r) => {
|
|
16327
|
+
release = r;
|
|
16328
|
+
});
|
|
16329
|
+
const prior = this.configApplyLock;
|
|
16330
|
+
this.configApplyLock = prior.then(() => next);
|
|
16331
|
+
await prior;
|
|
16332
|
+
return release;
|
|
16231
16333
|
}
|
|
16232
16334
|
async handleAgentSmoke(req, started) {
|
|
16233
16335
|
const container = `switchroom-${req.args.name}`;
|
|
@@ -16529,6 +16631,104 @@ function isSafeExecArgvElement(s) {
|
|
|
16529
16631
|
return !/[\u0000-\u001f\u007f]/.test(s);
|
|
16530
16632
|
}
|
|
16531
16633
|
|
|
16634
|
+
// src/host-control/approval-gateway.ts
|
|
16635
|
+
import { connect } from "node:net";
|
|
16636
|
+
|
|
16637
|
+
class SocketApprovalGateway {
|
|
16638
|
+
opts;
|
|
16639
|
+
constructor(opts) {
|
|
16640
|
+
this.opts = opts;
|
|
16641
|
+
}
|
|
16642
|
+
async requestApproval(req) {
|
|
16643
|
+
const sockPath = this.opts.resolveGatewaySocket(req.agentName);
|
|
16644
|
+
if (sockPath === null) {
|
|
16645
|
+
return {
|
|
16646
|
+
verdict: "deny",
|
|
16647
|
+
finalize: async () => {}
|
|
16648
|
+
};
|
|
16649
|
+
}
|
|
16650
|
+
return await new Promise((resolve6) => {
|
|
16651
|
+
const client = connect({ path: sockPath });
|
|
16652
|
+
let buffer = "";
|
|
16653
|
+
let resolved = false;
|
|
16654
|
+
const log = this.opts.log ?? (() => {});
|
|
16655
|
+
const finalize = async (outcome) => {
|
|
16656
|
+
if (client.destroyed)
|
|
16657
|
+
return;
|
|
16658
|
+
try {
|
|
16659
|
+
client.write(JSON.stringify({
|
|
16660
|
+
type: "request_config_finalize",
|
|
16661
|
+
requestId: req.requestId,
|
|
16662
|
+
outcome: outcome.outcome,
|
|
16663
|
+
...outcome.detail ? { detail: outcome.detail } : {}
|
|
16664
|
+
}) + `
|
|
16665
|
+
`);
|
|
16666
|
+
client.end();
|
|
16667
|
+
} catch (err) {
|
|
16668
|
+
log(`finalize write failed (requestId=${req.requestId}): ${err.message}`);
|
|
16669
|
+
}
|
|
16670
|
+
};
|
|
16671
|
+
client.on("connect", () => {
|
|
16672
|
+
try {
|
|
16673
|
+
client.write(JSON.stringify({
|
|
16674
|
+
type: "request_config_approval",
|
|
16675
|
+
requestId: req.requestId,
|
|
16676
|
+
agentName: req.agentName,
|
|
16677
|
+
reason: req.reason,
|
|
16678
|
+
unifiedDiff: req.unifiedDiff,
|
|
16679
|
+
timeoutMs: req.timeoutMs
|
|
16680
|
+
}) + `
|
|
16681
|
+
`);
|
|
16682
|
+
} catch (err) {
|
|
16683
|
+
if (resolved)
|
|
16684
|
+
return;
|
|
16685
|
+
resolved = true;
|
|
16686
|
+
log(`request_config_approval write failed (requestId=${req.requestId}): ${err.message}`);
|
|
16687
|
+
resolve6({ verdict: "deny", finalize: async () => {} });
|
|
16688
|
+
}
|
|
16689
|
+
});
|
|
16690
|
+
client.on("data", (chunk2) => {
|
|
16691
|
+
buffer += chunk2.toString("utf8");
|
|
16692
|
+
const lines = buffer.split(`
|
|
16693
|
+
`);
|
|
16694
|
+
buffer = lines.pop() ?? "";
|
|
16695
|
+
for (const line of lines) {
|
|
16696
|
+
if (!line.trim())
|
|
16697
|
+
continue;
|
|
16698
|
+
let parsed;
|
|
16699
|
+
try {
|
|
16700
|
+
parsed = JSON.parse(line);
|
|
16701
|
+
} catch {
|
|
16702
|
+
log(`bad JSON from gateway: ${line.slice(0, 200)}`);
|
|
16703
|
+
continue;
|
|
16704
|
+
}
|
|
16705
|
+
const obj = parsed;
|
|
16706
|
+
if (obj.type === "config_approval_resolved" && obj.requestId === req.requestId && (obj.verdict === "approve" || obj.verdict === "deny" || obj.verdict === "timeout") && !resolved) {
|
|
16707
|
+
resolved = true;
|
|
16708
|
+
resolve6({
|
|
16709
|
+
verdict: obj.verdict,
|
|
16710
|
+
finalize
|
|
16711
|
+
});
|
|
16712
|
+
}
|
|
16713
|
+
}
|
|
16714
|
+
});
|
|
16715
|
+
client.on("error", (err) => {
|
|
16716
|
+
if (resolved)
|
|
16717
|
+
return;
|
|
16718
|
+
resolved = true;
|
|
16719
|
+
log(`gateway socket error (requestId=${req.requestId}): ${err.message}`);
|
|
16720
|
+
resolve6({ verdict: "deny", finalize: async () => {} });
|
|
16721
|
+
});
|
|
16722
|
+
client.on("close", () => {
|
|
16723
|
+
if (resolved)
|
|
16724
|
+
return;
|
|
16725
|
+
resolved = true;
|
|
16726
|
+
resolve6({ verdict: "deny", finalize: async () => {} });
|
|
16727
|
+
});
|
|
16728
|
+
});
|
|
16729
|
+
}
|
|
16730
|
+
}
|
|
16731
|
+
|
|
16532
16732
|
// src/host-control/main.ts
|
|
16533
16733
|
async function main() {
|
|
16534
16734
|
const config = loadConfig();
|
|
@@ -16547,12 +16747,27 @@ async function main() {
|
|
|
16547
16747
|
process.stderr.write("hostd: no admin-flagged agents — nothing to serve. Set `admin: true` on at least one agent.\n");
|
|
16548
16748
|
process.exit(2);
|
|
16549
16749
|
}
|
|
16750
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join4(homedir3(), ".switchroom", "agents");
|
|
16751
|
+
const approvalGateway = new SocketApprovalGateway({
|
|
16752
|
+
resolveGatewaySocket: (agentName) => {
|
|
16753
|
+
const sock = resolve6(agentsDir, agentName, "telegram", "gateway.sock");
|
|
16754
|
+
return existsSync7(sock) ? sock : null;
|
|
16755
|
+
},
|
|
16756
|
+
log: (m) => process.stderr.write(`hostd: approval-gateway — ${m}
|
|
16757
|
+
`)
|
|
16758
|
+
});
|
|
16550
16759
|
const server = new HostdServer({
|
|
16551
16760
|
homeDir: homedir3(),
|
|
16552
16761
|
agentUids,
|
|
16553
16762
|
config: {
|
|
16554
|
-
agents: Object.fromEntries(Object.entries(config.agents).map(([n, a]) => [n, { admin: a.admin === true }]))
|
|
16555
|
-
|
|
16763
|
+
agents: Object.fromEntries(Object.entries(config.agents).map(([n, a]) => [n, { admin: a.admin === true }])),
|
|
16764
|
+
...config.hostd ? {
|
|
16765
|
+
hostd: {
|
|
16766
|
+
...config.hostd.config_edit_enabled !== undefined ? { config_edit_enabled: config.hostd.config_edit_enabled } : {}
|
|
16767
|
+
}
|
|
16768
|
+
} : {}
|
|
16769
|
+
},
|
|
16770
|
+
approvalGateway
|
|
16556
16771
|
});
|
|
16557
16772
|
await server.start();
|
|
16558
16773
|
const paths = server.getBoundPaths();
|
package/examples/switchroom.yaml
CHANGED
|
@@ -71,13 +71,31 @@ defaults:
|
|
|
71
71
|
PreToolUse:
|
|
72
72
|
- command: "/opt/switchroom-audit.sh"
|
|
73
73
|
timeout: 5
|
|
74
|
-
# Bundled skills that ship with switchroom
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
|
|
74
|
+
# Bundled skills that ship with switchroom — general-purpose capabilities
|
|
75
|
+
# every agent gets by default. Per-agent `skills:` is unioned with this
|
|
76
|
+
# list; don't repeat shared skills in each agent. Remove an entry here to
|
|
77
|
+
# disable it fleet-wide.
|
|
78
|
+
#
|
|
79
|
+
# docx / pdf / pptx / xlsx — Office + PDF document handling (read,
|
|
80
|
+
# extract, generate). Pairs with the
|
|
81
|
+
# pandoc/imagemagick bins in the base image.
|
|
82
|
+
# webapp-testing — Playwright-driven UI checks (browsers are
|
|
83
|
+
# baked into the agent image at
|
|
84
|
+
# /opt/playwright/browsers).
|
|
85
|
+
# mcp-builder — scaffold + iterate on MCP servers.
|
|
86
|
+
# skill-creator — author new Claude Code skills.
|
|
87
|
+
# file-bug — structured issue filing into the operator's
|
|
88
|
+
# tracker.
|
|
89
|
+
# humanizer — strips AI-writing patterns from replies
|
|
90
|
+
# before they reach Telegram (29 patterns
|
|
91
|
+
# from Wikipedia's "Signs of AI writing").
|
|
92
|
+
# humanizer-calibrate — builds a personal voice template from your
|
|
93
|
+
# message history; feeds humanizer.
|
|
94
|
+
#
|
|
95
|
+
# Job-specific / dev-only skills (buildkite-*, telegram-test-harness,
|
|
96
|
+
# token-helpers, switchroom-*) are intentionally NOT in this default
|
|
97
|
+
# list — opt in per-agent where they apply.
|
|
98
|
+
skills: [docx, pdf, pptx, xlsx, webapp-testing, mcp-builder, skill-creator, file-bug, humanizer, humanizer-calibrate]
|
|
81
99
|
# Optional: point the humanizer at a voice template generated by
|
|
82
100
|
# `/humanizer-calibrate`. Without this, falls back to generic rules.
|
|
83
101
|
# humanizer_voice_file: ~/.switchroom/voice.md
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ Telegram is a chat — replies should feel like one, not a terminal dump or a tr
|
|
|
8
8
|
- **1 · Acknowledge first.** Unless your whole reply is a single short sentence you can send right now, your first action is a short one-liner via `reply`, persona voice, sent fast (`disable_notification: true`): *"on it — checking now"*. This holds whether the work ahead is a tool call or a paragraph of pure reasoning — if the answer will run long, ack *before* you compose it. Skip the ack only for an immediate one-sentence answer (*"what's 2+2"*). This is a beat, not filler — it's the line between a colleague and a black box.
|
|
9
9
|
- **2 · Then go quiet and work.** Heads-down is right — do **not** narrate every tool call. A typing indicator runs for you automatically; you don't keep it alive.
|
|
10
10
|
- **3 · Surface meaningful progress** at genuine inflection points — a hard step finished, a blocker, a pivot, dispatching a sub-agent, a notably slow wait, a finding worth knowing now. One short `reply`, `disable_notification: true` (no mid-turn ping).
|
|
11
|
-
- **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"reviewer flagged the auth gap; fixing it now"*). Never let its raw report stand as your reply.
|
|
11
|
+
- **4 · Hand back delegations with synthesis.** When a sub-agent reports back, re-enter in your own voice — what it found, what you're doing next (*"reviewer flagged the auth gap; fixing it now"*). Never let its raw report stand as your reply. A *background* worker finishes after your turn has ended — its result is delivered to you as a fresh `<channel source="subagent_handback">` turn. That turn IS your cue: synthesise it for the user right then; don't treat it as noise and don't stay silent.
|
|
12
12
|
- **5 · Deliver the answer** as a fresh `reply` (omit `disable_notification` — pings once).
|
|
13
13
|
|
|
14
14
|
The one thing to avoid is **spam**: a reply on every tool call, on a cadence, or repeating yourself. Responsive and human, never a flood. A `<system-reminder>` containing `[silence-poke]` means you've gone quiet too long — send one short `reply` and carry on; skip it only if you're within ~5s of finishing.
|