switchroom 0.20.9 → 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 (59) hide show
  1. package/bin/handoff-briefing.sh +57 -5
  2. package/bin/working-state-reload-hook.sh +262 -0
  3. package/dist/agent-scheduler/index.js +65 -2
  4. package/dist/auth-broker/index.js +204 -24
  5. package/dist/cli/notion-write-pretool.mjs +65 -2
  6. package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
  7. package/dist/cli/self-improve-stop.mjs +889 -7
  8. package/dist/cli/skill-validate-pretool.mjs +82 -3
  9. package/dist/cli/switchroom.js +3699 -2110
  10. package/dist/host-control/main.js +67 -4
  11. package/dist/vault/approvals/kernel-server.js +66 -3
  12. package/dist/vault/broker/server.js +66 -3
  13. package/package.json +1 -1
  14. package/profiles/_base/start.sh.hbs +49 -0
  15. package/profiles/_shared/agent-self-service.md.hbs +15 -22
  16. package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
  17. package/profiles/_shared/dev-protocol.md.hbs +1 -1
  18. package/profiles/_shared/execution-discipline.md.hbs +4 -4
  19. package/profiles/_shared/vault-protocol.md.hbs +2 -18
  20. package/profiles/default/CLAUDE.md.hbs +3 -5
  21. package/telegram-plugin/auto-fallback-fleet.ts +37 -2
  22. package/telegram-plugin/dist/gateway/gateway.js +1414 -918
  23. package/telegram-plugin/fallback-card-collapse.ts +1 -0
  24. package/telegram-plugin/gateway/auth-command.ts +11 -1
  25. package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
  26. package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
  27. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
  28. package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
  29. package/telegram-plugin/gateway/gateway.ts +85 -90
  30. package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
  31. package/telegram-plugin/gateway/ipc-server.ts +28 -0
  32. package/telegram-plugin/gateway/narrative-lane.ts +33 -2
  33. package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
  34. package/telegram-plugin/gateway/privacy-reset.ts +87 -0
  35. package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
  36. package/telegram-plugin/gateway/privacy-state.ts +206 -0
  37. package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
  38. package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
  39. package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
  40. package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
  41. package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
  42. package/telegram-plugin/slot-banner-driver.ts +42 -5
  43. package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
  44. package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
  45. package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
  46. package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
  47. package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
  48. package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
  49. package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
  50. package/telegram-plugin/throttle-tier.ts +12 -3
  51. package/telegram-plugin/turn-flush-safety.ts +97 -0
  52. package/vendor/hindsight-memory/CHANGELOG.md +31 -0
  53. package/vendor/hindsight-memory/hooks/hooks.json +2 -1
  54. package/vendor/hindsight-memory/scripts/retain.py +306 -0
  55. package/vendor/hindsight-memory/scripts/session_start.py +35 -8
  56. package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
  57. package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
  58. package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
  59. package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
@@ -1,7 +1,7 @@
1
1
  // src/cli/self-improve-stop.ts
2
- import { readFileSync as readFileSync2 } from "node:fs";
2
+ import { readFileSync as readFileSync4 } from "node:fs";
3
3
  import { createConnection } from "node:net";
4
- import { join as join2 } from "node:path";
4
+ import { join as join6 } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
 
7
7
  // src/self-improve/config.ts
@@ -16,6 +16,7 @@ function resolveSelfImproveConfig() {
16
16
  return {
17
17
  repetitionThreshold: Math.max(2, intEnv("SWITCHROOM_SELF_IMPROVE_THRESHOLD", 2)),
18
18
  t1MaxChangedLines: intEnv("SWITCHROOM_SELF_IMPROVE_T1_MAX_LINES", 30),
19
+ t1MaxGrowthBytes: intEnv("SWITCHROOM_SELF_IMPROVE_T1_MAX_GROWTH_BYTES", 4096),
19
20
  maxAutoAppliesPerDay: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_AUTO_PER_DAY", 3),
20
21
  maxOutstandingPending: intEnv("SWITCHROOM_SELF_IMPROVE_MAX_PENDING", 5)
21
22
  };
@@ -206,6 +207,7 @@ function buildReviewPrompt(signals) {
206
207
  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
208
  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
209
  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.");
210
+ lines.push(" 5. If this was an operator CORRECTION tied to a skill you own, turn " + "it into a regression test: run `switchroom self-improve add-eval-case " + "--skill <slug> --chat <operator chat id> " + '--prompt "<the correction framed as a test prompt>"`. The `--chat` ' + "flag is REQUIRED \u2014 pass the real operator chat id from this session's " + "context (the chat the correction arrived in), NOT the review turn's " + "own placeholder chat id, or the command is rejected. This proposes " + "the case as a one-tap card; the operator's tap runs a deterministic " + "applier that writes it (PII/secret-scanned). Do NOT Write or Edit " + "evals/evals.json yourself \u2014 a direct write is blocked, bypasses the " + "scan, and skips the operator's approval.");
209
211
  lines.push("");
210
212
  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
213
  return lines.join(`
@@ -237,11 +239,878 @@ function clearReviewContext(stateDir) {
237
239
  } catch {}
238
240
  }
239
241
 
242
+ // src/self-improve/eval-cases.ts
243
+ import {
244
+ closeSync as closeSync2,
245
+ copyFileSync,
246
+ existsSync as existsSync3,
247
+ mkdirSync as mkdirSync3,
248
+ openSync as openSync2,
249
+ readFileSync as readFileSync3,
250
+ readdirSync,
251
+ renameSync,
252
+ writeFileSync as writeFileSync2,
253
+ writeSync as writeSync2
254
+ } from "node:fs";
255
+ import { dirname, join as join4 } from "node:path";
256
+ import { createHash } from "node:crypto";
257
+
258
+ // src/self-improve/eval-gate.ts
259
+ import { join as join2 } from "node:path";
260
+ function evalsJsonPath(skillDir) {
261
+ return join2(skillDir, "evals", "evals.json");
262
+ }
263
+
264
+ // telegram-plugin/secret-detect/patterns.ts
265
+ var ANCHORED_PATTERNS = [
266
+ { rule_id: "anthropic_api_key", regex: /\b(sk-ant-[A-Za-z0-9_-]{8,})\b/g, captureIndex: 1, slugHint: "anthropic_api_key" },
267
+ { 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" },
268
+ { rule_id: "openai_api_key", regex: /\b(sk-[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "openai_api_key" },
269
+ { rule_id: "github_pat_classic", regex: /\b(ghp_[A-Za-z0-9]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
270
+ { rule_id: "github_pat_fine_grained", regex: /\b(github_pat_[A-Za-z0-9_]{20,})\b/g, captureIndex: 1, slugHint: "github_pat" },
271
+ { rule_id: "slack_token", regex: /\b(xox[baprs]-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_token" },
272
+ { rule_id: "slack_app_token", regex: /\b(xapp-[A-Za-z0-9-]{10,})\b/g, captureIndex: 1, slugHint: "slack_app_token" },
273
+ { rule_id: "groq_api_key", regex: /\b(gsk_[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "groq_api_key" },
274
+ { rule_id: "google_api_key", regex: /\b(AIza[0-9A-Za-z\-_]{20,})\b/g, captureIndex: 1, slugHint: "google_api_key" },
275
+ { rule_id: "perplexity_api_key", regex: /\b(pplx-[A-Za-z0-9_-]{10,})\b/g, captureIndex: 1, slugHint: "perplexity_api_key" },
276
+ { rule_id: "npm_token", regex: /\b(npm_[A-Za-z0-9]{10,})\b/g, captureIndex: 1, slugHint: "npm_token" },
277
+ { rule_id: "telegram_bot_token_prefixed", regex: /\bbot(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
278
+ { rule_id: "telegram_bot_token", regex: /\b(\d{6,}:[A-Za-z0-9_-]{20,})\b/g, captureIndex: 1, slugHint: "telegram_bot_token" },
279
+ { rule_id: "laravel_sanctum_token", regex: /\b(\d+\|[A-Za-z0-9]{40,})\b/g, captureIndex: 1, slugHint: "api_token" },
280
+ { rule_id: "aws_access_key", regex: /\b(AKIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
281
+ { 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" }
282
+ ];
283
+ var STRUCTURED_PATTERNS = [
284
+ {
285
+ rule_id: "env_key_value",
286
+ regex: /\b([A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD))\b\s*[=:]\s*(["']?)([^\s"'\\]+)\2/g,
287
+ captureIndex: 3,
288
+ slugHint: "env"
289
+ },
290
+ {
291
+ rule_id: "json_secret_field",
292
+ regex: /"(?:apiKey|token|secret|password|passwd|accessToken|refreshToken)"\s*:\s*"([^"]+)"/g,
293
+ captureIndex: 1,
294
+ slugHint: "json_secret"
295
+ },
296
+ {
297
+ rule_id: "cli_flag",
298
+ regex: /--(?:api[-_]?key|hook[-_]?token|token|secret|password|passwd)\s+(["']?)([^\s"']+)\1/g,
299
+ captureIndex: 2,
300
+ slugHint: "cli_flag"
301
+ },
302
+ {
303
+ rule_id: "bearer_auth_header",
304
+ regex: /Authorization\s*[:=]\s*Bearer\s+([A-Za-z0-9._\-+=]+)/gi,
305
+ captureIndex: 1,
306
+ slugHint: "bearer_token"
307
+ },
308
+ {
309
+ rule_id: "bearer_loose",
310
+ regex: /\bBearer\s+([A-Za-z0-9._\-+=]{18,})\b/gi,
311
+ captureIndex: 1,
312
+ slugHint: "bearer_token"
313
+ },
314
+ {
315
+ rule_id: "basic_auth_header",
316
+ regex: /Authorization\s*[:=]\s*Basic\s+([A-Za-z0-9+/=]{8,})/gi,
317
+ captureIndex: 1,
318
+ slugHint: "basic_auth"
319
+ },
320
+ {
321
+ rule_id: "pem_private_key",
322
+ regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]+?-----END [A-Z ]*PRIVATE KEY-----/g,
323
+ captureIndex: 0,
324
+ slugHint: "pem_private_key"
325
+ }
326
+ ];
327
+ var PROVIDER_PATTERNS = [
328
+ { rule_id: "slack_webhook", regex: /(https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_/]+)/g, captureIndex: 1, slugHint: "slack_webhook" },
329
+ { rule_id: "stripe_live_secret", regex: /\b(sk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
330
+ { rule_id: "stripe_restricted", regex: /\b(rk_live_[A-Za-z0-9]{24,})\b/g, captureIndex: 1, slugHint: "stripe_key" },
331
+ { 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" },
332
+ { rule_id: "gitlab_pat", regex: /\b(glpat-[A-Za-z0-9_-]{20})\b/g, captureIndex: 1, slugHint: "gitlab_pat" },
333
+ { rule_id: "huggingface_token", regex: /\b(hf_[A-Za-z0-9]{34,})\b/g, captureIndex: 1, slugHint: "huggingface_token" },
334
+ { rule_id: "twilio_api_key", regex: /\b(SK[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "twilio_api_key" },
335
+ { rule_id: "mailgun_key", regex: /\b(key-[0-9a-f]{32})\b/g, captureIndex: 1, slugHint: "mailgun_key" },
336
+ { rule_id: "digitalocean_pat", regex: /\b(dop_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
337
+ { rule_id: "digitalocean_oauth", regex: /\b(doo_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
338
+ { rule_id: "digitalocean_refresh", regex: /\b(dor_v1_[a-f0-9]{64})\b/g, captureIndex: 1, slugHint: "digitalocean_token" },
339
+ { 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" },
340
+ { rule_id: "linear_api_key", regex: /\b(lin_api_[A-Za-z0-9]{40})\b/g, captureIndex: 1, slugHint: "linear_api_key" },
341
+ { rule_id: "shopify_access_token", regex: /\b(shpat_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
342
+ { rule_id: "shopify_shared_secret", regex: /\b(shpss_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
343
+ { rule_id: "shopify_private_app", regex: /\b(shppa_[a-fA-F0-9]{32})\b/g, captureIndex: 1, slugHint: "shopify_token" },
344
+ { rule_id: "square_access_token", regex: /\b(sq0atp-[A-Za-z0-9_-]{22})\b/g, captureIndex: 1, slugHint: "square_token" },
345
+ { rule_id: "square_oauth_secret", regex: /\b(sq0csp-[A-Za-z0-9_-]{43})\b/g, captureIndex: 1, slugHint: "square_token" },
346
+ { rule_id: "newrelic_key", regex: /\b(NRAK-[A-Z0-9]{27})\b/g, captureIndex: 1, slugHint: "newrelic_key" },
347
+ { rule_id: "notion_token", regex: /\b(ntn_[A-Za-z0-9]{46})\b/g, captureIndex: 1, slugHint: "notion_token" },
348
+ { rule_id: "planetscale_password", regex: /\b(pscale_pw_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
349
+ { rule_id: "planetscale_token", regex: /\b(pscale_tkn_[A-Za-z0-9_.-]{43})\b/g, captureIndex: 1, slugHint: "planetscale_token" },
350
+ { rule_id: "supabase_service_key", regex: /\b(sbp_[a-f0-9]{40})\b/g, captureIndex: 1, slugHint: "supabase_key" },
351
+ { rule_id: "atlassian_token", regex: /\b(ATATT[A-Za-z0-9_\-=]{20,})\b/g, captureIndex: 1, slugHint: "atlassian_token" },
352
+ { rule_id: "dropbox_token", regex: /\b(sl\.[A-Za-z0-9_-]{130,})/g, captureIndex: 1, slugHint: "dropbox_token" },
353
+ { rule_id: "databricks_token", regex: /\b(dapi[a-f0-9]{32})\b/g, captureIndex: 1, slugHint: "databricks_token" },
354
+ { 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" },
355
+ { rule_id: "pypi_token", regex: /\b(pypi-AgEIcHlwaS[A-Za-z0-9_-]{50,})/g, captureIndex: 1, slugHint: "pypi_token" },
356
+ { rule_id: "aws_temp_access_key", regex: /\b(ASIA[0-9A-Z]{16})\b/g, captureIndex: 1, slugHint: "aws_access_key" },
357
+ { rule_id: "gcp_oauth_token", regex: /\b(ya29\.[A-Za-z0-9_-]{30,})/g, captureIndex: 1, slugHint: "gcp_oauth_token" }
358
+ ];
359
+ var ALL_PATTERNS = [...ANCHORED_PATTERNS, ...PROVIDER_PATTERNS, ...STRUCTURED_PATTERNS];
360
+
361
+ // telegram-plugin/secret-detect/entropy.ts
362
+ function shannonEntropy(s) {
363
+ if (s.length === 0)
364
+ return 0;
365
+ const counts = new Map;
366
+ for (const ch of s) {
367
+ counts.set(ch, (counts.get(ch) ?? 0) + 1);
368
+ }
369
+ let h = 0;
370
+ const len = s.length;
371
+ for (const c of counts.values()) {
372
+ const p = c / len;
373
+ h -= p * Math.log2(p);
374
+ }
375
+ return h;
376
+ }
377
+
378
+ // telegram-plugin/secret-detect/inert-values.ts
379
+ var INERT_VALUE_RE = [
380
+ /^\[REDACTED(?::[A-Za-z0-9_]+)?\]$/i,
381
+ /^\$\{?[A-Za-z_][A-Za-z0-9_]*\}?$/,
382
+ /^\{\{[^\n\r\u2028\u2029]*\}\}$/,
383
+ /^<[^<>\n\r\u2028\u2029]*>$/,
384
+ /^%[A-Za-z_][A-Za-z0-9_]*%$/,
385
+ /^vault:[A-Za-z0-9_./-]*$/i,
386
+ /^[*x\u2022.]+$/i,
387
+ /^(?:process\.env|import\.meta\.env|os\.environ)(?:\.[A-Za-z_$][A-Za-z0-9_$]*|\[["']?[A-Za-z_$][A-Za-z0-9_$]*["']?\])*$/,
388
+ /^(?:changeme|change-me|replaceme|replace-me|placeholder|todo|tbd|yourkey|your-key|yourpassword|your-password|yoursecret|your-secret|yourtoken|your-token)(?:[-_][A-Za-z0-9-]+)?$/i,
389
+ /^[a-z]+(?: [a-z]+){2,}$/
390
+ ];
391
+ var CREDENTIAL_RUN_RE = /[A-Za-z0-9]{12,}/g;
392
+ var MIXED_CLASS_RUN_MIN = 12;
393
+ var SINGLE_CLASS_RUN_MIN = 16;
394
+ function runIsCredentialShaped(run) {
395
+ let classes = 0;
396
+ if (/[a-z]/.test(run))
397
+ classes++;
398
+ if (/[A-Z]/.test(run))
399
+ classes++;
400
+ if (/[0-9]/.test(run))
401
+ classes++;
402
+ return run.length >= (classes >= 2 ? MIXED_CLASS_RUN_MIN : SINGLE_CLASS_RUN_MIN);
403
+ }
404
+ function hasCredentialShapedRun(value) {
405
+ for (const run of value.match(CREDENTIAL_RUN_RE) ?? []) {
406
+ if (runIsCredentialShaped(run))
407
+ return true;
408
+ }
409
+ return false;
410
+ }
411
+ function isInertValue(value) {
412
+ if (!INERT_VALUE_RE.some((re) => re.test(value)))
413
+ return false;
414
+ return !hasCredentialShapedRun(value);
415
+ }
416
+ var INERT_GATED_RULES = new Set([
417
+ "env_key_value",
418
+ "json_secret_field",
419
+ "cli_flag"
420
+ ]);
421
+ var TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
422
+ function stripTrailingPunctuation(value) {
423
+ return value.replace(TRAILING_PUNCT_RE, "");
424
+ }
425
+
426
+ // telegram-plugin/secret-detect/kv-scanner.ts
427
+ var KV_RE = /\b([A-Za-z_][A-Za-z0-9_-]*(?:password|passwd|token|secret|key|api[_-]?key))\s*[:=]\s*["']?([^\s"'\\]{8,})["']?/gi;
428
+ var KV_ENTROPY_THRESHOLD = 4;
429
+ var MEMORABLE_PW_RE = /\b([A-Za-z0-9_-]*(?:password|passwd|passphrase|pwd))\b\s*(?:[:=]\s*|\s+is\s+)(["']?)([^\s"']{8,64})\2/gi;
430
+ var MEMORABLE_PW_RULE_ID = "memorable_password";
431
+ var MEMORABLE_PW_MIN_CLASSES = 2;
432
+ function charClassCount(value) {
433
+ let n = 0;
434
+ if (/[a-z]/.test(value))
435
+ n++;
436
+ if (/[A-Z]/.test(value))
437
+ n++;
438
+ if (/[0-9]/.test(value))
439
+ n++;
440
+ if (/[^A-Za-z0-9]/.test(value))
441
+ n++;
442
+ return n;
443
+ }
444
+ function looksLikeMemorablePassword(value) {
445
+ if (isInertValue(value))
446
+ return false;
447
+ const core = stripTrailingPunctuation(value);
448
+ if (core.length < 8 || core.length > 64)
449
+ return false;
450
+ if (isInertValue(core))
451
+ return false;
452
+ if (new Set(core).size < 4)
453
+ return false;
454
+ return charClassCount(core) >= MEMORABLE_PW_MIN_CLASSES;
455
+ }
456
+ function scanMemorablePasswords(text) {
457
+ const hits = [];
458
+ MEMORABLE_PW_RE.lastIndex = 0;
459
+ let m;
460
+ while ((m = MEMORABLE_PW_RE.exec(text)) !== null) {
461
+ const keyName = m[1];
462
+ const value = m[3];
463
+ if (!value || !looksLikeMemorablePassword(value))
464
+ continue;
465
+ const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
466
+ if (valueOffsetInMatch < 0)
467
+ continue;
468
+ const start = m.index + valueOffsetInMatch;
469
+ hits.push({
470
+ rule_id: MEMORABLE_PW_RULE_ID,
471
+ start,
472
+ end: start + value.length,
473
+ matched_text: value,
474
+ key_name: keyName,
475
+ confidence: "ambiguous"
476
+ });
477
+ }
478
+ return hits;
479
+ }
480
+ function scanKeyValue(text) {
481
+ const hits = [];
482
+ KV_RE.lastIndex = 0;
483
+ let m;
484
+ while ((m = KV_RE.exec(text)) !== null) {
485
+ const [, keyName, value] = m;
486
+ if (!value)
487
+ continue;
488
+ if (isInertValue(value))
489
+ continue;
490
+ const h = shannonEntropy(value);
491
+ if (h < KV_ENTROPY_THRESHOLD)
492
+ continue;
493
+ const valueOffsetInMatch = m[0].indexOf(value, keyName.length);
494
+ if (valueOffsetInMatch < 0)
495
+ continue;
496
+ const start = m.index + valueOffsetInMatch;
497
+ const end = start + value.length;
498
+ hits.push({
499
+ rule_id: "kv_entropy",
500
+ start,
501
+ end,
502
+ matched_text: value,
503
+ key_name: keyName,
504
+ confidence: "ambiguous"
505
+ });
506
+ }
507
+ return hits;
508
+ }
509
+
510
+ // telegram-plugin/secret-detect/db-uri.ts
511
+ var DB_URI_RE = /\b([a-zA-Z][a-zA-Z0-9+.-]+):\/\/([^\s:/@]+):([^\s/?#]+)@([^\s/@?#]+)/g;
512
+ var DB_URI_RULE_ID = "db_uri_password";
513
+ function isInertPassword(value) {
514
+ if (value.startsWith("[REDACTED"))
515
+ return true;
516
+ return /^[*x]+$/i.test(value);
517
+ }
518
+ function scanDbUris(text) {
519
+ const hits = [];
520
+ DB_URI_RE.lastIndex = 0;
521
+ let m;
522
+ while ((m = DB_URI_RE.exec(text)) !== null) {
523
+ const scheme = m[1];
524
+ const user = m[2];
525
+ const password = m[3];
526
+ if (isInertPassword(password))
527
+ continue;
528
+ const start = m.index + scheme.length + 3 + user.length + 1;
529
+ hits.push({
530
+ rule_id: DB_URI_RULE_ID,
531
+ start,
532
+ end: start + password.length,
533
+ matched_text: password,
534
+ key_name: `${scheme}_password`,
535
+ confidence: "ambiguous"
536
+ });
537
+ }
538
+ return hits;
539
+ }
540
+
541
+ // telegram-plugin/secret-detect/generic-entropy.ts
542
+ var CANDIDATE_RE = /[A-Za-z0-9]{28,}/g;
543
+ var GENERIC_MIN_DISTINCT = 18;
544
+ var MAX_GENERIC_HITS = 20;
545
+ function hasDistinctChars(tok, n) {
546
+ const seen = new Uint8Array(128);
547
+ let distinct = 0;
548
+ for (let i = 0;i < tok.length; i++) {
549
+ const c = tok.charCodeAt(i);
550
+ if (seen[c] === 0) {
551
+ seen[c] = 1;
552
+ if (++distinct >= n)
553
+ return true;
554
+ }
555
+ }
556
+ return false;
557
+ }
558
+ function hasDigit(tok) {
559
+ for (let i = 0;i < tok.length; i++) {
560
+ const c = tok.charCodeAt(i);
561
+ if (c >= 48 && c <= 57)
562
+ return true;
563
+ }
564
+ return false;
565
+ }
566
+ function scanGenericSecrets(text) {
567
+ const hits = [];
568
+ CANDIDATE_RE.lastIndex = 0;
569
+ let m;
570
+ while ((m = CANDIDATE_RE.exec(text)) !== null) {
571
+ if (hits.length >= MAX_GENERIC_HITS)
572
+ break;
573
+ const tok = m[0];
574
+ if (!hasDigit(tok))
575
+ continue;
576
+ if (!hasDistinctChars(tok, GENERIC_MIN_DISTINCT))
577
+ continue;
578
+ hits.push({
579
+ rule_id: "generic_high_entropy",
580
+ start: m.index,
581
+ end: m.index + tok.length,
582
+ matched_text: tok,
583
+ confidence: "ambiguous"
584
+ });
585
+ }
586
+ return hits;
587
+ }
588
+
589
+ // telegram-plugin/secret-detect/chunker.ts
590
+ var CHUNK_THRESHOLD = 32 * 1024;
591
+ var WINDOW_SIZE = 16 * 1024;
592
+ var OVERLAP = 8 * 1024;
593
+ function chunk(text) {
594
+ if (text.length <= CHUNK_THRESHOLD) {
595
+ return [{ offset: 0, text }];
596
+ }
597
+ const out = [];
598
+ let offset = 0;
599
+ while (offset < text.length) {
600
+ const end = Math.min(offset + WINDOW_SIZE, text.length);
601
+ out.push({ offset, text: text.slice(offset, end) });
602
+ if (end >= text.length)
603
+ break;
604
+ offset = end - OVERLAP;
605
+ }
606
+ return out;
607
+ }
608
+
609
+ // telegram-plugin/secret-detect/suppressor.ts
610
+ var MARKERS = ["test", "mock", "example", "fixture", "dummy"];
611
+ var WINDOW = 40;
612
+ var MARKER_RE = new RegExp(`\\b(?:${MARKERS.join("|")})\\b`, "i");
613
+ function isSuppressed(text, start, end) {
614
+ const left = Math.max(0, start - WINDOW);
615
+ const right = Math.min(text.length, end + WINDOW);
616
+ const context = text.slice(left, start) + text.slice(end, right);
617
+ return MARKER_RE.test(context);
618
+ }
619
+
620
+ // telegram-plugin/secret-detect/slug.ts
621
+ function sanitizeKeyName(raw) {
622
+ const up = raw.toUpperCase();
623
+ const cleaned = up.replace(/[^A-Z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
624
+ return cleaned.length > 0 ? cleaned : "SECRET";
625
+ }
626
+ function datePart(now = new Date) {
627
+ const y = now.getUTCFullYear();
628
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
629
+ const d = String(now.getUTCDate()).padStart(2, "0");
630
+ return `${y}${m}${d}`;
631
+ }
632
+ function deriveSlug(inputs, existing) {
633
+ let base;
634
+ if (inputs.key_name && inputs.key_name.trim().length > 0) {
635
+ base = sanitizeKeyName(inputs.key_name);
636
+ } else {
637
+ base = `${inputs.rule_id}_${datePart(inputs.now)}`;
638
+ }
639
+ if (!existing.has(base))
640
+ return base;
641
+ let n = 2;
642
+ while (existing.has(`${base}_${n}`))
643
+ n++;
644
+ return `${base}_${n}`;
645
+ }
646
+ // telegram-plugin/secret-detect/url-redact.ts
647
+ var SENSITIVE_PARAMS = new Set([
648
+ "token",
649
+ "key",
650
+ "api_key",
651
+ "apikey",
652
+ "secret",
653
+ "access_token",
654
+ "password",
655
+ "pass",
656
+ "auth",
657
+ "client_secret",
658
+ "refresh_token",
659
+ "signature"
660
+ ]);
661
+
662
+ // telegram-plugin/secret-detect/index.ts
663
+ function detectSecrets(text) {
664
+ if (!text || text.length === 0)
665
+ return [];
666
+ const windows = chunk(text);
667
+ const raw = [];
668
+ for (const win of windows) {
669
+ for (const p of ALL_PATTERNS) {
670
+ const re = new RegExp(p.regex.source, p.regex.flags.includes("g") ? p.regex.flags : p.regex.flags + "g");
671
+ let m;
672
+ while ((m = re.exec(win.text)) !== null) {
673
+ if (m[0].length === 0) {
674
+ re.lastIndex++;
675
+ continue;
676
+ }
677
+ const cap = p.captureIndex === 0 ? m[0] : m[p.captureIndex];
678
+ if (!cap)
679
+ continue;
680
+ const matchStart = p.captureIndex === 0 ? m.index : m.index + m[0].indexOf(cap);
681
+ if (matchStart < 0)
682
+ continue;
683
+ const globalStart = win.offset + matchStart;
684
+ const globalEnd = globalStart + cap.length;
685
+ const keyName = p.rule_id === "env_key_value" ? m[1] : undefined;
686
+ if (INERT_GATED_RULES.has(p.rule_id) && isInertValue(cap))
687
+ continue;
688
+ if (p.rule_id === "env_key_value") {
689
+ const ENV_KV_MIN_LEN = 12;
690
+ const ENV_KV_MIN_ENTROPY = 3.5;
691
+ if (cap.length < ENV_KV_MIN_LEN)
692
+ continue;
693
+ if (shannonEntropy(cap) < ENV_KV_MIN_ENTROPY)
694
+ continue;
695
+ }
696
+ raw.push({
697
+ rule_id: p.rule_id,
698
+ start: globalStart,
699
+ end: globalEnd,
700
+ matched_text: cap,
701
+ key_name: keyName,
702
+ confidence: "high"
703
+ });
704
+ }
705
+ }
706
+ const kvHits = scanKeyValue(win.text);
707
+ for (const h of kvHits) {
708
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
709
+ }
710
+ for (const h of scanDbUris(win.text)) {
711
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
712
+ }
713
+ for (const h of scanMemorablePasswords(win.text)) {
714
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
715
+ }
716
+ const genHits = scanGenericSecrets(win.text);
717
+ for (const h of genHits) {
718
+ raw.push({ ...h, start: h.start + win.offset, end: h.end + win.offset });
719
+ }
720
+ }
721
+ const deduped = dedupeRaw(raw);
722
+ const final = dropOverlaps(deduped);
723
+ const existing = new Set;
724
+ const out = [];
725
+ for (const h of final) {
726
+ const suggested_slug = deriveSlug({ key_name: h.key_name, rule_id: h.rule_id }, existing);
727
+ existing.add(suggested_slug);
728
+ out.push({
729
+ rule_id: h.rule_id,
730
+ matched_text: h.matched_text,
731
+ start: h.start,
732
+ end: h.end,
733
+ confidence: h.confidence,
734
+ suppressed: isSuppressed(text, h.start, h.end),
735
+ suggested_slug,
736
+ key_name: h.key_name
737
+ });
738
+ }
739
+ out.sort((a, b) => a.start - b.start);
740
+ return out;
741
+ }
742
+ function dedupeRaw(raw) {
743
+ const seen = new Map;
744
+ for (const h of raw) {
745
+ const key = `${h.start}:${h.end}`;
746
+ const existing = seen.get(key);
747
+ if (!existing) {
748
+ seen.set(key, h);
749
+ continue;
750
+ }
751
+ if (existing.confidence === "ambiguous" && h.confidence === "high") {
752
+ seen.set(key, h);
753
+ }
754
+ }
755
+ return Array.from(seen.values());
756
+ }
757
+ function dropOverlaps(hits) {
758
+ const out = hits.filter((h) => !(h.confidence === "ambiguous" && hits.some((o) => o !== h && o.start <= h.start && o.end >= h.end && !(o.start === h.start && o.end === h.end))));
759
+ out.sort((a, b) => a.start - b.start || a.end - b.end);
760
+ return out;
761
+ }
762
+
763
+ // src/self-improve/pii-scan.ts
764
+ function maskEmail(s) {
765
+ const at = s.indexOf("@");
766
+ if (at <= 0)
767
+ return "[email]";
768
+ const local = s.slice(0, at);
769
+ const domain = s.slice(at + 1);
770
+ const head = local.slice(0, 1);
771
+ const dot = domain.lastIndexOf(".");
772
+ const tld = dot >= 0 ? domain.slice(dot) : "";
773
+ return `${head}***@***${tld}`;
774
+ }
775
+ function maskDigits(s) {
776
+ const digits = s.replace(/\D/g, "");
777
+ if (digits.length <= 4)
778
+ return "***";
779
+ return `***${digits.slice(-4)}`;
780
+ }
781
+ var EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\.[A-Za-z]{2,}/g;
782
+ var PHONE_E164_RE = /\+[1-9]\d{9,14}(?!\d)/g;
783
+ var PHONE_SEP_RE = /(?<!\d)(?:\(\d{3}\)[\s.-]?|\d{3}[\s.-])\d{3}[\s.-]\d{4}(?!\d)/g;
784
+ var SSN_RE = /(?<!\d)\d{3}-\d{2}-\d{4}(?!\d)/g;
785
+ var CARD_CANDIDATE_RE = /(?<![\d-])(?:\d[ -]?){13,19}(?![\d-])/g;
786
+ function luhnValid(digits) {
787
+ if (digits.length < 13 || digits.length > 19)
788
+ return false;
789
+ let sum = 0;
790
+ let alt = false;
791
+ for (let i = digits.length - 1;i >= 0; i--) {
792
+ let d = digits.charCodeAt(i) - 48;
793
+ if (d < 0 || d > 9)
794
+ return false;
795
+ if (alt) {
796
+ d *= 2;
797
+ if (d > 9)
798
+ d -= 9;
799
+ }
800
+ sum += d;
801
+ alt = !alt;
802
+ }
803
+ return sum % 10 === 0;
804
+ }
805
+ function scanForPII(text) {
806
+ const findings = [];
807
+ if (typeof text !== "string" || text.length === 0) {
808
+ return { ok: true, findings };
809
+ }
810
+ try {
811
+ for (const d of detectSecrets(text)) {
812
+ findings.push({
813
+ kind: "secret",
814
+ rule: d.rule_id,
815
+ excerpt: `[secret:${d.rule_id}]`
816
+ });
817
+ }
818
+ } catch (err) {
819
+ findings.push({
820
+ kind: "scan-error",
821
+ excerpt: `secret scan failed: ${err.message}`.slice(0, 120)
822
+ });
823
+ }
824
+ try {
825
+ for (const m of text.matchAll(EMAIL_RE)) {
826
+ findings.push({ kind: "email", excerpt: maskEmail(m[0]) });
827
+ }
828
+ for (const m of text.matchAll(PHONE_E164_RE)) {
829
+ findings.push({ kind: "phone", excerpt: maskDigits(m[0]) });
830
+ }
831
+ for (const m of text.matchAll(PHONE_SEP_RE)) {
832
+ findings.push({ kind: "phone", excerpt: maskDigits(m[0]) });
833
+ }
834
+ for (const m of text.matchAll(SSN_RE)) {
835
+ findings.push({ kind: "ssn", excerpt: maskDigits(m[0]) });
836
+ }
837
+ for (const m of text.matchAll(CARD_CANDIDATE_RE)) {
838
+ const digits = m[0].replace(/\D/g, "");
839
+ if (luhnValid(digits)) {
840
+ findings.push({ kind: "card", excerpt: maskDigits(m[0]) });
841
+ }
842
+ }
843
+ } catch (err) {
844
+ findings.push({
845
+ kind: "scan-error",
846
+ excerpt: `pii scan failed: ${err.message}`.slice(0, 120)
847
+ });
848
+ }
849
+ return { ok: findings.length === 0, findings };
850
+ }
851
+
852
+ // src/self-improve/pending-queue.ts
853
+ import {
854
+ closeSync,
855
+ existsSync as existsSync2,
856
+ mkdirSync as mkdirSync2,
857
+ openSync,
858
+ readFileSync as readFileSync2,
859
+ writeSync
860
+ } from "node:fs";
861
+ import { join as join3 } from "node:path";
862
+ import { randomUUID } from "node:crypto";
863
+ var PENDING_FILE = "self-improve-pending.jsonl";
864
+ function pendingPath(stateDir) {
865
+ return join3(stateDir, PENDING_FILE);
866
+ }
867
+ function readPending(stateDir) {
868
+ const p = pendingPath(stateDir);
869
+ if (!existsSync2(p))
870
+ return [];
871
+ let raw;
872
+ try {
873
+ raw = readFileSync2(p, "utf-8");
874
+ } catch {
875
+ return [];
876
+ }
877
+ const out = [];
878
+ for (const line of raw.split(`
879
+ `)) {
880
+ if (!line.trim())
881
+ continue;
882
+ try {
883
+ const parsed = JSON.parse(line);
884
+ if (parsed && typeof parsed.id === "string" && parsed.tier) {
885
+ out.push(parsed);
886
+ }
887
+ } catch {}
888
+ }
889
+ return out;
890
+ }
891
+ function outstandingCount(stateDir) {
892
+ return readPending(stateDir).filter((s) => s.actioned !== true).length;
893
+ }
894
+ function enqueuePending(stateDir, input, opts = {}) {
895
+ const cfg = resolveSelfImproveConfig();
896
+ const cap = opts.cap ?? cfg.maxOutstandingPending;
897
+ const now = opts.now ?? Date.now;
898
+ ensureDir(stateDir);
899
+ const outstanding = outstandingCount(stateDir);
900
+ if (outstanding >= cap) {
901
+ return { ok: false, reason: "cap-reached", outstanding, cap };
902
+ }
903
+ const suggestion = {
904
+ id: randomUUID(),
905
+ created_at: new Date(now()).toISOString(),
906
+ ...input
907
+ };
908
+ const fd = openSync(pendingPath(stateDir), "a");
909
+ try {
910
+ writeSync(fd, JSON.stringify(suggestion) + `
911
+ `);
912
+ } finally {
913
+ closeSync(fd);
914
+ }
915
+ return { ok: true, suggestion };
916
+ }
917
+ function ensureDir(stateDir) {
918
+ if (!existsSync2(stateDir)) {
919
+ mkdirSync2(stateDir, { recursive: true, mode: 493 });
920
+ }
921
+ }
922
+
923
+ // src/self-improve/eval-cases.ts
924
+ function writeAtomic(path, content) {
925
+ mkdirSync3(dirname(path), { recursive: true });
926
+ const tmp = `${path}.tmp-${process.pid}-${Date.now()}`;
927
+ writeFileSync2(tmp, content, { encoding: "utf-8", mode: 420 });
928
+ renameSync(tmp, path);
929
+ }
930
+ var EVAL_MANIFEST_FILE = "self-improve-eval-manifest.json";
931
+ var EVAL_BASELINE_SUBDIR = "self-improve-eval-baselines";
932
+ function manifestPath(stateDir) {
933
+ return join4(stateDir, EVAL_MANIFEST_FILE);
934
+ }
935
+ function baselineEvalsPath(stateDir, slug) {
936
+ return join4(stateDir, EVAL_BASELINE_SUBDIR, slug, "evals.json");
937
+ }
938
+ function evalsSha256(skillDir) {
939
+ const p = evalsJsonPath(skillDir);
940
+ if (!existsSync3(p))
941
+ return null;
942
+ try {
943
+ return createHash("sha256").update(readFileSync3(p)).digest("hex");
944
+ } catch {
945
+ return null;
946
+ }
947
+ }
948
+ function readManifest(stateDir) {
949
+ const p = manifestPath(stateDir);
950
+ if (!existsSync3(p))
951
+ return {};
952
+ try {
953
+ const parsed = JSON.parse(readFileSync3(p, "utf-8"));
954
+ return parsed && typeof parsed === "object" ? parsed : {};
955
+ } catch {
956
+ return {};
957
+ }
958
+ }
959
+ function writeManifest(stateDir, m) {
960
+ writeAtomic(manifestPath(stateDir), JSON.stringify(m, null, 2) + `
961
+ `);
962
+ }
963
+ function recordEvalBaseline(stateDir, slug, skillDir, opts = {}) {
964
+ const sha = evalsSha256(skillDir);
965
+ if (sha === null)
966
+ return;
967
+ const now = opts.now ?? Date.now;
968
+ const m = readManifest(stateDir);
969
+ m[slug] = { sha256: sha, updated_at: new Date(now()).toISOString(), skill_dir: skillDir };
970
+ writeManifest(stateDir, m);
971
+ const snap = baselineEvalsPath(stateDir, slug);
972
+ mkdirSync3(dirname(snap), { recursive: true });
973
+ copyFileSync(evalsJsonPath(skillDir), snap);
974
+ }
975
+ function evalsBaselineTrusted(skillDir) {
976
+ const p = evalsJsonPath(skillDir);
977
+ let text;
978
+ try {
979
+ text = readFileSync3(p, "utf-8");
980
+ } catch {
981
+ return false;
982
+ }
983
+ return scanForPII(text).ok;
984
+ }
985
+ function verifyEvalIntegrity(stateDir, slug, skillDir) {
986
+ const sha = evalsSha256(skillDir);
987
+ if (sha === null)
988
+ return "no-evals";
989
+ const m = readManifest(stateDir);
990
+ const entry = m[slug];
991
+ if (!entry)
992
+ return "missing";
993
+ return sha === entry.sha256 ? "ok" : "drift";
994
+ }
995
+ function listSkillsWithEvals(skillsRoot) {
996
+ const out = [];
997
+ let entries;
998
+ try {
999
+ entries = readdirSync(skillsRoot);
1000
+ } catch {
1001
+ return out;
1002
+ }
1003
+ for (const slug of entries) {
1004
+ const skillDir = join4(skillsRoot, slug);
1005
+ if (existsSync3(evalsJsonPath(skillDir)))
1006
+ out.push({ slug, skillDir });
1007
+ }
1008
+ return out;
1009
+ }
1010
+ function sweepEvalIntegrity(skillsRoot, stateDir, opts = {}) {
1011
+ const report = {
1012
+ recorded: [],
1013
+ healed: [],
1014
+ ok: [],
1015
+ quarantined: []
1016
+ };
1017
+ const adoptIfTrusted = (slug, skillDir) => {
1018
+ if (evalsBaselineTrusted(skillDir)) {
1019
+ recordEvalBaseline(stateDir, slug, skillDir, opts);
1020
+ report.recorded.push(slug);
1021
+ } else {
1022
+ report.quarantined.push(slug);
1023
+ }
1024
+ };
1025
+ for (const { slug, skillDir } of listSkillsWithEvals(skillsRoot)) {
1026
+ try {
1027
+ const status = verifyEvalIntegrity(stateDir, slug, skillDir);
1028
+ if (status === "missing") {
1029
+ adoptIfTrusted(slug, skillDir);
1030
+ } else if (status === "drift") {
1031
+ const snap = baselineEvalsPath(stateDir, slug);
1032
+ if (existsSync3(snap)) {
1033
+ copyFileSync(snap, evalsJsonPath(skillDir));
1034
+ recordEvalBaseline(stateDir, slug, skillDir, opts);
1035
+ report.healed.push(slug);
1036
+ } else {
1037
+ adoptIfTrusted(slug, skillDir);
1038
+ }
1039
+ } else {
1040
+ report.ok.push(slug);
1041
+ }
1042
+ } catch {}
1043
+ }
1044
+ return report;
1045
+ }
1046
+ var SWEEP_TAMPER_SIGNAL = "eval-integrity-sweep";
1047
+ var SWEEP_HEALED_SIGNAL = "drift-reverted";
1048
+ var SWEEP_QUARANTINED_SIGNAL = "quarantined";
1049
+ function hasOutstandingSweepNote(pending, slug, kindSignal) {
1050
+ return pending.some((p) => p.actioned !== true && p.skill_slug === slug && Array.isArray(p.triggered_by) && p.triggered_by.includes(SWEEP_TAMPER_SIGNAL) && p.triggered_by.includes(kindSignal));
1051
+ }
1052
+ function surfaceSweepTamper(stateDir, report, opts = {}) {
1053
+ const enqueued = { healed: [], quarantined: [] };
1054
+ if (report.healed.length === 0 && report.quarantined.length === 0) {
1055
+ return enqueued;
1056
+ }
1057
+ let pending;
1058
+ try {
1059
+ pending = readPending(stateDir);
1060
+ } catch {
1061
+ pending = [];
1062
+ }
1063
+ const tryEnqueue = (slug, kindSignal, lesson, proposed_change, tier_reason, bucket) => {
1064
+ if (hasOutstandingSweepNote(pending, slug, kindSignal))
1065
+ return;
1066
+ try {
1067
+ const q = enqueuePending(stateDir, {
1068
+ tier: "T2",
1069
+ lesson,
1070
+ proposed_change,
1071
+ tier_reason,
1072
+ skill_slug: slug,
1073
+ triggered_by: [SWEEP_TAMPER_SIGNAL, kindSignal]
1074
+ }, { now: opts.now });
1075
+ if (q.ok) {
1076
+ pending.push(q.suggestion);
1077
+ bucket.push(slug);
1078
+ }
1079
+ } catch {}
1080
+ };
1081
+ for (const slug of report.healed) {
1082
+ tryEnqueue(slug, SWEEP_HEALED_SIGNAL, `Eval-integrity sweep reverted out-of-band drift in skill "${slug}" (evals/evals.json) to the sanctioned baseline.`, `Confirm the change to ${slug}/evals/evals.json was intended. A Bash/Write edit that bypasses the applier is auto-reverted every turn; re-add the case via \`switchroom self-improve add-eval-case\` so it becomes a sanctioned baseline.`, `eval-integrity sweep detected + healed out-of-band tamper (RFC invariant 3(b))`, enqueued.healed);
1083
+ }
1084
+ for (const slug of report.quarantined) {
1085
+ tryEnqueue(slug, SWEEP_QUARANTINED_SIGNAL, `Eval-integrity sweep QUARANTINED skill "${slug}": an un-sanctioned evals.json failed the PII/secret scan and was NOT adopted as a baseline.`, `Review ${slug}/evals/evals.json for PII/secrets or corruption \u2014 the sweep refuses to trust it. Re-add clean cases via \`switchroom self-improve add-eval-case\`.`, `eval-integrity sweep quarantined un-sanctioned evals.json (RFC invariant 3(b), MAJOR 2)`, enqueued.quarantined);
1086
+ }
1087
+ return enqueued;
1088
+ }
1089
+
1090
+ // src/memory/hindsight-retain-provenance.ts
1091
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "node:fs";
1092
+ import { join as join5 } from "node:path";
1093
+ var RETAIN_PROVENANCE_TAG = "source:transcript";
1094
+ var RETAIN_TAGS_DEFAULT = Object.freeze([
1095
+ "{session_id}",
1096
+ RETAIN_PROVENANCE_TAG
1097
+ ]);
1098
+ var SELF_IMPROVE_CORRECTION_PENDING_FILE = "self-improve-correction-pending";
1099
+ function correctionPendingPath(stateDir) {
1100
+ return join5(stateDir, SELF_IMPROVE_CORRECTION_PENDING_FILE);
1101
+ }
1102
+ function writeCorrectionPending(stateDir) {
1103
+ if (!existsSync4(stateDir)) {
1104
+ mkdirSync4(stateDir, { recursive: true, mode: 493 });
1105
+ }
1106
+ writeFileSync3(correctionPendingPath(stateDir), new Date().toISOString(), "utf-8");
1107
+ }
1108
+
240
1109
  // src/cli/self-improve-stop.ts
241
1110
  var SCAN_WINDOW = 40;
242
1111
  function readStdin() {
243
1112
  try {
244
- return readFileSync2(0, "utf8");
1113
+ return readFileSync4(0, "utf8");
245
1114
  } catch {
246
1115
  return "";
247
1116
  }
@@ -309,7 +1178,7 @@ function readTranscript(path) {
309
1178
  return [];
310
1179
  let raw;
311
1180
  try {
312
- raw = readFileSync2(path, "utf8");
1181
+ raw = readFileSync4(path, "utf8");
313
1182
  } catch {
314
1183
  return [];
315
1184
  }
@@ -328,14 +1197,14 @@ function readTranscript(path) {
328
1197
  return out.length > SCAN_WINDOW ? out.slice(-SCAN_WINDOW) : out;
329
1198
  }
330
1199
  function resolveStateDir() {
331
- return process.env.TELEGRAM_STATE_DIR ?? join2(homedir(), ".claude", "channels", "telegram");
1200
+ return process.env.TELEGRAM_STATE_DIR ?? join6(homedir(), ".claude", "channels", "telegram");
332
1201
  }
333
1202
  function resolveSocketPath() {
334
1203
  if (process.env.SWITCHROOM_GATEWAY_SOCKET) {
335
1204
  return process.env.SWITCHROOM_GATEWAY_SOCKET;
336
1205
  }
337
- const stateDir = process.env.TELEGRAM_STATE_DIR ?? join2(homedir(), ".claude", "channels", "telegram");
338
- return join2(stateDir, "gateway.sock");
1206
+ const stateDir = process.env.TELEGRAM_STATE_DIR ?? join6(homedir(), ".claude", "channels", "telegram");
1207
+ return join6(stateDir, "gateway.sock");
339
1208
  }
340
1209
  function resolveChatId() {
341
1210
  return process.env.SWITCHROOM_SELF_IMPROVE_CHAT_ID ?? process.env.SWITCHROOM_DEFAULT_CHAT_ID ?? "self-improve";
@@ -387,9 +1256,17 @@ function injectReview(agentName, text, sessionId) {
387
1256
  });
388
1257
  sock.on("error", () => done());
389
1258
  }
1259
+ function resolveSkillsRoot() {
1260
+ return join6(homedir(), ".claude", "skills");
1261
+ }
390
1262
  function main() {
391
1263
  if (!selfImproveEnabled())
392
1264
  process.exit(0);
1265
+ try {
1266
+ const stateDir = resolveStateDir();
1267
+ const report = sweepEvalIntegrity(resolveSkillsRoot(), stateDir);
1268
+ surfaceSweepTamper(stateDir, report);
1269
+ } catch {}
393
1270
  const raw = readStdin().trim();
394
1271
  if (!raw)
395
1272
  process.exit(0);
@@ -411,6 +1288,11 @@ function main() {
411
1288
  const gate = runGate(scanMessages);
412
1289
  if (!gate.tripped)
413
1290
  process.exit(0);
1291
+ if (gate.signals.some((s) => s.kind === "operator-correction")) {
1292
+ try {
1293
+ writeCorrectionPending(resolveStateDir());
1294
+ } catch {}
1295
+ }
414
1296
  const agentName = process.env.SWITCHROOM_AGENT_NAME ?? "";
415
1297
  if (!agentName)
416
1298
  process.exit(0);