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.
- package/dist/agent-scheduler/index.js +65 -2
- package/dist/auth-broker/index.js +185 -23
- package/dist/cli/notion-write-pretool.mjs +65 -2
- package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
- package/dist/cli/self-improve-stop.mjs +889 -7
- package/dist/cli/skill-validate-pretool.mjs +82 -3
- package/dist/cli/switchroom.js +3669 -2107
- package/dist/host-control/main.js +67 -4
- package/dist/vault/approvals/kernel-server.js +66 -3
- package/dist/vault/broker/server.js +66 -3
- package/package.json +1 -1
- package/profiles/_shared/agent-self-service.md.hbs +15 -22
- package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
- package/profiles/_shared/dev-protocol.md.hbs +1 -1
- package/profiles/_shared/execution-discipline.md.hbs +4 -4
- package/profiles/_shared/vault-protocol.md.hbs +2 -18
- package/profiles/default/CLAUDE.md.hbs +3 -5
- package/telegram-plugin/auto-fallback-fleet.ts +37 -2
- package/telegram-plugin/dist/gateway/gateway.js +1405 -918
- package/telegram-plugin/fallback-card-collapse.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +11 -1
- package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
- package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
- package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
- package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
- package/telegram-plugin/gateway/gateway.ts +85 -90
- package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/narrative-lane.ts +33 -2
- package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
- package/telegram-plugin/gateway/privacy-reset.ts +87 -0
- package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
- package/telegram-plugin/gateway/privacy-state.ts +206 -0
- package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
- package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
- package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
- package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
- package/telegram-plugin/slot-banner-driver.ts +42 -5
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
- package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
- package/telegram-plugin/throttle-tier.ts +12 -3
- package/telegram-plugin/turn-flush-safety.ts +97 -0
- package/vendor/hindsight-memory/scripts/retain.py +306 -0
- package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
- package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
- 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
|
|
3
|
-
import { join as
|
|
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
|
|
56
|
-
import { join as
|
|
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/
|
|
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
|
|
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 (!
|
|
462
|
+
if (!existsSync5(p))
|
|
208
463
|
return 0;
|
|
209
464
|
let raw;
|
|
210
465
|
try {
|
|
211
|
-
raw =
|
|
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 (!
|
|
231
|
-
|
|
485
|
+
if (!existsSync5(stateDir)) {
|
|
486
|
+
mkdirSync4(stateDir, { recursive: true, mode: 493 });
|
|
232
487
|
}
|
|
233
488
|
const ms = now();
|
|
234
|
-
const fd =
|
|
489
|
+
const fd = openSync3(appliesPath(stateDir), "a");
|
|
235
490
|
try {
|
|
236
|
-
|
|
491
|
+
writeSync3(fd, JSON.stringify({ ts: ms, day: utcDay(ms) }) + `
|
|
237
492
|
`);
|
|
238
493
|
} finally {
|
|
239
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 ??
|
|
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)`;
|