switchroom 0.20.10 → 0.20.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.
Files changed (52) hide show
  1. package/dist/agent-scheduler/index.js +65 -2
  2. package/dist/auth-broker/index.js +185 -23
  3. package/dist/cli/notion-write-pretool.mjs +65 -2
  4. package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
  5. package/dist/cli/self-improve-stop.mjs +889 -7
  6. package/dist/cli/skill-validate-pretool.mjs +82 -3
  7. package/dist/cli/switchroom.js +3669 -2107
  8. package/dist/host-control/main.js +67 -4
  9. package/dist/vault/approvals/kernel-server.js +66 -3
  10. package/dist/vault/broker/server.js +66 -3
  11. package/package.json +1 -1
  12. package/profiles/_shared/agent-self-service.md.hbs +15 -22
  13. package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
  14. package/profiles/_shared/dev-protocol.md.hbs +1 -1
  15. package/profiles/_shared/execution-discipline.md.hbs +4 -4
  16. package/profiles/_shared/vault-protocol.md.hbs +2 -18
  17. package/profiles/default/CLAUDE.md.hbs +3 -5
  18. package/telegram-plugin/auto-fallback-fleet.ts +37 -2
  19. package/telegram-plugin/dist/gateway/gateway.js +1405 -918
  20. package/telegram-plugin/fallback-card-collapse.ts +1 -0
  21. package/telegram-plugin/gateway/auth-command.ts +11 -1
  22. package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
  23. package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
  24. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
  25. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
  26. package/telegram-plugin/gateway/gateway.ts +85 -90
  27. package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
  28. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  29. package/telegram-plugin/gateway/narrative-lane.ts +33 -2
  30. package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
  31. package/telegram-plugin/gateway/privacy-reset.ts +87 -0
  32. package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
  33. package/telegram-plugin/gateway/privacy-state.ts +206 -0
  34. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
  35. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
  36. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
  37. package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
  38. package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
  39. package/telegram-plugin/slot-banner-driver.ts +42 -5
  40. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
  41. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
  42. package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
  43. package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
  44. package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
  45. package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
  46. package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
  47. package/telegram-plugin/throttle-tier.ts +12 -3
  48. package/telegram-plugin/turn-flush-safety.ts +97 -0
  49. package/vendor/hindsight-memory/scripts/retain.py +306 -0
  50. package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
  51. package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
  52. package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
@@ -1,6 +1,6 @@
1
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";
2
+ import { readFileSync as readFileSync7 } from "node:fs";
3
+ import { join as join7 } from "node:path";
4
4
  import { homedir } from "node:os";
5
5
 
6
6
  // src/self-improve/config.ts
@@ -15,6 +15,7 @@ function resolveSelfImproveConfig() {
15
15
  return {
16
16
  repetitionThreshold: Math.max(2, intEnv("SWITCHROOM_SELF_IMPROVE_THRESHOLD", 2)),
17
17
  t1MaxChangedLines: intEnv("SWITCHROOM_SELF_IMPROVE_T1_MAX_LINES", 30),
18
+ t1MaxGrowthBytes: intEnv("SWITCHROOM_SELF_IMPROVE_T1_MAX_GROWTH_BYTES", 4096),
18
19
  maxAutoAppliesPerDay: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_AUTO_PER_DAY", 3),
19
20
  maxOutstandingPending: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_PENDING", 5)
20
21
  };
@@ -22,6 +23,9 @@ function resolveSelfImproveConfig() {
22
23
  function selfImproveEnabled() {
23
24
  return process.env.SWITCHROOM_SELF_IMPROVE !== "0";
24
25
  }
26
+ function t1LiveEnabled() {
27
+ return process.env.SWITCHROOM_SELF_IMPROVE_T1_LIVE === "1";
28
+ }
25
29
 
26
30
  // src/self-improve/review-context.ts
27
31
  import {
@@ -52,8 +56,8 @@ function readReviewContext(stateDir) {
52
56
  }
53
57
 
54
58
  // 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";
59
+ import { existsSync as existsSync6, lstatSync, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
60
+ import { join as join6, resolve, sep } from "node:path";
57
61
 
58
62
  // src/self-improve/tier-router.ts
59
63
  function classifyTier(candidate, cfg = resolveSelfImproveConfig()) {
@@ -113,7 +117,7 @@ function classifyTier(candidate, cfg = resolveSelfImproveConfig()) {
113
117
  }
114
118
 
115
119
  // src/self-improve/eval-gate.ts
116
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
120
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
117
121
  import { join as join2 } from "node:path";
118
122
  function evalsJsonPath(skillDir) {
119
123
  return join2(skillDir, "evals", "evals.json");
@@ -185,7 +189,150 @@ function floatEnv(name, def) {
185
189
  return Number.isFinite(n) ? n : def;
186
190
  }
187
191
 
188
- // src/self-improve/rate-limit.ts
192
+ // src/self-improve/eval-cases.ts
193
+ import {
194
+ closeSync as closeSync2,
195
+ copyFileSync,
196
+ existsSync as existsSync4,
197
+ mkdirSync as mkdirSync3,
198
+ openSync as openSync2,
199
+ readFileSync as readFileSync4,
200
+ readdirSync as readdirSync2,
201
+ renameSync,
202
+ writeFileSync as writeFileSync2,
203
+ writeSync as writeSync2
204
+ } from "node:fs";
205
+ import { dirname, join as join4 } from "node:path";
206
+ import { createHash } from "node:crypto";
207
+
208
+ // telegram-plugin/secret-detect/patterns.ts
209
+ var ANCHORED_PATTERNS = [
210
+ { rule_id: "anthropic_api_key", regex: /\b(sk-ant-[A-Za-z0-9_-]{8,})\b/g, captureIndex: 1, slugHint: "anthropic_api_key" },
211
+ { rule_id: "anthropic_oauth_code", regex: /(?:^|\s)([A-Za-z0-9_-]{20,}#[A-Za-z0-9_-]{20,})(?=\s|$)/gm, captureIndex: 1, slugHint: "anthropic_oauth_code" },
212
+ { rule_id: "openai_api_key", regex: /\b(sk-[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "openai_api_key" },
213
+ { rule_id: "github_pat_classic", regex: /\b(ghp_[A-Za-z0-9]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
214
+ { rule_id: "github_pat_fine_grained", regex: /\b(github_pat_[A-Za-z0-9_]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
215
+ { rule_id: "slack_token", regex: /\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_token" },
216
+ { rule_id: "slack_app_token", regex: /\b(xapp-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_app_token" },
217
+ { rule_id: "groq_api_key", regex: /\b(gsk_[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "groq_api_key" },
218
+ { rule_id: "google_api_key", regex: /\b(AIza[0-9A-Za-z\-_]{20,})\b/g, captureIndex: 1, slugHint: "google_api_key" },
219
+ { rule_id: "perplexity_api_key", regex: /\b(pplx-[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "perplexity_api_key" },
220
+ { rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
221
+ { rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
222
+ { rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
223
+ { rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
224
+ { rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
225
+ { rule_id: "jwt", regex: /\b(eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "jwt" }
226
+ ];
227
+ var STRUCTURED_PATTERNS = [
228
+ {
229
+ rule_id: "env_key_value",
230
+ regex: /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\b\s*[=:]\s*(["']?)([^\s"'\\]+)\2/g,
231
+ captureIndex: 3,
232
+ slugHint: "env"
233
+ },
234
+ {
235
+ rule_id: "json_secret_field",
236
+ regex: /"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"/g,
237
+ captureIndex: 1,
238
+ slugHint: "json_secret"
239
+ },
240
+ {
241
+ rule_id: "cli_flag",
242
+ regex: /--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1/g,
243
+ captureIndex: 2,
244
+ slugHint: "cli_flag"
245
+ },
246
+ {
247
+ rule_id: "bearer_auth_header",
248
+ regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
249
+ captureIndex: 1,
250
+ slugHint: "bearer_token"
251
+ },
252
+ {
253
+ rule_id: "bearer_loose",
254
+ regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
255
+ captureIndex: 1,
256
+ slugHint: "bearer_token"
257
+ },
258
+ {
259
+ rule_id: "basic_auth_header",
260
+ regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
261
+ captureIndex: 1,
262
+ slugHint: "basic_auth"
263
+ },
264
+ {
265
+ rule_id: "pem_private_key",
266
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
267
+ captureIndex: 0,
268
+ slugHint: "pem_private_key"
269
+ }
270
+ ];
271
+ var PROVIDER_PATTERNS = [
272
+ { rule_id: "slack_webhook", regex: /(https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_/]+)/g, captureIndex: 1, slugHint: "slack_webhook" },
273
+ { rule_id: "stripe_live_secret", regex: /\b(sk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
274
+ { rule_id: "stripe_restricted", regex: /\b(rk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
275
+ { rule_id: "sendgrid_api_key", regex: /\b(SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "sendgrid_key" },
276
+ { rule_id: "gitlab_pat", regex: /\b(glpat-[A-Za-z0-9_-]{20})\b/g, captureIndex: 1, slugHint: "gitlab_pat" },
277
+ { rule_id: "huggingface_token", regex: /\b(hf_[A-Za-z0-9]{34,})\b/g, captureIndex: 1, slugHint: "huggingface_token" },
278
+ { rule_id: "twilio_api_key", regex: /\b(SK[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "twilio_api_key" },
279
+ { rule_id: "mailgun_key", regex: /\b(key-[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "mailgun_key" },
280
+ { rule_id: "digitalocean_pat", regex: /\b(dop_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
281
+ { rule_id: "digitalocean_oauth", regex: /\b(doo_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
282
+ { rule_id: "digitalocean_refresh", regex: /\b(dor_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
283
+ { rule_id: "doppler_token", regex: /\b(dp\.(?:pt|st|ct|sa|scim|audit)\.[A-Za-z0-9]{40,44})\b/g, captureIndex: 1, slugHint: "doppler_token" },
284
+ { rule_id: "linear_api_key", regex: /\b(lin_api_[A-Za-z0-9]{40})\b/g, captureIndex: 1, slugHint: "linear_api_key" },
285
+ { rule_id: "shopify_access_token", regex: /\b(shpat_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
286
+ { rule_id: "shopify_shared_secret", regex: /\b(shpss_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
287
+ { rule_id: "shopify_private_app", regex: /\b(shppa_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
288
+ { rule_id: "square_access_token", regex: /\b(sq0atp-[A-Za-z0-9_-]{22})\b/g, captureIndex: 1, slugHint: "square_token" },
289
+ { rule_id: "square_oauth_secret", regex: /\b(sq0csp-[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "square_token" },
290
+ { rule_id: "newrelic_key", regex: /\b(NRAK-[A-Z0-9]{27})\b/g, captureIndex: 1, slugHint: "newrelic_key" },
291
+ { rule_id: "notion_token", regex: /\b(ntn_[A-Za-z0-9]{46})\b/g, captureIndex: 1, slugHint: "notion_token" },
292
+ { rule_id: "planetscale_password", regex: /\b(pscale_pw_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
293
+ { rule_id: "planetscale_token", regex: /\b(pscale_tkn_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
294
+ { rule_id: "supabase_service_key", regex: /\b(sbp_[a-f0-9]{40})\b/g, captureIndex: 1, slugHint: "supabase_key" },
295
+ { rule_id: "atlassian_token", regex: /\b(ATATT[A-Za-z0-9_\-=]{20,})\b/g, captureIndex: 1, slugHint: "atlassian_token" },
296
+ { rule_id: "dropbox_token", regex: /\b(sl\.[A-Za-z0-9_-]{130,})/g, captureIndex: 1, slugHint: "dropbox_token" },
297
+ { rule_id: "databricks_token", regex: /\b(dapi[a-f0-9]{32})\b/g, captureIndex: 1, slugHint: "databricks_token" },
298
+ { rule_id: "grafana_service_account", regex: /\b(glsa_[A-Za-z0-9]{32}_[a-fA-F0-9]{8})\b/g, captureIndex: 1, slugHint: "grafana_token" },
299
+ { rule_id: "pypi_token", regex: /\b(pypi-AgEIcHlwaS[A-Za-z0-9_-]{50,})/g, captureIndex: 1, slugHint: "pypi_token" },
300
+ { rule_id: "aws_temp_access_key", regex: /\b(ASIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
301
+ { rule_id: "gcp_oauth_token", regex: /\b(ya29\.[A-Za-z0-9_-]{30,})/g, captureIndex: 1, slugHint: "gcp_oauth_token" }
302
+ ];
303
+ var ALL_PATTERNS = [...ANCHORED_PATTERNS, ...PROVIDER_PATTERNS, ...STRUCTURED_PATTERNS];
304
+
305
+ // telegram-plugin/secret-detect/inert-values.ts
306
+ var INERT_GATED_RULES = new Set([
307
+ "env_key_value",
308
+ "json_secret_field",
309
+ "cli_flag"
310
+ ]);
311
+
312
+ // telegram-plugin/secret-detect/chunker.ts
313
+ var CHUNK_THRESHOLD = 32 * 1024;
314
+ var WINDOW_SIZE = 16 * 1024;
315
+ var OVERLAP = 8 * 1024;
316
+
317
+ // telegram-plugin/secret-detect/suppressor.ts
318
+ var MARKERS = ["test", "mock", "example", "fixture", "dummy"];
319
+ var MARKER_RE = new RegExp(`\\b(?:${MARKERS.join("|")})\\b`, "i");
320
+ // telegram-plugin/secret-detect/url-redact.ts
321
+ var SENSITIVE_PARAMS = new Set([
322
+ "token",
323
+ "key",
324
+ "api_key",
325
+ "apikey",
326
+ "secret",
327
+ "access_token",
328
+ "password",
329
+ "pass",
330
+ "auth",
331
+ "client_secret",
332
+ "refresh_token",
333
+ "signature"
334
+ ]);
335
+ // src/self-improve/pending-queue.ts
189
336
  import {
190
337
  closeSync,
191
338
  existsSync as existsSync3,
@@ -195,20 +342,128 @@ import {
195
342
  writeSync
196
343
  } from "node:fs";
197
344
  import { join as join3 } from "node:path";
345
+ import { randomUUID } from "node:crypto";
346
+ var PENDING_FILE = "self-improve-pending.jsonl";
347
+ function pendingPath(stateDir) {
348
+ return join3(stateDir, PENDING_FILE);
349
+ }
350
+ function readPending(stateDir) {
351
+ const p = pendingPath(stateDir);
352
+ if (!existsSync3(p))
353
+ return [];
354
+ let raw;
355
+ try {
356
+ raw = readFileSync3(p, "utf-8");
357
+ } catch {
358
+ return [];
359
+ }
360
+ const out = [];
361
+ for (const line of raw.split(`
362
+ `)) {
363
+ if (!line.trim())
364
+ continue;
365
+ try {
366
+ const parsed = JSON.parse(line);
367
+ if (parsed && typeof parsed.id === "string" && parsed.tier) {
368
+ out.push(parsed);
369
+ }
370
+ } catch {}
371
+ }
372
+ return out;
373
+ }
374
+ function outstandingCount(stateDir) {
375
+ return readPending(stateDir).filter((s) => s.actioned !== true).length;
376
+ }
377
+ function enqueuePending(stateDir, input, opts = {}) {
378
+ const cfg = resolveSelfImproveConfig();
379
+ const cap = opts.cap ?? cfg.maxOutstandingPending;
380
+ const now = opts.now ?? Date.now;
381
+ ensureDir(stateDir);
382
+ const outstanding = outstandingCount(stateDir);
383
+ if (outstanding >= cap) {
384
+ return { ok: false, reason: "cap-reached", outstanding, cap };
385
+ }
386
+ const suggestion = {
387
+ id: randomUUID(),
388
+ created_at: new Date(now()).toISOString(),
389
+ ...input
390
+ };
391
+ const fd = openSync(pendingPath(stateDir), "a");
392
+ try {
393
+ writeSync(fd, JSON.stringify(suggestion) + `
394
+ `);
395
+ } finally {
396
+ closeSync(fd);
397
+ }
398
+ return { ok: true, suggestion };
399
+ }
400
+ function ensureDir(stateDir) {
401
+ if (!existsSync3(stateDir)) {
402
+ mkdirSync2(stateDir, { recursive: true, mode: 493 });
403
+ }
404
+ }
405
+
406
+ // src/self-improve/eval-cases.ts
407
+ var EVAL_MANIFEST_FILE = "self-improve-eval-manifest.json";
408
+ function manifestPath(stateDir) {
409
+ return join4(stateDir, EVAL_MANIFEST_FILE);
410
+ }
411
+ function evalsSha256(skillDir) {
412
+ const p = evalsJsonPath(skillDir);
413
+ if (!existsSync4(p))
414
+ return null;
415
+ try {
416
+ return createHash("sha256").update(readFileSync4(p)).digest("hex");
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ function readManifest(stateDir) {
422
+ const p = manifestPath(stateDir);
423
+ if (!existsSync4(p))
424
+ return {};
425
+ try {
426
+ const parsed = JSON.parse(readFileSync4(p, "utf-8"));
427
+ return parsed && typeof parsed === "object" ? parsed : {};
428
+ } catch {
429
+ return {};
430
+ }
431
+ }
432
+ function verifyEvalIntegrity(stateDir, slug, skillDir) {
433
+ const sha = evalsSha256(skillDir);
434
+ if (sha === null)
435
+ return "no-evals";
436
+ const m = readManifest(stateDir);
437
+ const entry = m[slug];
438
+ if (!entry)
439
+ return "missing";
440
+ return sha === entry.sha256 ? "ok" : "drift";
441
+ }
442
+
443
+ // src/self-improve/rate-limit.ts
444
+ import {
445
+ closeSync as closeSync3,
446
+ existsSync as existsSync5,
447
+ mkdirSync as mkdirSync4,
448
+ openSync as openSync3,
449
+ readFileSync as readFileSync5,
450
+ writeSync as writeSync3
451
+ } from "node:fs";
452
+ import { join as join5 } from "node:path";
198
453
  var APPLIES_FILE = "self-improve-applies.jsonl";
199
454
  function appliesPath(stateDir) {
200
- return join3(stateDir, APPLIES_FILE);
455
+ return join5(stateDir, APPLIES_FILE);
201
456
  }
202
457
  function utcDay(ms) {
203
458
  return new Date(ms).toISOString().slice(0, 10);
204
459
  }
205
460
  function appliesToday(stateDir, now = Date.now) {
206
461
  const p = appliesPath(stateDir);
207
- if (!existsSync3(p))
462
+ if (!existsSync5(p))
208
463
  return 0;
209
464
  let raw;
210
465
  try {
211
- raw = readFileSync3(p, "utf-8");
466
+ raw = readFileSync5(p, "utf-8");
212
467
  } catch {
213
468
  return 0;
214
469
  }
@@ -227,16 +482,16 @@ function appliesToday(stateDir, now = Date.now) {
227
482
  return count;
228
483
  }
229
484
  function recordAutoApply(stateDir, now = Date.now) {
230
- if (!existsSync3(stateDir)) {
231
- mkdirSync2(stateDir, { recursive: true, mode: 493 });
485
+ if (!existsSync5(stateDir)) {
486
+ mkdirSync4(stateDir, { recursive: true, mode: 493 });
232
487
  }
233
488
  const ms = now();
234
- const fd = openSync(appliesPath(stateDir), "a");
489
+ const fd = openSync3(appliesPath(stateDir), "a");
235
490
  try {
236
- writeSync(fd, JSON.stringify({ ts: ms, day: utcDay(ms) }) + `
491
+ writeSync3(fd, JSON.stringify({ ts: ms, day: utcDay(ms) }) + `
237
492
  `);
238
493
  } finally {
239
- closeSync(fd);
494
+ closeSync3(fd);
240
495
  }
241
496
  }
242
497
 
@@ -244,7 +499,7 @@ function recordAutoApply(stateDir, now = Date.now) {
244
499
  var SKILLS_SEGMENT = "/.claude/skills/";
245
500
  var BENCHMARK_SUBDIR = "self-improve-benchmarks";
246
501
  function benchmarkJsonForSkill(stateDir, slug) {
247
- return join4(stateDir, BENCHMARK_SUBDIR, slug, "benchmark.json");
502
+ return join6(stateDir, BENCHMARK_SUBDIR, slug, "benchmark.json");
248
503
  }
249
504
  function parseSkillTarget(filePath) {
250
505
  if (typeof filePath !== "string" || filePath.length === 0)
@@ -306,7 +561,7 @@ function estimateChangedLines(toolName, input, filePath) {
306
561
  };
307
562
  const readCurrent = () => {
308
563
  try {
309
- return readFileSync4(filePath, "utf-8");
564
+ return readFileSync6(filePath, "utf-8");
310
565
  } catch {
311
566
  return "";
312
567
  }
@@ -335,6 +590,55 @@ function estimateChangedLines(toolName, input, filePath) {
335
590
  }
336
591
  return 0;
337
592
  }
593
+ function estimateByteDelta(toolName, input, filePath) {
594
+ const byteLen = (s) => Buffer.byteLength(s, "utf8");
595
+ let before = 0;
596
+ try {
597
+ before = statSync2(filePath).size;
598
+ } catch {
599
+ before = 0;
600
+ }
601
+ let currentContent = null;
602
+ const occurrences = (needle) => {
603
+ if (needle.length === 0)
604
+ return 0;
605
+ if (currentContent === null) {
606
+ try {
607
+ currentContent = readFileSync6(filePath, "utf-8");
608
+ } catch {
609
+ currentContent = "";
610
+ }
611
+ }
612
+ return currentContent.split(needle).length - 1;
613
+ };
614
+ const editDelta = (oldS, newS, replaceAll) => {
615
+ const per = byteLen(newS) - byteLen(oldS);
616
+ const count = replaceAll ? occurrences(oldS) : 1;
617
+ return per * count;
618
+ };
619
+ let after = before;
620
+ if (toolName === "Write") {
621
+ const content = typeof input.content === "string" ? input.content : "";
622
+ after = byteLen(content);
623
+ } else if (toolName === "Edit") {
624
+ const oldS = typeof input.old_string === "string" ? input.old_string : "";
625
+ const newS = typeof input.new_string === "string" ? input.new_string : "";
626
+ after = before + editDelta(oldS, newS, input.replace_all === true);
627
+ } else if (toolName === "MultiEdit") {
628
+ const edits = Array.isArray(input.edits) ? input.edits : [];
629
+ let net = 0;
630
+ for (const e of edits) {
631
+ if (!e || typeof e !== "object")
632
+ continue;
633
+ const ed = e;
634
+ const oldS = typeof ed.old_string === "string" ? ed.old_string : "";
635
+ const newS = typeof ed.new_string === "string" ? ed.new_string : "";
636
+ net += editDelta(oldS, newS, ed.replace_all === true);
637
+ }
638
+ after = before + net;
639
+ }
640
+ return { before, after, delta: after - before };
641
+ }
338
642
  function downgradeCandidate(target, owns, changedLines, lesson) {
339
643
  return {
340
644
  lesson,
@@ -395,6 +699,18 @@ function decideApply(p) {
395
699
  candidate: downgradeCandidate(target, owns, changedLines, lesson)
396
700
  };
397
701
  }
702
+ const bytes = estimateByteDelta(p.toolName, p.input, p.filePath);
703
+ if (bytes.delta > cfg.t1MaxGrowthBytes) {
704
+ const over = bytes.delta - cfg.t1MaxGrowthBytes;
705
+ return {
706
+ action: "block",
707
+ downgradeTier: "T2",
708
+ reason: `T1 net-growth cap exceeded for "${target.slug}": +${bytes.delta} bytes (${bytes.before}\u2192${bytes.after}) is ${over} over the ${cfg.t1MaxGrowthBytes}-byte budget \u2014 downgraded to a proposal`,
709
+ candidate: downgradeCandidate(target, owns, changedLines, lesson),
710
+ bytesBefore: bytes.before,
711
+ bytesAfter: bytes.after
712
+ };
713
+ }
398
714
  if (!skillHasEvals(target.skillDir)) {
399
715
  return {
400
716
  action: "block",
@@ -404,7 +720,7 @@ function decideApply(p) {
404
720
  };
405
721
  }
406
722
  const benchPath = benchmarkJsonForSkill(p.stateDir, target.slug);
407
- if (!existsSync4(benchPath)) {
723
+ if (!existsSync6(benchPath)) {
408
724
  return {
409
725
  action: "block",
410
726
  downgradeTier: "T2",
@@ -415,7 +731,7 @@ function decideApply(p) {
415
731
  if (typeof p.freshAfterMs === "number") {
416
732
  let mtime = 0;
417
733
  try {
418
- mtime = statSync(benchPath).mtimeMs;
734
+ mtime = statSync2(benchPath).mtimeMs;
419
735
  } catch {
420
736
  mtime = 0;
421
737
  }
@@ -437,6 +753,16 @@ function decideApply(p) {
437
753
  candidate: downgradeCandidate(target, owns, changedLines, lesson)
438
754
  };
439
755
  }
756
+ const integrity = verifyEvalIntegrity(p.stateDir, target.slug, target.skillDir);
757
+ if (integrity !== "ok") {
758
+ const why = integrity === "drift" ? `eval set for "${target.slug}" drifted from its recorded baseline (out-of-band edit)` : `eval set for "${target.slug}" has no sanctioned baseline (integrity: ${integrity})`;
759
+ return {
760
+ action: "block",
761
+ downgradeTier: "T2",
762
+ reason: `${why} \u2014 refusing to auto-apply on unverified evals; downgraded to a proposal`,
763
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
764
+ };
765
+ }
440
766
  const used = appliesToday(p.stateDir, now);
441
767
  if (used >= cfg.maxAutoAppliesPerDay) {
442
768
  return {
@@ -446,6 +772,14 @@ function decideApply(p) {
446
772
  candidate: downgradeCandidate(target, owns, changedLines, lesson)
447
773
  };
448
774
  }
775
+ if (!t1LiveEnabled()) {
776
+ return {
777
+ action: "block",
778
+ downgradeTier: "T2",
779
+ reason: `verified T1 for "${target.slug}", but silent auto-apply is disabled (SWITCHROOM_SELF_IMPROVE_T1_LIVE unset) \u2014 surfacing as a one-tap proposal`,
780
+ candidate: downgradeCandidate(target, owns, changedLines, lesson)
781
+ };
782
+ }
449
783
  return {
450
784
  action: "allow",
451
785
  tier: "T1",
@@ -456,82 +790,11 @@ function decideApply(p) {
456
790
  };
457
791
  }
458
792
 
459
- // src/self-improve/pending-queue.ts
460
- import {
461
- closeSync as closeSync2,
462
- existsSync as existsSync5,
463
- mkdirSync as mkdirSync3,
464
- openSync as openSync2,
465
- readFileSync as readFileSync5,
466
- writeSync as writeSync2
467
- } from "node:fs";
468
- import { join as join5 } from "node:path";
469
- import { randomUUID } from "node:crypto";
470
- var PENDING_FILE = "self-improve-pending.jsonl";
471
- function pendingPath(stateDir) {
472
- return join5(stateDir, PENDING_FILE);
473
- }
474
- function readPending(stateDir) {
475
- const p = pendingPath(stateDir);
476
- if (!existsSync5(p))
477
- return [];
478
- let raw;
479
- try {
480
- raw = readFileSync5(p, "utf-8");
481
- } catch {
482
- return [];
483
- }
484
- const out = [];
485
- for (const line of raw.split(`
486
- `)) {
487
- if (!line.trim())
488
- continue;
489
- try {
490
- const parsed = JSON.parse(line);
491
- if (parsed && typeof parsed.id === "string" && parsed.tier) {
492
- out.push(parsed);
493
- }
494
- } catch {}
495
- }
496
- return out;
497
- }
498
- function outstandingCount(stateDir) {
499
- return readPending(stateDir).filter((s) => s.actioned !== true).length;
500
- }
501
- function enqueuePending(stateDir, input, opts = {}) {
502
- const cfg = resolveSelfImproveConfig();
503
- const cap = opts.cap ?? cfg.maxOutstandingPending;
504
- const now = opts.now ?? Date.now;
505
- ensureDir(stateDir);
506
- const outstanding = outstandingCount(stateDir);
507
- if (outstanding >= cap) {
508
- return { ok: false, reason: "cap-reached", outstanding, cap };
509
- }
510
- const suggestion = {
511
- id: randomUUID(),
512
- created_at: new Date(now()).toISOString(),
513
- ...input
514
- };
515
- const fd = openSync2(pendingPath(stateDir), "a");
516
- try {
517
- writeSync2(fd, JSON.stringify(suggestion) + `
518
- `);
519
- } finally {
520
- closeSync2(fd);
521
- }
522
- return { ok: true, suggestion };
523
- }
524
- function ensureDir(stateDir) {
525
- if (!existsSync5(stateDir)) {
526
- mkdirSync3(stateDir, { recursive: true, mode: 493 });
527
- }
528
- }
529
-
530
793
  // src/cli/self-improve-apply-guard-pretool.ts
531
794
  var EDIT_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
532
795
  function readStdin() {
533
796
  try {
534
- return readFileSync6(0, "utf8");
797
+ return readFileSync7(0, "utf8");
535
798
  } catch {
536
799
  return "";
537
800
  }
@@ -545,7 +808,7 @@ function block(reason) {
545
808
  process.exit(0);
546
809
  }
547
810
  function resolveStateDir() {
548
- return process.env.TELEGRAM_STATE_DIR ?? join6(homedir(), ".claude", "channels", "telegram");
811
+ return process.env.TELEGRAM_STATE_DIR ?? join7(homedir(), ".claude", "channels", "telegram");
549
812
  }
550
813
  function main() {
551
814
  if (!selfImproveEnabled())
@@ -608,7 +871,9 @@ function main() {
608
871
  proposed_change: decision.candidate.proposedChange,
609
872
  tier_reason: decision.reason,
610
873
  skill_slug: decision.candidate.skillSlug,
611
- triggered_by: (ctx.signals ?? []).map((s) => s.kind)
874
+ triggered_by: (ctx.signals ?? []).map((s) => s.kind),
875
+ bytes_before: decision.bytesBefore,
876
+ bytes_after: decision.bytesAfter
612
877
  });
613
878
  if (!q.ok) {
614
879
  capNote = ` (pending queue full ${q.outstanding}/${q.cap} \u2014 proposal not recorded)`;