switchroom 0.15.45 → 0.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/dist/agent-scheduler/index.js +122 -88
  2. package/dist/auth-broker/index.js +463 -177
  3. package/dist/cli/autoaccept-poll.js +4842 -35
  4. package/dist/cli/drive-write-pretool.mjs +17 -14
  5. package/dist/cli/notion-write-pretool.mjs +117 -86
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
  7. package/dist/cli/self-improve-stop.mjs +428 -0
  8. package/dist/cli/skill-validate-pretool.mjs +72 -72
  9. package/dist/cli/switchroom.js +3158 -1178
  10. package/dist/host-control/main.js +2833 -355
  11. package/dist/vault/approvals/kernel-server.js +7479 -7439
  12. package/dist/vault/broker/server.js +11312 -11272
  13. package/examples/minimal.yaml +1 -0
  14. package/examples/switchroom.yaml +1 -0
  15. package/package.json +3 -3
  16. package/profiles/_base/start.sh.hbs +88 -1
  17. package/profiles/_shared/execution-discipline.md.hbs +18 -0
  18. package/profiles/default/CLAUDE.md.hbs +0 -19
  19. package/telegram-plugin/.claude-plugin/plugin.json +2 -2
  20. package/telegram-plugin/answer-stream-flag.ts +12 -49
  21. package/telegram-plugin/answer-stream.ts +5 -150
  22. package/telegram-plugin/auth-snapshot-format.ts +280 -48
  23. package/telegram-plugin/auto-fallback-fleet.ts +44 -1
  24. package/telegram-plugin/context-exhaustion.ts +12 -0
  25. package/telegram-plugin/demo-mask.ts +154 -0
  26. package/telegram-plugin/dist/bridge/bridge.js +167 -124
  27. package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
  28. package/telegram-plugin/dist/server.js +215 -172
  29. package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
  30. package/telegram-plugin/draft-stream.ts +47 -410
  31. package/telegram-plugin/final-answer-detect.ts +17 -12
  32. package/telegram-plugin/fleet-fallback-resume.ts +131 -0
  33. package/telegram-plugin/format.ts +56 -19
  34. package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
  35. package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
  36. package/telegram-plugin/gateway/auth-command.ts +70 -14
  37. package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
  38. package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
  39. package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
  40. package/telegram-plugin/gateway/current-turn-map.ts +188 -0
  41. package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
  42. package/telegram-plugin/gateway/effort-command.ts +8 -3
  43. package/telegram-plugin/gateway/emission-authority.ts +369 -0
  44. package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
  45. package/telegram-plugin/gateway/gateway.ts +1837 -291
  46. package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
  47. package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
  48. package/telegram-plugin/gateway/represent-guard.ts +72 -0
  49. package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
  50. package/telegram-plugin/gateway/status-surface-log.ts +14 -3
  51. package/telegram-plugin/history.ts +33 -11
  52. package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
  53. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
  54. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
  55. package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
  56. package/telegram-plugin/issues-card.ts +4 -0
  57. package/telegram-plugin/model-unavailable.ts +124 -0
  58. package/telegram-plugin/narrative-dedup.ts +69 -0
  59. package/telegram-plugin/over-ping-safety-net.ts +70 -4
  60. package/telegram-plugin/package.json +3 -3
  61. package/telegram-plugin/pending-work-progress.ts +12 -0
  62. package/telegram-plugin/permission-rule.ts +32 -5
  63. package/telegram-plugin/permission-title.ts +152 -9
  64. package/telegram-plugin/quota-check.ts +13 -0
  65. package/telegram-plugin/quota-watch.ts +135 -7
  66. package/telegram-plugin/registry/turns-schema.test.ts +24 -0
  67. package/telegram-plugin/registry/turns-schema.ts +9 -0
  68. package/telegram-plugin/runtime-metrics.ts +13 -0
  69. package/telegram-plugin/session-tail.ts +96 -11
  70. package/telegram-plugin/silence-poke.ts +170 -24
  71. package/telegram-plugin/slot-banner-driver.ts +3 -0
  72. package/telegram-plugin/status-no-truncate.ts +44 -0
  73. package/telegram-plugin/status-reactions.ts +20 -3
  74. package/telegram-plugin/stream-controller.ts +4 -23
  75. package/telegram-plugin/stream-reply-handler.ts +6 -24
  76. package/telegram-plugin/streaming-metrics.ts +91 -0
  77. package/telegram-plugin/subagent-watcher.ts +212 -66
  78. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
  79. package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
  80. package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
  81. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
  82. package/telegram-plugin/tests/answer-stream.test.ts +2 -411
  83. package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
  84. package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
  85. package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
  86. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
  87. package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
  88. package/telegram-plugin/tests/demo-mask.test.ts +127 -0
  89. package/telegram-plugin/tests/draft-stream.test.ts +0 -827
  90. package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
  91. package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
  92. package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
  93. package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
  94. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
  95. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
  96. package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
  97. package/telegram-plugin/tests/feed-survival.test.ts +526 -0
  98. package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
  99. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
  100. package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
  101. package/telegram-plugin/tests/history.test.ts +60 -0
  102. package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
  103. package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
  104. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
  105. package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
  106. package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
  107. package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
  108. package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
  109. package/telegram-plugin/tests/permission-rule.test.ts +17 -0
  110. package/telegram-plugin/tests/permission-title.test.ts +206 -17
  111. package/telegram-plugin/tests/quota-watch.test.ts +252 -9
  112. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
  113. package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
  114. package/telegram-plugin/tests/represent-guard.test.ts +162 -0
  115. package/telegram-plugin/tests/session-tail.test.ts +147 -3
  116. package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
  117. package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
  118. package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
  119. package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
  120. package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
  121. package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
  122. package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
  123. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
  124. package/telegram-plugin/tests/telegram-format.test.ts +101 -6
  125. package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
  126. package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
  127. package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
  128. package/telegram-plugin/tests/tool-labels.test.ts +67 -0
  129. package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
  130. package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
  131. package/telegram-plugin/tests/welcome-text.test.ts +32 -3
  132. package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
  133. package/telegram-plugin/tool-activity-summary.ts +375 -58
  134. package/telegram-plugin/turn-liveness-floor.ts +240 -0
  135. package/telegram-plugin/uat/assertions.ts +115 -0
  136. package/telegram-plugin/uat/driver.ts +68 -0
  137. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
  138. package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
  139. package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
  140. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
  141. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
  142. package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
  143. package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
  144. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
  145. package/telegram-plugin/welcome-text.ts +13 -1
  146. package/telegram-plugin/worker-activity-feed.ts +157 -82
  147. package/telegram-plugin/draft-transport.ts +0 -122
  148. package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
  149. package/telegram-plugin/tests/draft-transport.test.ts +0 -211
@@ -0,0 +1,428 @@
1
+ // src/cli/self-improve-stop.ts
2
+ import { readFileSync as readFileSync2 } from "node:fs";
3
+ import { createConnection } from "node:net";
4
+ import { join as join2 } from "node:path";
5
+ import { homedir } from "node:os";
6
+
7
+ // src/self-improve/config.ts
8
+ function intEnv(name, def) {
9
+ const raw = process.env[name];
10
+ if (raw === undefined)
11
+ return def;
12
+ const n = Number.parseInt(raw, 10);
13
+ return Number.isFinite(n) && n >= 0 ? n : def;
14
+ }
15
+ function resolveSelfImproveConfig() {
16
+ return {
17
+ repetitionThreshold: Math.max(2, intEnv("SWITCHROOM_SELF_IMPROVE_THRESHOLD", 2)),
18
+ t1MaxChangedLines: intEnv("SWITCHROOM_SELF_IMPROVE_T1_MAX_LINES", 30),
19
+ maxAutoAppliesPerDay: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_AUTO_PER_DAY", 3),
20
+ maxOutstandingPending: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_PENDING", 5)
21
+ };
22
+ }
23
+ function selfImproveEnabled() {
24
+ return process.env.SWITCHROOM_SELF_IMPROVE !== "0";
25
+ }
26
+
27
+ // src/self-improve/gate.ts
28
+ var EVIDENCE_MAX = 160;
29
+ function excerpt(s) {
30
+ const t = s.replace(/\s+/g, " ").trim();
31
+ return t.length > EVIDENCE_MAX ? t.slice(0, EVIDENCE_MAX - 1) + "\u2026" : t;
32
+ }
33
+ var CORRECTION_PATTERNS = [
34
+ /\bno,? that'?s (?:not right|wrong|incorrect|not what)\b/i,
35
+ /\bthat'?s (?:not right|wrong|incorrect)\b/i,
36
+ /\bthat'?s not what i (?:asked|wanted|meant|said)\b/i,
37
+ /\bi (?:already )?told you\b/i,
38
+ /\bi keep telling you\b/i,
39
+ /\b(?:stop|don'?t) (?:doing|do) (?:that|this)\b/i,
40
+ /\bwhy did you\b.*\b(?:again|still)\b/i,
41
+ /\byou (?:keep|always) (?:doing|getting)\b/i,
42
+ /\bnot again\b/i,
43
+ /\byou got it wrong\b/i
44
+ ];
45
+ var DIRECTIVE_PATTERNS = [
46
+ /\b(?:always|never|every time|from now on|going forward|remember to|make sure (?:to|you))\b/i
47
+ ];
48
+ var DIRECTIVE_STOPWORDS = new Set([
49
+ "always",
50
+ "never",
51
+ "every",
52
+ "time",
53
+ "from",
54
+ "now",
55
+ "on",
56
+ "going",
57
+ "forward",
58
+ "remember",
59
+ "to",
60
+ "make",
61
+ "sure",
62
+ "you",
63
+ "the",
64
+ "a",
65
+ "an",
66
+ "please",
67
+ "and",
68
+ "do",
69
+ "don't",
70
+ "dont",
71
+ "that",
72
+ "this",
73
+ "is",
74
+ "be",
75
+ "i",
76
+ "your",
77
+ "my"
78
+ ]);
79
+ function directiveContentWords(text) {
80
+ return new Set(text.toLowerCase().replace(/[^a-z0-9\s'-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !DIRECTIVE_STOPWORDS.has(w)));
81
+ }
82
+ function jaccard(a, b) {
83
+ if (a.size === 0 || b.size === 0)
84
+ return 0;
85
+ let inter = 0;
86
+ for (const w of a)
87
+ if (b.has(w))
88
+ inter += 1;
89
+ return inter / (a.size + b.size - inter);
90
+ }
91
+ var DIRECTIVE_SIM_THRESHOLD = 0.5;
92
+ function directiveStatements(text) {
93
+ const out = [];
94
+ for (const sentence of text.split(/(?<=[.!?\n])\s+/)) {
95
+ if (DIRECTIVE_PATTERNS.some((re) => re.test(sentence))) {
96
+ const words = directiveContentWords(sentence);
97
+ if (words.size > 0)
98
+ out.push({ words, sample: sentence.trim() });
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+ var FIX_PATTERNS = [
104
+ /\bEdit\(([^)]+)\)/g,
105
+ /\bBash\(([^)]+)\)/g
106
+ ];
107
+ function fixFingerprints(text) {
108
+ const out = [];
109
+ for (const re of FIX_PATTERNS) {
110
+ re.lastIndex = 0;
111
+ let m;
112
+ while ((m = re.exec(text)) !== null) {
113
+ const inner = (m[1] ?? "").replace(/\s+/g, " ").trim().toLowerCase();
114
+ if (inner.length >= 3)
115
+ out.push(`${m[0].split("(")[0]}::${inner}`);
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+ function runGate(messages, cfg = resolveSelfImproveConfig()) {
121
+ const threshold = cfg.repetitionThreshold;
122
+ const signals = [];
123
+ if (!messages || messages.length === 0) {
124
+ return { tripped: false, signals };
125
+ }
126
+ let correctionCount = 0;
127
+ let lastCorrection = "";
128
+ const directiveClusters = [];
129
+ const fixCounts = new Map;
130
+ for (const m of messages) {
131
+ const text = typeof m.text === "string" ? m.text : "";
132
+ if (text.length === 0)
133
+ continue;
134
+ if (m.role === "user") {
135
+ if (CORRECTION_PATTERNS.some((re) => re.test(text))) {
136
+ correctionCount += 1;
137
+ lastCorrection = text;
138
+ }
139
+ for (const stmt of directiveStatements(text)) {
140
+ const hit = directiveClusters.find((c) => jaccard(c.words, stmt.words) >= DIRECTIVE_SIM_THRESHOLD);
141
+ if (hit) {
142
+ hit.count += 1;
143
+ if (stmt.sample.length > hit.sample.length)
144
+ hit.sample = stmt.sample;
145
+ } else {
146
+ directiveClusters.push({ words: stmt.words, count: 1, sample: stmt.sample });
147
+ }
148
+ }
149
+ } else if (m.role === "assistant") {
150
+ for (const fp of fixFingerprints(text)) {
151
+ fixCounts.set(fp, (fixCounts.get(fp) ?? 0) + 1);
152
+ }
153
+ }
154
+ }
155
+ if (correctionCount >= threshold) {
156
+ signals.push(buildSignal("operator-correction", `Operator pushed back ${correctionCount}\u00d7 this turn \u2014 the same correction recurred`, correctionCount, lastCorrection));
157
+ }
158
+ for (const { count, sample } of directiveClusters) {
159
+ if (count >= threshold) {
160
+ signals.push(buildSignal("directive-not-bound", `A standing rule was restated ${count}\u00d7 \u2014 it isn't binding where the action runs`, count, sample));
161
+ }
162
+ }
163
+ for (const [fp, count] of fixCounts) {
164
+ if (count >= threshold) {
165
+ signals.push(buildSignal("repeated-manual-fix", `The same manual fix was applied ${count}\u00d7 \u2014 codify it so it stops recurring`, count, fp));
166
+ }
167
+ }
168
+ return { tripped: signals.length > 0, signals };
169
+ }
170
+ function buildSignal(kind, reason, occurrences, evidence) {
171
+ return { kind, reason, occurrences, evidence: excerpt(evidence) };
172
+ }
173
+
174
+ // src/self-improve/review-prompt.ts
175
+ var REVIEW_SOURCE = "self_improve_review";
176
+ var REVIEW_BANNER = "[self-improvement review]";
177
+ function isReviewTurn(text) {
178
+ if (!text)
179
+ return false;
180
+ if (text.startsWith(REVIEW_BANNER))
181
+ return true;
182
+ const env = /^<channel\b[^>]*>/i.exec(text);
183
+ if (env) {
184
+ if (env[0].includes(`source="${REVIEW_SOURCE}"`))
185
+ return true;
186
+ if (text.slice(env[0].length).replace(/^\s+/, "").startsWith(REVIEW_BANNER)) {
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ function buildReviewPrompt(signals) {
193
+ const cfg = resolveSelfImproveConfig();
194
+ const lines = [];
195
+ lines.push(`${REVIEW_BANNER} The turn-end gate detected a learning ` + "signal \u2014 a correction or pattern that should bind on future runs. " + "Run a focused, forked review. Use ONLY memory tools (recall/retain/" + "directives) and skill read/write tools (Read/Write/Edit under your " + "own .claude/skills/, skill_* / skill-personal). Do NOT touch " + "anything else, do NOT reply to the operator, and end the turn when " + "done.");
196
+ lines.push("");
197
+ lines.push("Signals detected this turn:");
198
+ for (const s of signals) {
199
+ lines.push(` - [${s.kind}] ${s.reason}`);
200
+ if (s.evidence)
201
+ lines.push(` evidence: ${s.evidence}`);
202
+ }
203
+ lines.push("");
204
+ lines.push("Diagnose and act, strictly within these rules:");
205
+ lines.push(" 1. State the lesson in one line and the SMALLEST change that makes " + "it bind (prefer the strongest deterministic layer: a skill edit " + "over a Hindsight directive).");
206
+ lines.push(` 2. Classify the change's blast radius. T1 = a small (<= ${cfg.t1MaxChangedLines} ` + "changed lines, single file) reversible edit to a skill you ALREADY " + "own. Anything medium / shared / new-cron / new-skill / cross-agent " + "/ irreversible is T2 or T3.");
207
+ lines.push(" 3. For a T1: BEFORE editing, confirm the skill has evals/evals.json " + "and that your edit passes them without regressing baseline (the " + "skill-creator grader + aggregate_benchmark.py). Land the edit via " + "the native skill-write path (Write/Edit) only if the evals pass. " + "If the skill has no evals.json, do NOT auto-apply \u2014 treat it as T2.");
208
+ lines.push(" 4. For T2/T3: do NOT apply anything. Record a one-line pending " + "suggestion (the operator actions it later). Never auto-create a " + "cron or a new skill, never edit a shared/other-agent skill.");
209
+ lines.push("");
210
+ lines.push(`Rate limits in effect: <= ${cfg.maxAutoAppliesPerDay} auto-applies/day, ` + `<= ${cfg.maxOutstandingPending} outstanding pending suggestions. If a ` + "limit is reached, stop and leave a note rather than forcing the change.");
211
+ return lines.join(`
212
+ `);
213
+ }
214
+
215
+ // src/self-improve/review-context.ts
216
+ import {
217
+ existsSync,
218
+ mkdirSync,
219
+ readFileSync,
220
+ rmSync,
221
+ writeFileSync
222
+ } from "node:fs";
223
+ import { join } from "node:path";
224
+ var REVIEW_CONTEXT_FILE = "self-improve-review-context.json";
225
+ function contextPath(stateDir) {
226
+ return join(stateDir, REVIEW_CONTEXT_FILE);
227
+ }
228
+ function writeReviewContext(stateDir, ctx) {
229
+ if (!existsSync(stateDir)) {
230
+ mkdirSync(stateDir, { recursive: true, mode: 493 });
231
+ }
232
+ writeFileSync(contextPath(stateDir), JSON.stringify(ctx, null, 2), "utf-8");
233
+ }
234
+ function clearReviewContext(stateDir) {
235
+ try {
236
+ rmSync(contextPath(stateDir), { force: true });
237
+ } catch {}
238
+ }
239
+
240
+ // src/cli/self-improve-stop.ts
241
+ var SCAN_WINDOW = 40;
242
+ function readStdin() {
243
+ try {
244
+ return readFileSync2(0, "utf8");
245
+ } catch {
246
+ return "";
247
+ }
248
+ }
249
+ function shortHash(s) {
250
+ let h = 5381;
251
+ for (let i = 0;i < s.length; i++) {
252
+ h = (h << 5) + h + s.charCodeAt(i) | 0;
253
+ }
254
+ return (h >>> 0).toString(36);
255
+ }
256
+ function flatten(entry) {
257
+ if (!entry || typeof entry !== "object")
258
+ return null;
259
+ const e = entry;
260
+ let msg = null;
261
+ if ((e.type === "user" || e.type === "assistant") && e.message && typeof e.message === "object") {
262
+ msg = e.message;
263
+ } else if (typeof e.role === "string" && "content" in e) {
264
+ msg = e;
265
+ }
266
+ if (!msg)
267
+ return null;
268
+ const role = typeof msg.role === "string" ? msg.role : "";
269
+ if (role !== "user" && role !== "assistant")
270
+ return null;
271
+ const content = msg.content;
272
+ let text = "";
273
+ if (typeof content === "string") {
274
+ text = content;
275
+ } else if (Array.isArray(content)) {
276
+ const parts = [];
277
+ for (const part of content) {
278
+ if (!part || typeof part !== "object")
279
+ continue;
280
+ const p = part;
281
+ if (p.type === "text" && typeof p.text === "string") {
282
+ parts.push(p.text);
283
+ } else if (p.type === "tool_use" && typeof p.name === "string") {
284
+ const input = p.input ?? {};
285
+ const file = typeof input.file_path === "string" && input.file_path || typeof input.path === "string" && input.path || "";
286
+ let arg;
287
+ if (typeof input.command === "string") {
288
+ arg = input.command;
289
+ } else if (file) {
290
+ const editish = [
291
+ typeof input.old_string === "string" ? input.old_string : "",
292
+ typeof input.new_string === "string" ? input.new_string : "",
293
+ typeof input.content === "string" ? input.content : "",
294
+ input.edits !== undefined ? JSON.stringify(input.edits) : ""
295
+ ];
296
+ arg = editish.some((x) => x.length > 0) ? `${file} #${shortHash(editish.join(" "))}` : file;
297
+ } else {
298
+ arg = "";
299
+ }
300
+ parts.push(`${p.name}(${arg})`);
301
+ }
302
+ }
303
+ text = parts.join(" ");
304
+ }
305
+ return { role, text };
306
+ }
307
+ function readTranscript(path) {
308
+ if (!path)
309
+ return [];
310
+ let raw;
311
+ try {
312
+ raw = readFileSync2(path, "utf8");
313
+ } catch {
314
+ return [];
315
+ }
316
+ const out = [];
317
+ for (const line of raw.split(`
318
+ `)) {
319
+ const t = line.trim();
320
+ if (!t)
321
+ continue;
322
+ try {
323
+ const m = flatten(JSON.parse(t));
324
+ if (m && m.text)
325
+ out.push(m);
326
+ } catch {}
327
+ }
328
+ return out.length > SCAN_WINDOW ? out.slice(-SCAN_WINDOW) : out;
329
+ }
330
+ function resolveStateDir() {
331
+ return process.env.TELEGRAM_STATE_DIR ?? join2(homedir(), ".claude", "channels", "telegram");
332
+ }
333
+ function resolveSocketPath() {
334
+ if (process.env.SWITCHROOM_GATEWAY_SOCKET) {
335
+ return process.env.SWITCHROOM_GATEWAY_SOCKET;
336
+ }
337
+ const stateDir = process.env.TELEGRAM_STATE_DIR ?? join2(homedir(), ".claude", "channels", "telegram");
338
+ return join2(stateDir, "gateway.sock");
339
+ }
340
+ function resolveChatId() {
341
+ return process.env.SWITCHROOM_SELF_IMPROVE_CHAT_ID ?? process.env.SWITCHROOM_DEFAULT_CHAT_ID ?? "self-improve";
342
+ }
343
+ function injectReview(agentName, text, sessionId) {
344
+ const socketPath = resolveSocketPath();
345
+ const chatId = resolveChatId();
346
+ const now = Date.now();
347
+ const envelope = {
348
+ type: "inject_inbound",
349
+ agentName,
350
+ inbound: {
351
+ type: "inbound",
352
+ chatId,
353
+ messageId: now,
354
+ user: "self-improve",
355
+ userId: 0,
356
+ ts: now,
357
+ text,
358
+ meta: {
359
+ source: REVIEW_SOURCE,
360
+ triggering_session: sessionId
361
+ }
362
+ }
363
+ };
364
+ let settled = false;
365
+ const sock = createConnection(socketPath);
366
+ const done = () => {
367
+ if (settled)
368
+ return;
369
+ settled = true;
370
+ try {
371
+ sock.end();
372
+ } catch {}
373
+ try {
374
+ sock.destroy();
375
+ } catch {}
376
+ process.exit(0);
377
+ };
378
+ const timer = setTimeout(done, 3000);
379
+ timer.unref?.();
380
+ sock.on("connect", () => {
381
+ try {
382
+ sock.write(JSON.stringify(envelope) + `
383
+ `, () => done());
384
+ } catch {
385
+ done();
386
+ }
387
+ });
388
+ sock.on("error", () => done());
389
+ }
390
+ function main() {
391
+ if (!selfImproveEnabled())
392
+ process.exit(0);
393
+ const raw = readStdin().trim();
394
+ if (!raw)
395
+ process.exit(0);
396
+ let event;
397
+ try {
398
+ event = JSON.parse(raw);
399
+ } catch {
400
+ process.exit(0);
401
+ }
402
+ const transcriptPath = typeof event.transcript_path === "string" ? event.transcript_path : "";
403
+ const sessionId = typeof event.session_id === "string" ? event.session_id : "unknown";
404
+ const messages = readTranscript(transcriptPath);
405
+ const lastUser = [...messages].reverse().find((m) => m.role === "user");
406
+ if (lastUser && isReviewTurn(lastUser.text)) {
407
+ clearReviewContext(resolveStateDir());
408
+ process.exit(0);
409
+ }
410
+ const scanMessages = messages.filter((m) => !isReviewTurn(m.text));
411
+ const gate = runGate(scanMessages);
412
+ if (!gate.tripped)
413
+ process.exit(0);
414
+ const agentName = process.env.SWITCHROOM_AGENT_NAME ?? "";
415
+ if (!agentName)
416
+ process.exit(0);
417
+ try {
418
+ writeReviewContext(resolveStateDir(), {
419
+ created_at: new Date().toISOString(),
420
+ triggering_session: sessionId,
421
+ chat_id: resolveChatId(),
422
+ signals: gate.signals
423
+ });
424
+ } catch {}
425
+ const prompt = buildReviewPrompt(gate.signals);
426
+ injectReview(agentName, prompt, sessionId);
427
+ }
428
+ main();