switchroom 0.15.44 → 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 (150) 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 +3249 -1241
  10. package/dist/cli/ui/index.html +1 -1
  11. package/dist/host-control/main.js +2833 -355
  12. package/dist/vault/approvals/kernel-server.js +7482 -7439
  13. package/dist/vault/broker/server.js +11315 -11272
  14. package/examples/minimal.yaml +1 -0
  15. package/examples/switchroom.yaml +1 -0
  16. package/package.json +3 -3
  17. package/profiles/_base/start.sh.hbs +88 -1
  18. package/profiles/_shared/execution-discipline.md.hbs +18 -0
  19. package/profiles/default/CLAUDE.md.hbs +3 -22
  20. package/telegram-plugin/.claude-plugin/plugin.json +2 -2
  21. package/telegram-plugin/answer-stream-flag.ts +12 -49
  22. package/telegram-plugin/answer-stream.ts +5 -150
  23. package/telegram-plugin/auth-snapshot-format.ts +280 -48
  24. package/telegram-plugin/auto-fallback-fleet.ts +44 -1
  25. package/telegram-plugin/context-exhaustion.ts +12 -0
  26. package/telegram-plugin/demo-mask.ts +154 -0
  27. package/telegram-plugin/dist/bridge/bridge.js +167 -124
  28. package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
  29. package/telegram-plugin/dist/server.js +215 -172
  30. package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
  31. package/telegram-plugin/draft-stream.ts +47 -410
  32. package/telegram-plugin/final-answer-detect.ts +17 -12
  33. package/telegram-plugin/fleet-fallback-resume.ts +131 -0
  34. package/telegram-plugin/format.ts +56 -19
  35. package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
  36. package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
  37. package/telegram-plugin/gateway/auth-command.ts +70 -14
  38. package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
  39. package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
  40. package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
  41. package/telegram-plugin/gateway/current-turn-map.ts +188 -0
  42. package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
  43. package/telegram-plugin/gateway/effort-command.ts +8 -3
  44. package/telegram-plugin/gateway/emission-authority.ts +369 -0
  45. package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
  46. package/telegram-plugin/gateway/gateway.ts +1837 -291
  47. package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
  48. package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
  49. package/telegram-plugin/gateway/represent-guard.ts +72 -0
  50. package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
  51. package/telegram-plugin/gateway/status-surface-log.ts +14 -3
  52. package/telegram-plugin/history.ts +33 -11
  53. package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
  54. package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
  55. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
  56. package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
  57. package/telegram-plugin/issues-card.ts +4 -0
  58. package/telegram-plugin/model-unavailable.ts +124 -0
  59. package/telegram-plugin/narrative-dedup.ts +69 -0
  60. package/telegram-plugin/over-ping-safety-net.ts +70 -4
  61. package/telegram-plugin/package.json +3 -3
  62. package/telegram-plugin/pending-work-progress.ts +12 -0
  63. package/telegram-plugin/permission-rule.ts +32 -5
  64. package/telegram-plugin/permission-title.ts +152 -9
  65. package/telegram-plugin/quota-check.ts +13 -0
  66. package/telegram-plugin/quota-watch.ts +135 -7
  67. package/telegram-plugin/registry/turns-schema.test.ts +24 -0
  68. package/telegram-plugin/registry/turns-schema.ts +9 -0
  69. package/telegram-plugin/runtime-metrics.ts +13 -0
  70. package/telegram-plugin/session-tail.ts +96 -11
  71. package/telegram-plugin/silence-poke.ts +170 -24
  72. package/telegram-plugin/slot-banner-driver.ts +3 -0
  73. package/telegram-plugin/status-no-truncate.ts +44 -0
  74. package/telegram-plugin/status-reactions.ts +20 -3
  75. package/telegram-plugin/stream-controller.ts +4 -23
  76. package/telegram-plugin/stream-reply-handler.ts +6 -24
  77. package/telegram-plugin/streaming-metrics.ts +91 -0
  78. package/telegram-plugin/subagent-watcher.ts +212 -66
  79. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
  80. package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
  81. package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
  82. package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
  83. package/telegram-plugin/tests/answer-stream.test.ts +2 -411
  84. package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
  85. package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
  86. package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
  87. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
  88. package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
  89. package/telegram-plugin/tests/demo-mask.test.ts +127 -0
  90. package/telegram-plugin/tests/draft-stream.test.ts +0 -827
  91. package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
  92. package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
  93. package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
  94. package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
  95. package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
  96. package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
  97. package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
  98. package/telegram-plugin/tests/feed-survival.test.ts +526 -0
  99. package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
  100. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
  101. package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
  102. package/telegram-plugin/tests/history.test.ts +60 -0
  103. package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
  104. package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
  105. package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
  106. package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
  107. package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
  108. package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
  109. package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
  110. package/telegram-plugin/tests/permission-rule.test.ts +17 -0
  111. package/telegram-plugin/tests/permission-title.test.ts +206 -17
  112. package/telegram-plugin/tests/quota-watch.test.ts +252 -9
  113. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
  114. package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
  115. package/telegram-plugin/tests/represent-guard.test.ts +162 -0
  116. package/telegram-plugin/tests/session-tail.test.ts +147 -3
  117. package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
  118. package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
  119. package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
  120. package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
  121. package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
  122. package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
  123. package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
  124. package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
  125. package/telegram-plugin/tests/telegram-format.test.ts +101 -6
  126. package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
  127. package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
  128. package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
  129. package/telegram-plugin/tests/tool-labels.test.ts +67 -0
  130. package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
  131. package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
  132. package/telegram-plugin/tests/welcome-text.test.ts +32 -3
  133. package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
  134. package/telegram-plugin/tool-activity-summary.ts +375 -58
  135. package/telegram-plugin/turn-liveness-floor.ts +240 -0
  136. package/telegram-plugin/uat/assertions.ts +115 -0
  137. package/telegram-plugin/uat/driver.ts +68 -0
  138. package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
  139. package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
  140. package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
  141. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
  142. package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
  143. package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
  144. package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
  145. package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
  146. package/telegram-plugin/welcome-text.ts +13 -1
  147. package/telegram-plugin/worker-activity-feed.ts +157 -82
  148. package/telegram-plugin/draft-transport.ts +0 -122
  149. package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
  150. package/telegram-plugin/tests/draft-transport.test.ts +0 -211
@@ -0,0 +1,626 @@
1
+ // src/cli/self-improve-apply-guard-pretool.ts
2
+ import { readFileSync as readFileSync6 } from "node:fs";
3
+ import { join as join6 } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ // src/self-improve/config.ts
7
+ function intEnv(name, def) {
8
+ const raw = process.env[name];
9
+ if (raw === undefined)
10
+ return def;
11
+ const n = Number.parseInt(raw, 10);
12
+ return Number.isFinite(n) && n >= 0 ? n : def;
13
+ }
14
+ function resolveSelfImproveConfig() {
15
+ return {
16
+ repetitionThreshold: Math.max(2, intEnv("SWITCHROOM_SELF_IMPROVE_THRESHOLD", 2)),
17
+ t1MaxChangedLines: intEnv("SWITCHROOM_SELF_IMPROVE_T1_MAX_LINES", 30),
18
+ maxAutoAppliesPerDay: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_AUTO_PER_DAY", 3),
19
+ maxOutstandingPending: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_PENDING", 5)
20
+ };
21
+ }
22
+ function selfImproveEnabled() {
23
+ return process.env.SWITCHROOM_SELF_IMPROVE !== "0";
24
+ }
25
+
26
+ // src/self-improve/review-context.ts
27
+ import {
28
+ existsSync,
29
+ mkdirSync,
30
+ readFileSync,
31
+ rmSync,
32
+ writeFileSync
33
+ } from "node:fs";
34
+ import { join } from "node:path";
35
+ var REVIEW_CONTEXT_FILE = "self-improve-review-context.json";
36
+ function contextPath(stateDir) {
37
+ return join(stateDir, REVIEW_CONTEXT_FILE);
38
+ }
39
+ function readReviewContext(stateDir) {
40
+ const p = contextPath(stateDir);
41
+ if (!existsSync(p))
42
+ return null;
43
+ try {
44
+ const parsed = JSON.parse(readFileSync(p, "utf-8"));
45
+ if (parsed && typeof parsed.created_at === "string") {
46
+ if (!Array.isArray(parsed.signals))
47
+ parsed.signals = [];
48
+ return parsed;
49
+ }
50
+ } catch {}
51
+ return null;
52
+ }
53
+
54
+ // src/self-improve/apply-guard.ts
55
+ import { existsSync as existsSync4, lstatSync, readFileSync as readFileSync4, statSync } from "node:fs";
56
+ import { join as join4, resolve, sep } from "node:path";
57
+
58
+ // src/self-improve/tier-router.ts
59
+ function classifyTier(candidate, cfg = resolveSelfImproveConfig()) {
60
+ if (!candidate.proposedChange || candidate.proposedChange.trim() === "") {
61
+ return { tier: "T0", reason: "no actionable change proposed" };
62
+ }
63
+ if (candidate.irreversible) {
64
+ return { tier: "T3", reason: "irreversible change \u2014 operator must decide" };
65
+ }
66
+ if (candidate.crossAgent) {
67
+ return {
68
+ tier: "T3",
69
+ reason: "cross-agent change \u2014 operator must decide (no self-escalation)"
70
+ };
71
+ }
72
+ if (candidate.touchesCron) {
73
+ return {
74
+ tier: "T3",
75
+ reason: "creates/edits a cron \u2014 never self-served (on-leash)"
76
+ };
77
+ }
78
+ if (candidate.createsNewSkill) {
79
+ return {
80
+ tier: "T3",
81
+ reason: "creates a new skill \u2014 proposed, never auto-created"
82
+ };
83
+ }
84
+ if (!candidate.skillSlug || candidate.ownsSkill !== true) {
85
+ return {
86
+ tier: "T2",
87
+ reason: candidate.skillSlug ? `targets skill "${candidate.skillSlug}" the agent does not own \u2014 propose, don't auto-apply` : "no own-skill target \u2014 surface as a one-tap suggestion"
88
+ };
89
+ }
90
+ if (candidate.multiFile === true) {
91
+ return {
92
+ tier: "T2",
93
+ reason: "touches more than one skill file \u2014 over the T1 single-file bound"
94
+ };
95
+ }
96
+ const lines = candidate.changedLines ?? 0;
97
+ if (lines > cfg.t1MaxChangedLines) {
98
+ return {
99
+ tier: "T2",
100
+ reason: `${lines} changed lines exceeds the T1 cap of ${cfg.t1MaxChangedLines}`
101
+ };
102
+ }
103
+ return {
104
+ tier: "T1",
105
+ reason: `small (${lines} line${lines === 1 ? "" : "s"}) reversible edit to owned skill "${candidate.skillSlug}"`
106
+ };
107
+ }
108
+
109
+ // src/self-improve/eval-gate.ts
110
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
111
+ import { join as join2 } from "node:path";
112
+ function evalsJsonPath(skillDir) {
113
+ return join2(skillDir, "evals", "evals.json");
114
+ }
115
+ function skillHasEvals(skillDir) {
116
+ const p = evalsJsonPath(skillDir);
117
+ if (!existsSync2(p))
118
+ return false;
119
+ try {
120
+ const parsed = JSON.parse(readFileSync2(p, "utf-8"));
121
+ return Array.isArray(parsed.evals) && parsed.evals.length > 0;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+ var CANDIDATE_CONFIGS = ["with_skill", "candidate", "new_skill", "after"];
127
+ var BASELINE_CONFIGS = ["without_skill", "baseline", "old_skill", "before"];
128
+ function pickPassRate(summary, names) {
129
+ for (const n of names) {
130
+ const c = summary[n];
131
+ if (c && c.pass_rate && typeof c.pass_rate.mean === "number") {
132
+ return c.pass_rate.mean;
133
+ }
134
+ }
135
+ return;
136
+ }
137
+ function evalVerdict(benchmarkJson, _cfg = resolveSelfImproveConfig()) {
138
+ let bench;
139
+ try {
140
+ bench = JSON.parse(readFileSync2(benchmarkJson, "utf-8"));
141
+ } catch (err) {
142
+ return { pass: false, reason: `unreadable benchmark.json: ${err.message}` };
143
+ }
144
+ const summary = bench.run_summary ?? {};
145
+ const candidate = pickPassRate(summary, CANDIDATE_CONFIGS);
146
+ if (candidate === undefined) {
147
+ return { pass: false, reason: "no candidate pass-rate in benchmark.json" };
148
+ }
149
+ const baseline = pickPassRate(summary, BASELINE_CONFIGS);
150
+ const floor = floatEnv("SWITCHROOM_SELF_IMPROVE_EVAL_FLOOR", 1);
151
+ const regressBand = floatEnv("SWITCHROOM_SELF_IMPROVE_EVAL_REGRESS_BAND", 0);
152
+ if (candidate < floor) {
153
+ return {
154
+ pass: false,
155
+ reason: `candidate pass-rate ${candidate.toFixed(2)} below floor ${floor.toFixed(2)}`,
156
+ candidatePassRate: candidate,
157
+ baselinePassRate: baseline
158
+ };
159
+ }
160
+ if (baseline !== undefined && candidate < baseline - regressBand) {
161
+ return {
162
+ pass: false,
163
+ reason: `regression: candidate ${candidate.toFixed(2)} < baseline ${baseline.toFixed(2)} (band ${regressBand.toFixed(2)})`,
164
+ candidatePassRate: candidate,
165
+ baselinePassRate: baseline
166
+ };
167
+ }
168
+ return {
169
+ pass: true,
170
+ candidatePassRate: candidate,
171
+ baselinePassRate: baseline ?? candidate
172
+ };
173
+ }
174
+ function floatEnv(name, def) {
175
+ const raw = process.env[name];
176
+ if (raw === undefined)
177
+ return def;
178
+ const n = Number.parseFloat(raw);
179
+ return Number.isFinite(n) ? n : def;
180
+ }
181
+
182
+ // src/self-improve/rate-limit.ts
183
+ import {
184
+ closeSync,
185
+ existsSync as existsSync3,
186
+ mkdirSync as mkdirSync2,
187
+ openSync,
188
+ readFileSync as readFileSync3,
189
+ writeSync
190
+ } from "node:fs";
191
+ import { join as join3 } from "node:path";
192
+ var APPLIES_FILE = "self-improve-applies.jsonl";
193
+ function appliesPath(stateDir) {
194
+ return join3(stateDir, APPLIES_FILE);
195
+ }
196
+ function utcDay(ms) {
197
+ return new Date(ms).toISOString().slice(0, 10);
198
+ }
199
+ function appliesToday(stateDir, now = Date.now) {
200
+ const p = appliesPath(stateDir);
201
+ if (!existsSync3(p))
202
+ return 0;
203
+ let raw;
204
+ try {
205
+ raw = readFileSync3(p, "utf-8");
206
+ } catch {
207
+ return 0;
208
+ }
209
+ const today = utcDay(now());
210
+ let count = 0;
211
+ for (const line of raw.split(`
212
+ `)) {
213
+ if (!line.trim())
214
+ continue;
215
+ try {
216
+ const rec = JSON.parse(line);
217
+ if (rec.day === today)
218
+ count += 1;
219
+ } catch {}
220
+ }
221
+ return count;
222
+ }
223
+ function recordAutoApply(stateDir, now = Date.now) {
224
+ if (!existsSync3(stateDir)) {
225
+ mkdirSync2(stateDir, { recursive: true, mode: 493 });
226
+ }
227
+ const ms = now();
228
+ const fd = openSync(appliesPath(stateDir), "a");
229
+ try {
230
+ writeSync(fd, JSON.stringify({ ts: ms, day: utcDay(ms) }) + `
231
+ `);
232
+ } finally {
233
+ closeSync(fd);
234
+ }
235
+ }
236
+
237
+ // src/self-improve/apply-guard.ts
238
+ var SKILLS_SEGMENT = "/.claude/skills/";
239
+ var BENCHMARK_SUBDIR = "self-improve-benchmarks";
240
+ function benchmarkJsonForSkill(stateDir, slug) {
241
+ return join4(stateDir, BENCHMARK_SUBDIR, slug, "benchmark.json");
242
+ }
243
+ function parseSkillTarget(filePath) {
244
+ if (typeof filePath !== "string" || filePath.length === 0)
245
+ return null;
246
+ const segIdx = filePath.indexOf(SKILLS_SEGMENT);
247
+ if (segIdx < 0)
248
+ return null;
249
+ const after = filePath.slice(segIdx + SKILLS_SEGMENT.length);
250
+ const segs = after.split("/").filter((s) => s.length > 0);
251
+ if (segs.length < 2)
252
+ return null;
253
+ const slug = segs[0];
254
+ const relPath = segs.slice(1).join("/");
255
+ const skillDir = filePath.slice(0, segIdx + SKILLS_SEGMENT.length) + slug;
256
+ return { skillDir, slug, relPath };
257
+ }
258
+ var SAFE_SLUG = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
259
+ function skillTargetEscapes(target) {
260
+ if (!SAFE_SLUG.test(target.slug))
261
+ return true;
262
+ for (const s of target.relPath.split("/")) {
263
+ if (s === "" || s === "." || s === ".." || s.includes("\x00"))
264
+ return true;
265
+ }
266
+ const base = resolve(target.skillDir);
267
+ const resolved = resolve(target.skillDir, target.relPath);
268
+ return resolved !== base && !resolved.startsWith(base + sep);
269
+ }
270
+ function ownsSkill(skillDir) {
271
+ try {
272
+ const st = lstatSync(skillDir);
273
+ if (st.isSymbolicLink())
274
+ return false;
275
+ return st.isDirectory();
276
+ } catch {
277
+ return false;
278
+ }
279
+ }
280
+ function estimateChangedLines(toolName, input, filePath) {
281
+ const splitLines = (s) => s.length === 0 ? [] : s.replace(/\r\n/g, `
282
+ `).split(`
283
+ `);
284
+ const multisetDiff = (before, after) => {
285
+ const counts = new Map;
286
+ for (const l of before)
287
+ counts.set(l, (counts.get(l) ?? 0) + 1);
288
+ let added = 0;
289
+ for (const l of after) {
290
+ const c = counts.get(l) ?? 0;
291
+ if (c > 0)
292
+ counts.set(l, c - 1);
293
+ else
294
+ added += 1;
295
+ }
296
+ let removed = 0;
297
+ for (const c of counts.values())
298
+ removed += c;
299
+ return added + removed;
300
+ };
301
+ const readCurrent = () => {
302
+ try {
303
+ return readFileSync4(filePath, "utf-8");
304
+ } catch {
305
+ return "";
306
+ }
307
+ };
308
+ if (toolName === "Write") {
309
+ const content = typeof input.content === "string" ? input.content : "";
310
+ return multisetDiff(splitLines(readCurrent()), splitLines(content));
311
+ }
312
+ if (toolName === "Edit") {
313
+ const oldS = typeof input.old_string === "string" ? input.old_string : "";
314
+ const newS = typeof input.new_string === "string" ? input.new_string : "";
315
+ return multisetDiff(splitLines(oldS), splitLines(newS));
316
+ }
317
+ if (toolName === "MultiEdit") {
318
+ const edits = Array.isArray(input.edits) ? input.edits : [];
319
+ let total = 0;
320
+ for (const e of edits) {
321
+ if (!e || typeof e !== "object")
322
+ continue;
323
+ const ed = e;
324
+ const oldS = typeof ed.old_string === "string" ? ed.old_string : "";
325
+ const newS = typeof ed.new_string === "string" ? ed.new_string : "";
326
+ total += multisetDiff(splitLines(oldS), splitLines(newS));
327
+ }
328
+ return total;
329
+ }
330
+ return 0;
331
+ }
332
+ function downgradeCandidate(target, owns, changedLines, lesson) {
333
+ return {
334
+ lesson,
335
+ proposedChange: `Edit ${target.relPath} in skill "${target.slug}" (auto-apply refused \u2014 see reason)`,
336
+ skillSlug: target.slug,
337
+ ownsSkill: owns,
338
+ changedLines,
339
+ multiFile: false
340
+ };
341
+ }
342
+ function decideApply(p) {
343
+ const cfg = p.cfg ?? resolveSelfImproveConfig();
344
+ const now = p.now ?? Date.now;
345
+ const target = parseSkillTarget(p.filePath);
346
+ if (!target) {
347
+ return {
348
+ action: "block",
349
+ downgradeTier: "T2",
350
+ reason: "write target is not an own-skill bundle file",
351
+ candidate: {
352
+ lesson: "self-improve review attempted a non-skill write",
353
+ proposedChange: p.filePath,
354
+ multiFile: false
355
+ }
356
+ };
357
+ }
358
+ if (skillTargetEscapes(target)) {
359
+ return {
360
+ action: "block",
361
+ downgradeTier: "T2",
362
+ reason: `skill write path escapes the bundle (slug="${target.slug}", rel="${target.relPath}") \u2014 refused`,
363
+ candidate: {
364
+ lesson: `Bind the recurring lesson into skill "${target.slug}"`,
365
+ proposedChange: `Edit ${target.relPath} in skill "${target.slug}" (path escapes bundle \u2014 refused)`,
366
+ skillSlug: target.slug,
367
+ ownsSkill: false,
368
+ changedLines: 0,
369
+ multiFile: false
370
+ }
371
+ };
372
+ }
373
+ const owns = (p.ownsSkillFn ?? ownsSkill)(target.skillDir);
374
+ const changedLines = estimateChangedLines(p.toolName, p.input, p.filePath);
375
+ const lesson = `Bind the recurring lesson into skill "${target.slug}"`;
376
+ const routed = classifyTier({
377
+ lesson,
378
+ proposedChange: "skill edit",
379
+ skillSlug: target.slug,
380
+ ownsSkill: owns,
381
+ changedLines,
382
+ multiFile: false
383
+ }, cfg);
384
+ if (routed.tier !== "T1") {
385
+ return {
386
+ action: "block",
387
+ downgradeTier: routed.tier === "T3" ? "T3" : "T2",
388
+ reason: `not a T1 auto-apply: ${routed.reason}`,
389
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
390
+ };
391
+ }
392
+ if (!skillHasEvals(target.skillDir)) {
393
+ return {
394
+ action: "block",
395
+ downgradeTier: "T2",
396
+ reason: `skill "${target.slug}" has no evals/evals.json \u2014 can't measure the edit; downgraded to a proposal`,
397
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
398
+ };
399
+ }
400
+ const benchPath = benchmarkJsonForSkill(p.stateDir, target.slug);
401
+ if (!existsSync4(benchPath)) {
402
+ return {
403
+ action: "block",
404
+ downgradeTier: "T2",
405
+ reason: `no eval result for skill "${target.slug}" \u2014 the per-skill eval did not run/pass; downgraded to a proposal`,
406
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
407
+ };
408
+ }
409
+ if (typeof p.freshAfterMs === "number") {
410
+ let mtime = 0;
411
+ try {
412
+ mtime = statSync(benchPath).mtimeMs;
413
+ } catch {
414
+ mtime = 0;
415
+ }
416
+ if (mtime < p.freshAfterMs) {
417
+ return {
418
+ action: "block",
419
+ downgradeTier: "T2",
420
+ reason: `eval result for "${target.slug}" is stale (predates this review) \u2014 re-run evals; downgraded to a proposal`,
421
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
422
+ };
423
+ }
424
+ }
425
+ const verdict = evalVerdict(benchPath, cfg);
426
+ if (!verdict.pass) {
427
+ return {
428
+ action: "block",
429
+ downgradeTier: "T2",
430
+ reason: `eval gate blocked the edit to "${target.slug}": ${verdict.reason}`,
431
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
432
+ };
433
+ }
434
+ const used = appliesToday(p.stateDir, now);
435
+ if (used >= cfg.maxAutoAppliesPerDay) {
436
+ return {
437
+ action: "block",
438
+ downgradeTier: "T2",
439
+ reason: `daily auto-apply cap reached (${used}/${cfg.maxAutoAppliesPerDay}) \u2014 downgraded to a proposal`,
440
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
441
+ };
442
+ }
443
+ return {
444
+ action: "allow",
445
+ tier: "T1",
446
+ changedLines,
447
+ candidatePassRate: verdict.candidatePassRate,
448
+ baselinePassRate: verdict.baselinePassRate,
449
+ reason: `verified T1: ${changedLines} changed line${changedLines === 1 ? "" : "s"} to owned skill "${target.slug}", evals ${verdict.candidatePassRate.toFixed(2)} \u2265 baseline ${verdict.baselinePassRate.toFixed(2)}, ${used + 1}/${cfg.maxAutoAppliesPerDay} today`
450
+ };
451
+ }
452
+
453
+ // src/self-improve/pending-queue.ts
454
+ import {
455
+ closeSync as closeSync2,
456
+ existsSync as existsSync5,
457
+ mkdirSync as mkdirSync3,
458
+ openSync as openSync2,
459
+ readFileSync as readFileSync5,
460
+ writeSync as writeSync2
461
+ } from "node:fs";
462
+ import { join as join5 } from "node:path";
463
+ import { randomUUID } from "node:crypto";
464
+ var PENDING_FILE = "self-improve-pending.jsonl";
465
+ function pendingPath(stateDir) {
466
+ return join5(stateDir, PENDING_FILE);
467
+ }
468
+ function readPending(stateDir) {
469
+ const p = pendingPath(stateDir);
470
+ if (!existsSync5(p))
471
+ return [];
472
+ let raw;
473
+ try {
474
+ raw = readFileSync5(p, "utf-8");
475
+ } catch {
476
+ return [];
477
+ }
478
+ const out = [];
479
+ for (const line of raw.split(`
480
+ `)) {
481
+ if (!line.trim())
482
+ continue;
483
+ try {
484
+ const parsed = JSON.parse(line);
485
+ if (parsed && typeof parsed.id === "string" && parsed.tier) {
486
+ out.push(parsed);
487
+ }
488
+ } catch {}
489
+ }
490
+ return out;
491
+ }
492
+ function outstandingCount(stateDir) {
493
+ return readPending(stateDir).filter((s) => s.actioned !== true).length;
494
+ }
495
+ function enqueuePending(stateDir, input, opts = {}) {
496
+ const cfg = resolveSelfImproveConfig();
497
+ const cap = opts.cap ?? cfg.maxOutstandingPending;
498
+ const now = opts.now ?? Date.now;
499
+ ensureDir(stateDir);
500
+ const outstanding = outstandingCount(stateDir);
501
+ if (outstanding >= cap) {
502
+ return { ok: false, reason: "cap-reached", outstanding, cap };
503
+ }
504
+ const suggestion = {
505
+ id: randomUUID(),
506
+ created_at: new Date(now()).toISOString(),
507
+ ...input
508
+ };
509
+ const fd = openSync2(pendingPath(stateDir), "a");
510
+ try {
511
+ writeSync2(fd, JSON.stringify(suggestion) + `
512
+ `);
513
+ } finally {
514
+ closeSync2(fd);
515
+ }
516
+ return { ok: true, suggestion };
517
+ }
518
+ function ensureDir(stateDir) {
519
+ if (!existsSync5(stateDir)) {
520
+ mkdirSync3(stateDir, { recursive: true, mode: 493 });
521
+ }
522
+ }
523
+
524
+ // src/cli/self-improve-apply-guard-pretool.ts
525
+ var EDIT_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
526
+ function readStdin() {
527
+ try {
528
+ return readFileSync6(0, "utf8");
529
+ } catch {
530
+ return "";
531
+ }
532
+ }
533
+ function allow() {
534
+ process.exit(0);
535
+ }
536
+ function block(reason) {
537
+ const safe = String(reason).replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 300);
538
+ process.stdout.write(JSON.stringify({ decision: "block", reason: safe }));
539
+ process.exit(0);
540
+ }
541
+ function resolveStateDir() {
542
+ return process.env.TELEGRAM_STATE_DIR ?? join6(homedir(), ".claude", "channels", "telegram");
543
+ }
544
+ function main() {
545
+ if (!selfImproveEnabled())
546
+ allow();
547
+ const raw = readStdin().trim();
548
+ if (!raw)
549
+ allow();
550
+ let event;
551
+ try {
552
+ event = JSON.parse(raw);
553
+ } catch {
554
+ allow();
555
+ }
556
+ const toolName = typeof event.tool_name === "string" ? event.tool_name : "";
557
+ if (!EDIT_TOOLS.has(toolName))
558
+ allow();
559
+ const input = event.tool_input && typeof event.tool_input === "object" ? event.tool_input : {};
560
+ const filePath = typeof input.file_path === "string" ? input.file_path : "";
561
+ if (!filePath)
562
+ allow();
563
+ if (!parseSkillTarget(filePath))
564
+ allow();
565
+ const stateDir = resolveStateDir();
566
+ let ctx = null;
567
+ try {
568
+ ctx = readReviewContext(stateDir);
569
+ } catch {
570
+ ctx = null;
571
+ }
572
+ if (!ctx)
573
+ allow();
574
+ let freshAfterMs;
575
+ const t = Date.parse(ctx.created_at);
576
+ if (Number.isFinite(t))
577
+ freshAfterMs = t;
578
+ let decision;
579
+ try {
580
+ decision = decideApply({
581
+ toolName,
582
+ input,
583
+ filePath,
584
+ stateDir,
585
+ freshAfterMs
586
+ });
587
+ } catch {
588
+ safeDowngrade(stateDir, ctx, filePath, "apply-guard internal error");
589
+ block("self-improve apply-guard: internal error evaluating the edit \u2014 " + "downgraded to a pending proposal, write blocked (fail-closed).");
590
+ }
591
+ if (decision.action === "allow") {
592
+ try {
593
+ recordAutoApply(stateDir);
594
+ } catch {}
595
+ allow();
596
+ }
597
+ let capNote = "";
598
+ try {
599
+ const q = enqueuePending(stateDir, {
600
+ tier: decision.downgradeTier,
601
+ lesson: decision.candidate.lesson,
602
+ proposed_change: decision.candidate.proposedChange,
603
+ tier_reason: decision.reason,
604
+ skill_slug: decision.candidate.skillSlug,
605
+ triggered_by: (ctx.signals ?? []).map((s) => s.kind)
606
+ });
607
+ if (!q.ok) {
608
+ capNote = ` (pending queue full ${q.outstanding}/${q.cap} \u2014 proposal not recorded)`;
609
+ }
610
+ } catch {}
611
+ block(`self-improve apply-guard: ${decision.reason}${capNote}`);
612
+ }
613
+ function safeDowngrade(stateDir, ctx, filePath, reason) {
614
+ try {
615
+ const target = parseSkillTarget(filePath);
616
+ enqueuePending(stateDir, {
617
+ tier: "T2",
618
+ lesson: target ? `Bind the recurring lesson into skill "${target.slug}"` : "self-improve review edit (guard error)",
619
+ proposed_change: filePath,
620
+ tier_reason: reason,
621
+ skill_slug: target?.slug,
622
+ triggered_by: (ctx.signals ?? []).map((s) => s.kind)
623
+ });
624
+ } catch {}
625
+ }
626
+ main();