glimmervoid 0.35.1 → 0.36.0

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 (38) hide show
  1. package/README.md +2 -0
  2. package/dist/bin/glimmervoid.js +17 -12
  3. package/dist/chunks/{adapters-B37wqNti.js → adapters-BVozg-Lg.js} +1 -1
  4. package/dist/chunks/{adapters-CADejSpq.js → adapters-BdnLyJjf.js} +10 -49
  5. package/dist/chunks/{agent-api-cli-CRj_FYbz.js → agent-api-cli-DIqRLvu3.js} +1 -1
  6. package/dist/chunks/{agent-api-cli-vGwstJ1j.js → agent-api-cli-d-Bgsshx.js} +3 -3
  7. package/dist/chunks/{agent-api-core-D1233u1U.js → agent-api-core-BUC9WND7.js} +2 -2
  8. package/dist/chunks/{agent-setup-cli-BTAFcckd.js → agent-setup-cli-CH7EHsAX.js} +1 -1
  9. package/dist/chunks/{config-rPbbL4nW.js → config-CuZ16F9W.js} +47 -771
  10. package/dist/chunks/config-store-LqQ7MuyV.js +4181 -0
  11. package/dist/chunks/config-store-tVmt_nfm.js +1 -0
  12. package/dist/chunks/{contracts-DjIgIuDk.js → contracts-BLZAOP57.js} +55 -6
  13. package/dist/chunks/{grok-KKgdSe-7.js → grok-Ch4-35_r.js} +3 -3
  14. package/dist/chunks/{hook-command-core-DclEoE9e.js → hook-command-core-qTNK-xMF.js} +4 -1
  15. package/dist/chunks/{hook-tools-UX2fmt81.js → hook-tools-BkxllUHE.js} +9 -16
  16. package/dist/chunks/{knowledge-graph-cli-Rj5nj87L.js → knowledge-graph-cli-DD7QvcFd.js} +1 -1
  17. package/dist/chunks/{main-C-T2Uk20.js → main-Rmo83Ow-.js} +3075 -832
  18. package/dist/chunks/{pair-cli-Dcyzv3In.js → pair-cli-DzFeAmq5.js} +2 -2
  19. package/dist/chunks/{remote-config-Dd1T-VJA.js → remote-config-CPdSF2Xi.js} +1 -1
  20. package/dist/chunks/{rtk-hook-core-BmEz2aq9.js → rtk-hook-core-VrUafGAp.js} +15 -5
  21. package/dist/chunks/rtk-resolver-B8BPgN9L.js +1 -0
  22. package/dist/chunks/{rtk-resolver-B9pzjLsM.js → rtk-resolver-C_micGrL.js} +1 -1
  23. package/dist/chunks/session-B8RhqQJe.js +986 -0
  24. package/dist/chunks/{session-routes-BYy3am9m.js → session-routes-DB5aDKsx.js} +1 -1
  25. package/dist/chunks/settings-cli-C2TY6hl-.js +484 -0
  26. package/dist/chunks/{ssh-sign-cli-DPHpT22B.js → ssh-sign-cli-C2nUwJC-.js} +2 -2
  27. package/dist/client/assets/index-BIaSyXwH.css +1 -0
  28. package/dist/client/assets/index-Ddu7M4Ad.js +259 -0
  29. package/dist/client/index.html +5 -3
  30. package/dist/server/index.js +1 -1
  31. package/dist/session/hook-tool-relay.js +3 -3
  32. package/package.json +3 -3
  33. package/dist/chunks/config-store-BeIxAZNn.js +0 -1
  34. package/dist/chunks/config-store-BtsYbNC3.js +0 -1713
  35. package/dist/chunks/rtk-resolver-SWU5VVM7.js +0 -1
  36. package/dist/chunks/session-D6xXN0UU.js +0 -232
  37. package/dist/client/assets/index-CUxRHbCS.js +0 -254
  38. package/dist/client/assets/index-DE2GUmqs.css +0 -1
@@ -0,0 +1,4181 @@
1
+ import { n as errorLabel, r as errorMessage } from "./text-DoirglWV.js";
2
+ import { n as equalsIgnoringCaseOnWindows } from "./paths-Bu4eH1Yd.js";
3
+ import { n as glimmervoidHomeDir$1, t as decideConfigPath } from "./config-path-core-DFt-TemS.js";
4
+ import { n as isRecord } from "./coerce-NSst8oRP.js";
5
+ import { d as writeJsonAtomicSync, f as writeTextAtomic, p as writeTextAtomicSync } from "./json-file-BaxGcB2R.js";
6
+ import { A as KEEP_MERGEABLE_TIMEOUT_MINUTES_RANGE, B as USAGE_BUDGET_RANGE, C as normalizeLegacyConfigNumbers, D as FLYING_ANIMALS_DURATION_RANGE, E as FLYING_ANIMALS_DEFAULTS, F as POSTHOG_TRAFFIC_BASELINE_RANGE, G as WORKFLOWS_MAX_CONCURRENT_SESSIONS_RANGE, H as USAGE_RETAIN_DAYS_RANGE, I as POSTHOG_TRAFFIC_MIN_USERS_RANGE, L as POSTHOG_TRAFFIC_MULTIPLIER_RANGE, M as PORT_RANGE, N as POSITIVE_NUMBER_RANGE, O as FLYING_ANIMALS_GAP_RANGE, P as POSTHOG_FIX_TIMEOUT_RANGE, R as REPLAY_BUFFER_KB_RANGE, T as FACTORY_MAX_LIVE_WORKERS_RANGE, U as USAGE_SCAN_INTERVAL_RANGE, V as USAGE_INTEGER_RANGES, W as WORKFLOWS_MAX_ACTIONS_PER_POLL_RANGE, d as CHANGE_MAP_NARRATOR_ENGINES, f as DEFAULT_FACTORY_CHECKS, h as UPDATE_CHANNELS, i as Config, j as NON_NEGATIVE_NUMBER_RANGE, k as FLYING_ANIMALS_SCALE_RANGE, l as configIssueMessage, m as POST_TURN_CHECK_MODES, n as BUILTIN_AGENT_IDS, p as DEFAULT_FACTORY_PROTECTED_PATHS, s as RUNTIME_CONFIG_SCALAR_KEYS, t as AGENT_ID_SHAPE_MESSAGE, u as BranchGcFileSettings, w as CHANGE_MAP_NARRATOR_TIMEOUT_RANGE, x as USAGE_COST_MODES, z as TASK_TITLE_TIMEOUT_RANGE } from "./config-CuZ16F9W.js";
7
+ import { V as FeatureFlagsResponse, W as TELEMETRY_KILL_SWITCH_FLAG } from "./contracts-BLZAOP57.js";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+ import { promisify } from "node:util";
12
+ import crypto, { createHash } from "node:crypto";
13
+ //#region shared/canonical-paths.ts
14
+ var realpathNative = promisify(fs.realpath.native);
15
+ function canonicalizePath(p) {
16
+ try {
17
+ return fs.realpathSync.native(p);
18
+ } catch {
19
+ return p;
20
+ }
21
+ }
22
+ function isSameDirectoryPath(a, b) {
23
+ const resolvedA = path.resolve(String(a || ""));
24
+ const resolvedB = path.resolve(String(b || ""));
25
+ if (equalsIgnoringCaseOnWindows(resolvedA, resolvedB)) return true;
26
+ if (process.platform !== "win32" && process.platform !== "darwin") return false;
27
+ return equalsIgnoringCaseOnWindows(canonicalizePath(resolvedA), canonicalizePath(resolvedB));
28
+ }
29
+ async function comparableDirectoryPath(candidatePath) {
30
+ const resolvedPath = path.resolve(String(candidatePath || ""));
31
+ let canonicalPath;
32
+ try {
33
+ canonicalPath = await realpathNative(resolvedPath);
34
+ } catch {
35
+ canonicalPath = resolvedPath;
36
+ }
37
+ if (process.platform !== "win32") return canonicalPath;
38
+ return canonicalPath.toLowerCase();
39
+ }
40
+ //#endregion
41
+ //#region server/core/branch-gc-core.ts
42
+ var SESSION_BRANCH_PREFIX = "glimmervoid/session/";
43
+ var DEFAULT_BRANCH_GC_PREFIXES = [SESSION_BRANCH_PREFIX, "worktree-agent-"];
44
+ var DAY_MS = 864e5;
45
+ var MERGED_DELETION_REASON = "merged-into-integration";
46
+ var STALE_DELETION_REASON = "stale-orphan";
47
+ var UNMERGED_KEPT_REASON = "not-merged-and-unmerged-deletion-off";
48
+ function sessionIdFromBranch(branchName) {
49
+ if (!branchName.startsWith(SESSION_BRANCH_PREFIX)) return null;
50
+ return branchName.slice(20);
51
+ }
52
+ function usablePrefixes(prefixes) {
53
+ return prefixes.filter((prefix) => prefix.length > 0);
54
+ }
55
+ function matchedPrefix(name, prefixes) {
56
+ for (const prefix of usablePrefixes(prefixes)) if (name.startsWith(prefix)) return prefix;
57
+ return null;
58
+ }
59
+ function protectedBranchNames(integrationTips) {
60
+ return /* @__PURE__ */ new Set([
61
+ "main",
62
+ "master",
63
+ ...integrationTips.map((tip) => tip.branch)
64
+ ]);
65
+ }
66
+ function planBranchGc({ remoteBranches = [], integrationTips = [], liveSessionIds = /* @__PURE__ */ new Set(), prefixes = DEFAULT_BRANCH_GC_PREFIXES, nowMs, staleDays = 14, deleteUnmerged = false }) {
67
+ const deletions = [];
68
+ const kept = [];
69
+ const protectedNames = protectedBranchNames(integrationTips);
70
+ const staleBeforeMs = nowMs - staleDays * DAY_MS;
71
+ for (const remoteBranch of remoteBranches) {
72
+ const { name, tipSha } = remoteBranch;
73
+ if (matchedPrefix(name, prefixes) === null) {
74
+ kept.push({
75
+ name,
76
+ reason: "foreign-prefix"
77
+ });
78
+ continue;
79
+ }
80
+ if (protectedNames.has(name)) {
81
+ kept.push({
82
+ name,
83
+ reason: "not-merged-and-fresh"
84
+ });
85
+ continue;
86
+ }
87
+ const sessionId = sessionIdFromBranch(name);
88
+ if (sessionId !== null && liveSessionIds.has(sessionId)) {
89
+ kept.push({
90
+ name,
91
+ reason: "live-session"
92
+ });
93
+ continue;
94
+ }
95
+ if (remoteBranch.mergedIntoIntegration === true) {
96
+ deletions.push({
97
+ name,
98
+ reason: remoteBranch.mergedReason ?? MERGED_DELETION_REASON,
99
+ tipSha
100
+ });
101
+ continue;
102
+ }
103
+ if (!deleteUnmerged) {
104
+ kept.push({
105
+ name,
106
+ reason: UNMERGED_KEPT_REASON
107
+ });
108
+ continue;
109
+ }
110
+ if (Number.isFinite(remoteBranch.tipCommitTimeMs) && remoteBranch.tipCommitTimeMs < staleBeforeMs) {
111
+ deletions.push({
112
+ name,
113
+ reason: STALE_DELETION_REASON,
114
+ tipSha
115
+ });
116
+ continue;
117
+ }
118
+ kept.push({
119
+ name,
120
+ reason: "not-merged-and-fresh"
121
+ });
122
+ }
123
+ return {
124
+ deletions,
125
+ kept
126
+ };
127
+ }
128
+ function worktreeIntegrationTips({ projectTips, worktreeOwnTips }) {
129
+ const tipBySha = /* @__PURE__ */ new Map();
130
+ for (const tip of [...projectTips, ...worktreeOwnTips]) {
131
+ if (!tip.sha) continue;
132
+ if (tipBySha.has(tip.sha)) continue;
133
+ tipBySha.set(tip.sha, tip);
134
+ }
135
+ return [...tipBySha.values()];
136
+ }
137
+ function protectedWorktreeBranchNames(integrationTips, worktrees) {
138
+ const ownIntegrationBranches = worktrees.map((worktree) => worktree.integrationBranch).filter((branch) => Boolean(branch)).map((branch) => ({ branch }));
139
+ return protectedBranchNames([...integrationTips, ...ownIntegrationBranches]);
140
+ }
141
+ function planWorktreeGc({ worktrees, liveWorktreePaths, integrationTips, prefixes }) {
142
+ const removals = [];
143
+ const kept = [];
144
+ const protectedNames = protectedWorktreeBranchNames(integrationTips, worktrees);
145
+ for (const worktree of worktrees) {
146
+ const decision = (disposition, reason) => ({
147
+ cwd: worktree.cwd,
148
+ branch: worktree.branch,
149
+ disposition,
150
+ reason
151
+ });
152
+ if (worktree.prunable === true) {
153
+ kept.push(decision("keep", "missing-directory"));
154
+ continue;
155
+ }
156
+ if (worktree.locked) {
157
+ kept.push(decision("keep", "locked"));
158
+ continue;
159
+ }
160
+ if (liveWorktreePaths.has(worktree.cwd)) {
161
+ kept.push(decision("keep", "live-session"));
162
+ continue;
163
+ }
164
+ if (protectedNames.has(worktree.branch)) {
165
+ kept.push(decision("keep", "protected"));
166
+ continue;
167
+ }
168
+ if (matchedPrefix(worktree.branch, prefixes) === null) {
169
+ kept.push(decision("keep", "foreign-prefix"));
170
+ continue;
171
+ }
172
+ if (worktree.dirty === null) {
173
+ kept.push(decision("keep", "status-probe-failed"));
174
+ continue;
175
+ }
176
+ if (worktree.dirty) {
177
+ kept.push(decision("keep", "uncommitted-changes"));
178
+ continue;
179
+ }
180
+ if (worktree.atIntegrationTip === true) {
181
+ kept.push(decision("keep", "no-commits"));
182
+ continue;
183
+ }
184
+ if (worktree.merged === null) {
185
+ kept.push(decision("keep", "merge-proof-failed"));
186
+ continue;
187
+ }
188
+ if (worktree.merged !== true) {
189
+ kept.push(decision("keep", "unmerged"));
190
+ continue;
191
+ }
192
+ removals.push(decision("remove", "merged"));
193
+ }
194
+ return {
195
+ removals,
196
+ kept
197
+ };
198
+ }
199
+ //#endregion
200
+ //#region server/core/config-secrets-core.ts
201
+ var ENV_SECRET_BINDINGS = Object.freeze([{
202
+ blockName: "posthog",
203
+ secretKey: "apiKey",
204
+ environmentVariable: "GLIMMERVOID_POSTHOG_API_KEY"
205
+ }, {
206
+ blockName: "telegram",
207
+ secretKey: "botToken",
208
+ environmentVariable: "GLIMMERVOID_TELEGRAM_BOT_TOKEN"
209
+ }]);
210
+ function readEnvSecrets(env) {
211
+ const resolvedSecrets = [];
212
+ for (const binding of ENV_SECRET_BINDINGS) {
213
+ const value = env[binding.environmentVariable];
214
+ if (typeof value !== "string" || value.length === 0) continue;
215
+ resolvedSecrets.push({
216
+ ...binding,
217
+ value
218
+ });
219
+ }
220
+ return resolvedSecrets;
221
+ }
222
+ function copyOfBlock(config, blockName) {
223
+ const block = config[blockName];
224
+ if (!isRecord(block)) return {};
225
+ return { ...block };
226
+ }
227
+ function withEnvSecrets(config, envSecrets) {
228
+ if (envSecrets.length === 0) return config;
229
+ const overlaid = { ...config };
230
+ for (const secret of envSecrets) overlaid[secret.blockName] = {
231
+ ...copyOfBlock(overlaid, secret.blockName),
232
+ [secret.secretKey]: secret.value
233
+ };
234
+ return overlaid;
235
+ }
236
+ function withoutEnvSecrets(config, envSecrets) {
237
+ if (envSecrets.length === 0) return config;
238
+ const stripped = { ...config };
239
+ for (const secret of envSecrets) {
240
+ if (!isRecord(stripped[secret.blockName])) continue;
241
+ const remaining = copyOfBlock(stripped, secret.blockName);
242
+ delete remaining[secret.secretKey];
243
+ if (Object.keys(remaining).length === 0) {
244
+ delete stripped[secret.blockName];
245
+ continue;
246
+ }
247
+ stripped[secret.blockName] = remaining;
248
+ }
249
+ return stripped;
250
+ }
251
+ function withSortedKeys(value) {
252
+ if (Array.isArray(value)) return value.map(withSortedKeys);
253
+ if (!isRecord(value)) return value;
254
+ const sorted = {};
255
+ for (const key of Object.keys(value).sort()) sorted[key] = withSortedKeys(value[key]);
256
+ return sorted;
257
+ }
258
+ function stableConfigKey(value) {
259
+ return JSON.stringify(withSortedKeys(value));
260
+ }
261
+ //#endregion
262
+ //#region server/core/telemetry-core.ts
263
+ var POSTHOG_INGEST_HOST = "https://us.i.posthog.com";
264
+ var TELEMETRY_BATCH_URL = `${POSTHOG_INGEST_HOST}/batch/`;
265
+ var TELEMETRY_FLAGS_URL = `${POSTHOG_INGEST_HOST}/flags?v=2`;
266
+ var MAX_STACK_LENGTH = 16384;
267
+ var FIRST_RUN_NOTICE = ["Glimmervoid sends anonymous usage and error data to help improve it: app starts, daily activity, session starts and ends with the agent kind, exit kind and duration, errors as their type, error code and scrubbed stack frames, per session token counts, models and estimated cost, and factory outcomes as the kind of decision, how it came out, its duration and attempt number. No error messages, paths, repository names, prompts, objectives, model output or terminal output.", "Turn it off in Settings > Privacy, or set GLIMMERVOID_TELEMETRY=0 or DO_NOT_TRACK=1."].join("\n");
268
+ var TRUTHY_FLAGS = /* @__PURE__ */ new Set([
269
+ "1",
270
+ "true",
271
+ "yes"
272
+ ]);
273
+ var FALSY_FLAGS = /* @__PURE__ */ new Set([
274
+ "0",
275
+ "false",
276
+ "no",
277
+ "off"
278
+ ]);
279
+ function normalizedFlag(value) {
280
+ return (value ?? "").trim().toLowerCase();
281
+ }
282
+ function decideTelemetryConsent(env, config) {
283
+ if (TRUTHY_FLAGS.has(normalizedFlag(env.DO_NOT_TRACK))) return {
284
+ isEnabled: false,
285
+ source: "do-not-track"
286
+ };
287
+ if (FALSY_FLAGS.has(normalizedFlag(env.GLIMMERVOID_TELEMETRY))) return {
288
+ isEnabled: false,
289
+ source: "environment"
290
+ };
291
+ if (TRUTHY_FLAGS.has(normalizedFlag(env.CI))) return {
292
+ isEnabled: false,
293
+ source: "ci"
294
+ };
295
+ if (normalizedFlag(env.NODE_TEST_CONTEXT) !== "") return {
296
+ isEnabled: false,
297
+ source: "test-runner"
298
+ };
299
+ const configured = config.telemetry?.enabled;
300
+ if (typeof configured === "boolean") return {
301
+ isEnabled: configured,
302
+ source: "config"
303
+ };
304
+ return {
305
+ isEnabled: true,
306
+ source: "default"
307
+ };
308
+ }
309
+ function isTelemetryForcedOff(env) {
310
+ return decideTelemetryConsent(env, { telemetry: { enabled: true } }).isEnabled === false;
311
+ }
312
+ function resolveProjectToken(env) {
313
+ return env.GLIMMERVOID_TELEMETRY_PROJECT_TOKEN?.trim() || "phc_s68t9vWmeGcrWkx4NkLQaEiVZ9sMBxv8GjQBbAcix2Sf";
314
+ }
315
+ function toForwardSlashes(filePath) {
316
+ return filePath.replaceAll("\\", "/");
317
+ }
318
+ function isWindowsStylePath(filePath) {
319
+ return /^[A-Za-z]:\//.test(filePath);
320
+ }
321
+ function comparablePath(filePath) {
322
+ const forward = toForwardSlashes(filePath).replace(/\/+$/, "");
323
+ return isWindowsStylePath(forward) ? forward.toLowerCase() : forward;
324
+ }
325
+ function decodedFileUrlPath(fileUrl) {
326
+ const urlPath = fileUrl.slice(7).replace(/^\/([A-Za-z]:)/, "$1");
327
+ try {
328
+ return decodeURIComponent(urlPath);
329
+ } catch {
330
+ return urlPath;
331
+ }
332
+ }
333
+ function stripFileScheme(rawPath) {
334
+ if (!rawPath.startsWith("file://")) return toForwardSlashes(rawPath);
335
+ return toForwardSlashes(decodedFileUrlPath(rawPath));
336
+ }
337
+ function relativeUnder(filePath, root) {
338
+ if (!root) return null;
339
+ const comparableRoot = comparablePath(root);
340
+ if (!comparableRoot) return null;
341
+ if (!comparablePath(filePath).startsWith(`${comparableRoot}/`)) return null;
342
+ return toForwardSlashes(filePath).slice(comparableRoot.length + 1);
343
+ }
344
+ function scrubLocalPath(rawPath, packageRoot) {
345
+ const filePath = stripFileScheme(rawPath);
346
+ if (filePath.startsWith("node:")) return filePath;
347
+ const underPackage = relativeUnder(filePath, packageRoot);
348
+ if (underPackage !== null) return underPackage;
349
+ const nodeModulesAt = filePath.lastIndexOf("/node_modules/");
350
+ if (nodeModulesAt >= 0) return filePath.slice(nodeModulesAt + 1);
351
+ return filePath.slice(filePath.lastIndexOf("/") + 1);
352
+ }
353
+ function isInApp(rawPath, packageRoot) {
354
+ const filePath = stripFileScheme(rawPath);
355
+ if (filePath.startsWith("node:")) return false;
356
+ const underPackage = relativeUnder(filePath, packageRoot);
357
+ if (underPackage === null) return false;
358
+ return !underPackage.split("/").includes("node_modules");
359
+ }
360
+ var V8_FRAME_WITH_FUNCTION = /^\s*at (.+?) \((.+?):(\d+):(\d+)\)$/;
361
+ var V8_FRAME_ANONYMOUS = /^\s*at (.+?):(\d+):(\d+)$/;
362
+ function matchV8Frame(stackLine) {
363
+ const named = V8_FRAME_WITH_FUNCTION.exec(stackLine);
364
+ if (named) return {
365
+ functionName: named[1].replace(/^async /, ""),
366
+ filePath: named[2],
367
+ line: named[3],
368
+ column: named[4]
369
+ };
370
+ const anonymous = V8_FRAME_ANONYMOUS.exec(stackLine);
371
+ if (!anonymous) return null;
372
+ return {
373
+ functionName: "?",
374
+ filePath: anonymous[1].replace(/^async /, ""),
375
+ line: anonymous[2],
376
+ column: anonymous[3]
377
+ };
378
+ }
379
+ function parseV8StackFrames(stack, packageRoot, platform = "node:javascript") {
380
+ const frames = [];
381
+ for (const stackLine of stack.slice(0, MAX_STACK_LENGTH).split("\n")) {
382
+ const rawFrame = matchV8Frame(stackLine);
383
+ if (!rawFrame) continue;
384
+ frames.push({
385
+ platform,
386
+ function: rawFrame.functionName.slice(0, 512),
387
+ filename: scrubLocalPath(rawFrame.filePath, packageRoot).slice(0, 512),
388
+ lineno: Number(rawFrame.line),
389
+ colno: Number(rawFrame.column),
390
+ in_app: isInApp(rawFrame.filePath, packageRoot)
391
+ });
392
+ if (frames.length >= 64) break;
393
+ }
394
+ return frames.reverse();
395
+ }
396
+ var ERROR_CODE_SHAPE = /^[A-Z][A-Z0-9_]{0,63}$/;
397
+ function safeErrorCode(error) {
398
+ const code = Reflect.get(error, "code");
399
+ if (typeof code !== "string") return "";
400
+ return ERROR_CODE_SHAPE.test(code) ? code : "";
401
+ }
402
+ function errorParts(error) {
403
+ if (error instanceof Error) return {
404
+ type: error.name || "Error",
405
+ code: safeErrorCode(error),
406
+ stack: error.stack ?? ""
407
+ };
408
+ return {
409
+ type: "NonError",
410
+ code: "",
411
+ stack: ""
412
+ };
413
+ }
414
+ function exceptionPropertiesOf({ type, code, handled, level, frames }) {
415
+ return {
416
+ $exception_list: [{
417
+ type: type.slice(0, 512),
418
+ value: code,
419
+ mechanism: {
420
+ handled,
421
+ synthetic: false,
422
+ type: "generic"
423
+ },
424
+ stacktrace: {
425
+ type: "raw",
426
+ frames
427
+ }
428
+ }],
429
+ $exception_level: level
430
+ };
431
+ }
432
+ function buildExceptionProperties(error, { handled, packageRoot }) {
433
+ const parts = errorParts(error);
434
+ return exceptionPropertiesOf({
435
+ type: parts.type,
436
+ code: parts.code,
437
+ handled,
438
+ level: handled ? "error" : "fatal",
439
+ frames: parseV8StackFrames(parts.stack, packageRoot)
440
+ });
441
+ }
442
+ var BROWSER_FRAME_GECKO = /^\s*([^@]*)@(.+?):(\d+):(\d+)$/;
443
+ var URL_ORIGIN = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/]*/;
444
+ var SAFE_ERROR_TYPE_NAME = /^[A-Za-z_$][\w$]{0,127}$/;
445
+ function isWebUrl(rawUrl) {
446
+ return /^https?:\/\//i.test(rawUrl);
447
+ }
448
+ function lastPathSegment(urlPath) {
449
+ return urlPath.slice(urlPath.lastIndexOf("/") + 1);
450
+ }
451
+ function scrubWebPath(webPath) {
452
+ const nodeModulesAt = webPath.lastIndexOf("/node_modules/");
453
+ if (nodeModulesAt >= 0) return webPath.slice(nodeModulesAt + 1);
454
+ if (webPath.startsWith("/@fs/")) return lastPathSegment(webPath);
455
+ return webPath;
456
+ }
457
+ function urlPathOnly(rawUrl) {
458
+ const [withoutQueryOrHash] = rawUrl.split(/[?#]/, 1);
459
+ if (isWebUrl(withoutQueryOrHash)) return scrubWebPath(withoutQueryOrHash.replace(URL_ORIGIN, "") || "/");
460
+ return lastPathSegment(withoutQueryOrHash);
461
+ }
462
+ function matchBrowserFrame(stackLine) {
463
+ const v8Frame = matchV8Frame(stackLine);
464
+ if (v8Frame) return v8Frame;
465
+ const gecko = BROWSER_FRAME_GECKO.exec(stackLine);
466
+ if (!gecko) return null;
467
+ return {
468
+ functionName: gecko[1] || "?",
469
+ filePath: gecko[2],
470
+ line: gecko[3],
471
+ column: gecko[4]
472
+ };
473
+ }
474
+ function parseBrowserStackFrames(stack) {
475
+ const frames = [];
476
+ for (const stackLine of stack.slice(0, MAX_STACK_LENGTH).split("\n")) {
477
+ const rawFrame = matchBrowserFrame(stackLine);
478
+ if (!rawFrame) continue;
479
+ frames.push({
480
+ platform: "web:javascript",
481
+ function: rawFrame.functionName.slice(0, 512),
482
+ filename: urlPathOnly(rawFrame.filePath).slice(0, 512),
483
+ lineno: Number(rawFrame.line),
484
+ colno: Number(rawFrame.column),
485
+ in_app: isWebUrl(rawFrame.filePath)
486
+ });
487
+ if (frames.length >= 64) break;
488
+ }
489
+ return frames.reverse();
490
+ }
491
+ function safeErrorTypeName(name) {
492
+ return SAFE_ERROR_TYPE_NAME.test(name) ? name : "Error";
493
+ }
494
+ function buildBrowserExceptionProperties({ name, stack }) {
495
+ return exceptionPropertiesOf({
496
+ type: safeErrorTypeName(name),
497
+ code: "",
498
+ handled: false,
499
+ level: "error",
500
+ frames: parseBrowserStackFrames(stack)
501
+ });
502
+ }
503
+ function exceptionFingerprint(properties) {
504
+ const [exception] = properties.$exception_list;
505
+ const inAppFrames = exception.stacktrace.frames.filter((frame) => frame.in_app).map((frame) => `${frame.function}@${frame.filename}`);
506
+ return [exception.type, ...inAppFrames].join("|");
507
+ }
508
+ function isBuiltinAgentId(agentId) {
509
+ return BUILTIN_AGENT_IDS.some((builtinAgentId) => builtinAgentId === agentId);
510
+ }
511
+ function adapterBucket(agentId) {
512
+ if (isBuiltinAgentId(agentId)) return agentId;
513
+ return "custom";
514
+ }
515
+ function classifySessionExit({ exitCode, signal, reason }) {
516
+ if (reason === "no_output_before_exit") return "no_output";
517
+ if (signal) return "signal";
518
+ if (exitCode === 0) return "clean";
519
+ return "error";
520
+ }
521
+ var AI_PROVIDER_BY_VENDOR = Object.freeze({
522
+ claude: "anthropic",
523
+ codex: "openai",
524
+ grok: "xai"
525
+ });
526
+ var AGENT_ID_BY_VENDOR = Object.freeze({
527
+ claude: "claude-code",
528
+ codex: "codex",
529
+ grok: "grok"
530
+ });
531
+ function aiTraceId(installId, sessionId) {
532
+ return createHash("sha256").update(`${installId}${sessionId}`).digest("hex");
533
+ }
534
+ function wholeTokenCount(tokens) {
535
+ return Math.max(0, Math.round(tokens));
536
+ }
537
+ function buildAiGenerationEvent(row, installId) {
538
+ const properties = {
539
+ $ai_trace_id: aiTraceId(installId, row.sessionId),
540
+ $ai_provider: (AI_PROVIDER_BY_VENDOR[row.vendor] ?? row.vendor).slice(0, 64),
541
+ $ai_model: row.isModelKnown ? row.model.slice(0, 128) : "unknown",
542
+ $ai_input_tokens: wholeTokenCount(row.input),
543
+ $ai_output_tokens: wholeTokenCount(row.output),
544
+ $ai_cache_read_input_tokens: wholeTokenCount(row.cacheRead),
545
+ $ai_cache_creation_input_tokens: wholeTokenCount(row.cacheCreate),
546
+ agent_adapter: adapterBucket(AGENT_ID_BY_VENDOR[row.vendor])
547
+ };
548
+ if (!row.hasKnownCost) return properties;
549
+ return {
550
+ ...properties,
551
+ $ai_total_cost_usd: Math.max(0, row.costUSD)
552
+ };
553
+ }
554
+ function hasAnyTokens(row) {
555
+ return row.input + row.output + row.cacheRead + row.cacheCreate > 0;
556
+ }
557
+ function buildAiGenerationEvents(rows, installId) {
558
+ return rows.filter(hasAnyTokens).map((row) => buildAiGenerationEvent(row, installId));
559
+ }
560
+ function decideRemoteTelemetryState(previous, response) {
561
+ const parsed = FeatureFlagsResponse.safeParse(response);
562
+ if (!parsed.success || parsed.data.errorsWhileComputingFlags === true) return previous;
563
+ const killSwitchFlag = parsed.data.flags[TELEMETRY_KILL_SWITCH_FLAG];
564
+ if (!killSwitchFlag) return previous;
565
+ return killSwitchFlag.enabled ? "enabled" : "disabled";
566
+ }
567
+ var POSTHOG_RELEASE_ID_SHAPE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
568
+ function parsePosthogReleaseId(value) {
569
+ if (typeof value !== "string") return null;
570
+ return POSTHOG_RELEASE_ID_SHAPE.test(value) ? value : null;
571
+ }
572
+ function decideIsDevInstall({ installFlavor, isBundled, homeDir, defaultHomeDir }) {
573
+ if (installFlavor === "clone" || !isBundled) return true;
574
+ return comparablePath(homeDir) !== comparablePath(defaultHomeDir);
575
+ }
576
+ function buildFactoryDecisionEvent(decision) {
577
+ return {
578
+ kind: decision.kind,
579
+ outcome: decision.outcome,
580
+ failure_class: decision.failureClass,
581
+ duration_ms: decision.durationMs,
582
+ attempt: decision.attempt
583
+ };
584
+ }
585
+ function nodeMajorVersion(nodeVersion) {
586
+ const major = Number.parseInt(nodeVersion.replace(/^v/, ""), 10);
587
+ return Number.isFinite(major) ? major : 0;
588
+ }
589
+ //#endregion
590
+ //#region shared/settings/registry.ts
591
+ var BROWSER_NOW = {
592
+ scope: "browser",
593
+ access: "editable",
594
+ takesEffect: "now"
595
+ };
596
+ var BROWSER_ADVANCED = {
597
+ scope: "browser",
598
+ access: "advanced",
599
+ takesEffect: "now"
600
+ };
601
+ var MACHINE_NOW = {
602
+ scope: "machine",
603
+ access: "editable",
604
+ takesEffect: "now"
605
+ };
606
+ var MACHINE_NEW_SESSIONS = {
607
+ scope: "machine",
608
+ access: "editable",
609
+ takesEffect: "new-sessions"
610
+ };
611
+ var READOUT = {
612
+ scope: "machine",
613
+ access: "readout",
614
+ takesEffect: "now"
615
+ };
616
+ var FILE_ONLY = {
617
+ scope: "machine",
618
+ access: "file-only"
619
+ };
620
+ var MS_PER_SECOND = 1e3;
621
+ var SECONDS_PER_MINUTE = 60;
622
+ var MINUTES_PER_HOUR = 60;
623
+ var MS_PER_MINUTE = MS_PER_SECOND * SECONDS_PER_MINUTE;
624
+ var MS_PER_HOUR = MS_PER_MINUTE * MINUTES_PER_HOUR;
625
+ var MS_PER_DAY = MS_PER_HOUR * 24;
626
+ var INVESTIGATIONS = {
627
+ label: "investigations",
628
+ storedPerDisplay: 1
629
+ };
630
+ var SESSIONS = {
631
+ label: "sessions",
632
+ storedPerDisplay: 1
633
+ };
634
+ var WORKERS = {
635
+ label: "workers",
636
+ storedPerDisplay: 1
637
+ };
638
+ var RECURRENCES = {
639
+ label: "recurrences",
640
+ storedPerDisplay: 1
641
+ };
642
+ var ACTIONS = {
643
+ label: "actions",
644
+ storedPerDisplay: 1
645
+ };
646
+ var SECONDS = {
647
+ label: "seconds",
648
+ storedPerDisplay: 1
649
+ };
650
+ var MINUTES = {
651
+ label: "minutes",
652
+ storedPerDisplay: 1
653
+ };
654
+ var HOURS = {
655
+ label: "hours",
656
+ storedPerDisplay: 1
657
+ };
658
+ var DAYS = {
659
+ label: "days",
660
+ storedPerDisplay: 1
661
+ };
662
+ var MILLISECONDS = {
663
+ label: "ms",
664
+ storedPerDisplay: 1
665
+ };
666
+ var SECONDS_STORED_AS_MS = {
667
+ label: "seconds",
668
+ storedPerDisplay: MS_PER_SECOND,
669
+ storedLabel: "ms"
670
+ };
671
+ var MINUTES_STORED_AS_MS = {
672
+ label: "minutes",
673
+ storedPerDisplay: MS_PER_MINUTE,
674
+ storedLabel: "ms"
675
+ };
676
+ var MINUTES_STORED_AS_SECONDS = {
677
+ label: "minutes",
678
+ storedPerDisplay: SECONDS_PER_MINUTE,
679
+ storedLabel: "seconds"
680
+ };
681
+ var HOURS_STORED_AS_MS = {
682
+ label: "hours",
683
+ storedPerDisplay: MS_PER_HOUR,
684
+ storedLabel: "ms"
685
+ };
686
+ var HOURS_STORED_AS_MINUTES = {
687
+ label: "hours",
688
+ storedPerDisplay: MINUTES_PER_HOUR,
689
+ storedLabel: "minutes"
690
+ };
691
+ var DAYS_STORED_AS_MS = {
692
+ label: "days",
693
+ storedPerDisplay: MS_PER_DAY,
694
+ storedLabel: "ms"
695
+ };
696
+ var USD = {
697
+ label: "USD",
698
+ storedPerDisplay: 1
699
+ };
700
+ var USERS = {
701
+ label: "users",
702
+ storedPerDisplay: 1
703
+ };
704
+ var REVIEWS_PER_HOUR = {
705
+ label: "reviews per hour",
706
+ storedPerDisplay: 1
707
+ };
708
+ var MULTIPLIER = {
709
+ label: "times",
710
+ storedPerDisplay: 1
711
+ };
712
+ var TUNING_KNOB = "A tuning knob kept out of the Settings view; the default suits almost every install.";
713
+ var DIAGNOSTIC = "A diagnostic switch kept out of the Settings view; change it in config.json while investigating.";
714
+ var PROCESS_SETTING = "Changes how the server process or its worktrees behave, so it is set in config.json with a restart or new sessions in mind.";
715
+ var STRUCTURED_VALUE = "A structured value with no dashboard control; edit it in config.json.";
716
+ var BY_RULE = "Never editable from the dashboard by rule; edit it in config.json.";
717
+ var LANES_SECTION_ID = "lanes";
718
+ function capitalizedOptions(values) {
719
+ return values.map((value) => ({
720
+ value,
721
+ label: value[0].toUpperCase() + value.slice(1)
722
+ }));
723
+ }
724
+ var FILE_ONLY_GROUPS = Object.freeze({
725
+ "machine-terminal": {
726
+ title: "Terminal",
727
+ description: "Terminal behavior shared by dashboard clients."
728
+ },
729
+ "machine-detection-sessions": {
730
+ title: "Detection and sessions",
731
+ description: "Config-file-only detection, timing, worktree and process settings."
732
+ },
733
+ "lanes-ingest": {
734
+ title: "Ingest",
735
+ description: "The machine-context sources behind Visions."
736
+ }
737
+ });
738
+ var SECTIONS = Object.freeze([
739
+ {
740
+ id: "browser-appearance",
741
+ level: "browser",
742
+ title: "Appearance",
743
+ description: "How this browser draws the dashboard.",
744
+ legacyIds: [
745
+ "browser-flying-animals",
746
+ "animals",
747
+ "flying-animals"
748
+ ],
749
+ settings: [
750
+ {
751
+ id: "theme",
752
+ path: "pref:themeId",
753
+ title: "Theme",
754
+ description: "Color scheme for the dashboard.",
755
+ control: "select",
756
+ optionsFrom: "themes",
757
+ keywords: ["color", "palette"],
758
+ defaultValue: "phyrexian",
759
+ ...BROWSER_NOW
760
+ },
761
+ {
762
+ id: "compact-status",
763
+ path: "pref:compactStatusLabels",
764
+ title: "Hide status words in sidebar",
765
+ description: "Hide status words in the sidebar and on the phone Board while keeping the colored status mark and the phone Plan ready badge.",
766
+ control: "toggle",
767
+ keywords: [
768
+ "sidebar",
769
+ "status",
770
+ "compact"
771
+ ],
772
+ defaultValue: false,
773
+ ...BROWSER_NOW
774
+ },
775
+ {
776
+ id: "session-usage-chips",
777
+ path: "pref:sessionUsageChips",
778
+ title: "Show usage on session cards",
779
+ description: "Shows each card's transcript token count and CLI-reported cost when available, otherwise a calculated API list-price estimate.",
780
+ control: "toggle",
781
+ keywords: [
782
+ "usage",
783
+ "cost",
784
+ "tokens",
785
+ "card",
786
+ "price"
787
+ ],
788
+ defaultValue: false,
789
+ ...BROWSER_NOW
790
+ },
791
+ {
792
+ id: "flying-animals",
793
+ path: "pref:flyingAnimalsEnabled",
794
+ title: "Flying animals",
795
+ description: "Show animated animals flying across the dashboard.",
796
+ control: "toggle",
797
+ keywords: [
798
+ "animation",
799
+ "motion",
800
+ "animals"
801
+ ],
802
+ defaultValue: false,
803
+ ...BROWSER_NOW
804
+ },
805
+ {
806
+ id: "animals-min-gap",
807
+ path: "pref:flyingAnimalsMinGapSeconds",
808
+ title: "Time between flights: minimum",
809
+ description: "Shortest pause between flights.",
810
+ control: "number",
811
+ integer: false,
812
+ step: .1,
813
+ range: FLYING_ANIMALS_GAP_RANGE,
814
+ unit: SECONDS,
815
+ maximumSettingId: "animals-max-gap",
816
+ keywords: ["animals", "frequency"],
817
+ defaultValue: FLYING_ANIMALS_DEFAULTS.flyingAnimalsMinGapSeconds,
818
+ ...BROWSER_ADVANCED
819
+ },
820
+ {
821
+ id: "animals-max-gap",
822
+ path: "pref:flyingAnimalsMaxGapSeconds",
823
+ title: "Time between flights: maximum",
824
+ description: "Longest pause between flights.",
825
+ control: "number",
826
+ integer: false,
827
+ step: .1,
828
+ range: FLYING_ANIMALS_GAP_RANGE,
829
+ unit: SECONDS,
830
+ keywords: ["animals", "frequency"],
831
+ defaultValue: FLYING_ANIMALS_DEFAULTS.flyingAnimalsMaxGapSeconds,
832
+ ...BROWSER_ADVANCED
833
+ },
834
+ {
835
+ id: "animals-min-duration",
836
+ path: "pref:flyingAnimalsMinDurationSeconds",
837
+ title: "Flight duration: minimum",
838
+ description: "Shortest flight, for faster animals.",
839
+ control: "number",
840
+ integer: false,
841
+ step: .1,
842
+ range: FLYING_ANIMALS_DURATION_RANGE,
843
+ unit: SECONDS,
844
+ maximumSettingId: "animals-max-duration",
845
+ keywords: ["animals", "speed"],
846
+ defaultValue: FLYING_ANIMALS_DEFAULTS.flyingAnimalsMinDurationSeconds,
847
+ ...BROWSER_ADVANCED
848
+ },
849
+ {
850
+ id: "animals-max-duration",
851
+ path: "pref:flyingAnimalsMaxDurationSeconds",
852
+ title: "Flight duration: maximum",
853
+ description: "Longest flight, for slower animals.",
854
+ control: "number",
855
+ integer: false,
856
+ step: .1,
857
+ range: FLYING_ANIMALS_DURATION_RANGE,
858
+ unit: SECONDS,
859
+ keywords: ["animals", "speed"],
860
+ defaultValue: FLYING_ANIMALS_DEFAULTS.flyingAnimalsMaxDurationSeconds,
861
+ ...BROWSER_ADVANCED
862
+ },
863
+ {
864
+ id: "animals-scale",
865
+ path: "pref:flyingAnimalsScale",
866
+ title: "Size multiplier",
867
+ description: "Scale applied on top of the viewport size.",
868
+ control: "number",
869
+ integer: false,
870
+ step: .1,
871
+ range: FLYING_ANIMALS_SCALE_RANGE,
872
+ unit: MULTIPLIER,
873
+ keywords: ["animals", "size"],
874
+ defaultValue: FLYING_ANIMALS_DEFAULTS.flyingAnimalsScale,
875
+ ...BROWSER_ADVANCED
876
+ },
877
+ {
878
+ id: "animals-on-phone",
879
+ path: "pref:flyingAnimalsOnPhone",
880
+ title: "Show on phone layout",
881
+ description: "Allow flights on the phone layout.",
882
+ control: "toggle",
883
+ keywords: ["animals", "mobile"],
884
+ defaultValue: FLYING_ANIMALS_DEFAULTS.flyingAnimalsOnPhone,
885
+ ...BROWSER_ADVANCED
886
+ }
887
+ ]
888
+ },
889
+ {
890
+ id: "browser-notifications",
891
+ level: "browser",
892
+ title: "Notifications",
893
+ description: "How this browser, and the Telegram bot shared by every lane, get your attention.",
894
+ settings: [
895
+ {
896
+ id: "alert-sounds-enabled",
897
+ path: "pref:soundEnabled",
898
+ title: "Alert sounds",
899
+ legacyIds: [{
900
+ sectionId: "browser-appearance",
901
+ settingId: "alert-sounds-enabled"
902
+ }],
903
+ description: "Play sounds when a session needs attention and when a merge celebration plays. The same switch as the note button in the desktop header.",
904
+ control: "toggle",
905
+ keywords: [
906
+ "audio",
907
+ "mute",
908
+ "sound",
909
+ "notification"
910
+ ],
911
+ defaultValue: true,
912
+ ...BROWSER_NOW
913
+ },
914
+ {
915
+ id: "alert-sound",
916
+ path: "pref:soundId",
917
+ title: "Alert sound",
918
+ legacyIds: [{
919
+ sectionId: "browser-appearance",
920
+ settingId: "alert-sound"
921
+ }],
922
+ description: "Sound played when a session needs attention. Your own .ogg, .mp3, .wav, .m4a or .webm files dropped into the sounds folder of the Glimmervoid home (~/.glimmervoid/sounds/, or $GLIMMERVOID_HOME/sounds/) are listed after the built-in sounds. Glimmervoid never creates that folder, so make it yourself; new files show up the next time Settings opens.",
923
+ control: "select",
924
+ optionsFrom: "sounds",
925
+ keywords: [
926
+ "audio",
927
+ "notification",
928
+ "custom"
929
+ ],
930
+ defaultValue: "chime",
931
+ ...BROWSER_NOW
932
+ },
933
+ {
934
+ id: "desktop-notifications",
935
+ path: "pref:notificationsEnabled",
936
+ title: "Desktop notifications",
937
+ legacyIds: [{
938
+ sectionId: "browser-appearance",
939
+ settingId: "desktop-notifications"
940
+ }],
941
+ description: "Raise a browser notification when a session needs attention while this dashboard is in the background.",
942
+ control: "toggle",
943
+ keywords: ["browser", "attention"],
944
+ defaultValue: true,
945
+ ...BROWSER_NOW
946
+ }
947
+ ]
948
+ },
949
+ {
950
+ id: "machine-telegram",
951
+ level: "machine",
952
+ parent: "browser-notifications",
953
+ title: "Telegram",
954
+ description: "One bot shared by session, PR review and PostHog notifications.",
955
+ legacyIds: ["telegram", "notifications"],
956
+ settings: [
957
+ {
958
+ id: "telegram-bot-token",
959
+ path: "telegram.botToken",
960
+ title: "Bot token",
961
+ description: "Credential used by the shared Telegram bot. When GLIMMERVOID_TELEGRAM_BOT_TOKEN supplies it, this field is read-only.",
962
+ control: "password",
963
+ keywords: ["credential", "api"],
964
+ defaultValue: "",
965
+ ...MACHINE_NOW
966
+ },
967
+ {
968
+ id: "telegram-chat-id",
969
+ path: "telegram.chatId",
970
+ title: "Chat id",
971
+ description: "Destination chat for enabled Telegram lanes.",
972
+ control: "text",
973
+ keywords: ["destination", "channel"],
974
+ defaultValue: "",
975
+ ...MACHINE_NOW
976
+ },
977
+ {
978
+ id: "telegram-session-notifications",
979
+ path: "telegramNotifications",
980
+ title: "Send session notifications",
981
+ description: "Send session completion, input and failure alerts when no dashboard is open. Unacknowledged alerts also escalate after the phone escalation delay while a dashboard remains open.",
982
+ control: "toggle",
983
+ keywords: ["phone", "off dashboard"],
984
+ defaultValue: false,
985
+ ...MACHINE_NOW
986
+ }
987
+ ]
988
+ },
989
+ {
990
+ id: "machine-sessions",
991
+ level: "machine",
992
+ title: "Sessions",
993
+ description: "How sessions start, where projects are found, and what the dashboard shows for them.",
994
+ legacyIds: [
995
+ "machine-general",
996
+ "general",
997
+ "machine-repositories",
998
+ "repos",
999
+ "repositories"
1000
+ ],
1001
+ settings: [
1002
+ {
1003
+ id: "auto-resume",
1004
+ path: "autoResume",
1005
+ title: "Auto-resume sessions on startup",
1006
+ description: "Resume conversations that were live when Glimmervoid last shut down or crashed.",
1007
+ control: "toggle",
1008
+ keywords: ["startup", "conversation"],
1009
+ defaultValue: true,
1010
+ ...MACHINE_NOW
1011
+ },
1012
+ {
1013
+ id: "conversation-view",
1014
+ path: "conversationView",
1015
+ title: "Conversation view (experimental)",
1016
+ description: "Let each Claude Code and Codex session turn on a Conversation face from its card menu, which renders the transcript as messages, approvals and a composer. Sessions stay on the terminal until you turn it on for them. A Codex session connects on its next start, through a local app-server so approvals can be answered from the dashboard; sandboxed Codex sessions and those that bypass Codex hook trust keep the terminal only.",
1017
+ control: "toggle",
1018
+ keywords: [
1019
+ "experimental",
1020
+ "transcript",
1021
+ "chat",
1022
+ "codex",
1023
+ "claude"
1024
+ ],
1025
+ defaultValue: false,
1026
+ ...MACHINE_NOW
1027
+ },
1028
+ {
1029
+ id: "streamer-mode",
1030
+ path: "streamerMode",
1031
+ title: "Streamer mode",
1032
+ description: "Hide sensitive details in the dashboard while screen sharing or streaming.",
1033
+ control: "toggle",
1034
+ keywords: [
1035
+ "streaming",
1036
+ "screen share",
1037
+ "privacy"
1038
+ ],
1039
+ defaultValue: false,
1040
+ ...MACHINE_NOW
1041
+ },
1042
+ {
1043
+ id: "repository-roots",
1044
+ path: "repoRoots",
1045
+ title: "Repository roots",
1046
+ description: "Directories scanned for projects when adding sessions.",
1047
+ control: "list",
1048
+ keywords: ["folders", "project discovery"],
1049
+ defaultValue: [],
1050
+ listPlaceholder: "Repository root path",
1051
+ listEmptyText: "No repository roots configured.",
1052
+ ...MACHINE_NOW
1053
+ },
1054
+ {
1055
+ id: "custom-agents",
1056
+ path: "status:custom-agents",
1057
+ title: "Custom agents",
1058
+ description: "Extra agent CLIs a session can be spawned with, declared under customAgents in config.json.",
1059
+ control: "readonly",
1060
+ keywords: ["adapters", "cli"],
1061
+ status: "custom-agents",
1062
+ ...READOUT
1063
+ }
1064
+ ]
1065
+ },
1066
+ {
1067
+ id: "machine-guardrails",
1068
+ level: "machine",
1069
+ title: "Guardrails and unattended",
1070
+ description: "What a guarded session may touch, and which unattended actions automated work may take. Claude Code sessions on Linux and macOS write anywhere this account can, except credentials, Glimmervoid's own state and the blocked folders here.",
1071
+ legacyIds: ["lanes-unattended", "unattended"],
1072
+ settings: [
1073
+ {
1074
+ id: "skip-permissions-by-default",
1075
+ path: "skipPermissionsByDefault",
1076
+ title: "Skip permission prompts by default",
1077
+ description: "Start sessions whose project sets no permission choice with the agent CLI permission bypass flag.",
1078
+ control: "toggle",
1079
+ keywords: [
1080
+ "permissions",
1081
+ "yolo",
1082
+ "dangerously"
1083
+ ],
1084
+ danger: true,
1085
+ warning: "Enabling this control lets agents edit files and run shell commands without asking, and restarts running sessions that inherit it.",
1086
+ defaultValue: false,
1087
+ ...MACHINE_NOW
1088
+ },
1089
+ {
1090
+ id: "sane-yolo",
1091
+ path: "saneYolo",
1092
+ title: "Sane YOLO",
1093
+ description: "Block catastrophic commands in sessions that skip permission prompts when the cc-safety-net executable is available and trusted hooks are installed. Missing or untrusted hooks leave a session unprotected.",
1094
+ control: "toggle",
1095
+ keywords: [
1096
+ "permissions",
1097
+ "yolo",
1098
+ "safety"
1099
+ ],
1100
+ defaultValue: true,
1101
+ ...MACHINE_NEW_SESSIONS
1102
+ },
1103
+ {
1104
+ id: "rtk-compression",
1105
+ path: "rtk",
1106
+ title: "rtk output compression",
1107
+ description: "Compress Bash output with the rtk executable.",
1108
+ control: "toggle",
1109
+ keywords: ["bash", "tokens"],
1110
+ danger: true,
1111
+ status: "rtk-install",
1112
+ warning: "Enabling this control permits Glimmervoid to install the pinned rtk executable automatically.",
1113
+ defaultValue: false,
1114
+ ...MACHINE_NEW_SESSIONS
1115
+ },
1116
+ {
1117
+ id: "post-turn-checks-mode",
1118
+ path: "postTurnChecks.mode",
1119
+ title: "Post-turn checks mode",
1120
+ description: "Default mode for post-turn checks. Project and individual rule modes override it, so Report can still allow file fixes.",
1121
+ control: "select",
1122
+ options: capitalizedOptions(POST_TURN_CHECK_MODES),
1123
+ keywords: ["quality", "fixes"],
1124
+ defaultValue: "report",
1125
+ ...MACHINE_NOW
1126
+ },
1127
+ {
1128
+ id: "branch-gc-delete-unmerged",
1129
+ path: "branchGc.deleteUnmerged",
1130
+ title: "Delete unmerged branches",
1131
+ description: "Also delete stale remote branches with no merge proof. Off keeps every unmerged branch.",
1132
+ control: "toggle",
1133
+ keywords: [
1134
+ "git",
1135
+ "cleanup",
1136
+ "orphan"
1137
+ ],
1138
+ danger: true,
1139
+ warning: "Enabling this control lets branch cleanup delete stale remote branches without merge proof.",
1140
+ defaultValue: false,
1141
+ ...MACHINE_NOW
1142
+ },
1143
+ {
1144
+ id: "guardrails-unguarded-repos",
1145
+ path: "guardrails.unguardedRepos",
1146
+ title: "Unguarded repositories",
1147
+ description: "Folders whose sessions run without guardrails. A session started in one of these folders, or below it, is not guarded.",
1148
+ control: "list",
1149
+ keywords: [
1150
+ "guardrails",
1151
+ "opt out",
1152
+ "dotfiles"
1153
+ ],
1154
+ danger: true,
1155
+ warning: "A session without guardrails can read credentials and write anywhere this account can.",
1156
+ defaultValue: [],
1157
+ listPlaceholder: "Folder path",
1158
+ listEmptyText: "Every repository is guarded.",
1159
+ ...MACHINE_NEW_SESSIONS
1160
+ },
1161
+ {
1162
+ id: "guardrails-blocked-paths",
1163
+ path: "guardrails.blockedPaths",
1164
+ title: "Blocked folders",
1165
+ description: "Extra folders and files a guarded session cannot write, on top of credentials and Glimmervoid's own state. A leading ~ means the home folder.",
1166
+ control: "list",
1167
+ keywords: [
1168
+ "guardrails",
1169
+ "block",
1170
+ "protect"
1171
+ ],
1172
+ defaultValue: [],
1173
+ listPlaceholder: "~/folder",
1174
+ listEmptyText: "Only the built-in list is blocked.",
1175
+ ...MACHINE_NEW_SESSIONS
1176
+ },
1177
+ {
1178
+ id: "guardrails-unreadable-paths",
1179
+ path: "guardrails.unreadablePaths",
1180
+ title: "Unreadable folders",
1181
+ description: "Extra folders and files a guarded session cannot read, on top of the built-in credential list. A leading ~ means the home folder.",
1182
+ control: "list",
1183
+ keywords: [
1184
+ "guardrails",
1185
+ "secrets",
1186
+ "credentials"
1187
+ ],
1188
+ defaultValue: [],
1189
+ listPlaceholder: "~/folder",
1190
+ listEmptyText: "Only the built-in credential list is unreadable.",
1191
+ ...MACHINE_NEW_SESSIONS
1192
+ }
1193
+ ]
1194
+ },
1195
+ {
1196
+ id: "machine-updates",
1197
+ level: "machine",
1198
+ title: "Updates",
1199
+ description: "See which Glimmervoid version is running, which one is newest, and update in one step.",
1200
+ legacyIds: ["updates"],
1201
+ settings: [
1202
+ {
1203
+ id: "update-actions",
1204
+ path: "status:update-actions",
1205
+ title: "Status and actions",
1206
+ legacyIds: [
1207
+ "update-summary",
1208
+ "update-installed",
1209
+ "update-latest",
1210
+ "update-last-checked"
1211
+ ].map((settingId) => ({
1212
+ sectionId: "machine-updates",
1213
+ settingId
1214
+ })),
1215
+ description: "Whether this install is up to date, the running and newest versions, the last check, and the update and restart actions.",
1216
+ control: "readonly",
1217
+ keywords: [
1218
+ "version",
1219
+ "available",
1220
+ "current",
1221
+ "check",
1222
+ "restart",
1223
+ "commit",
1224
+ "installed",
1225
+ "release",
1226
+ "failure"
1227
+ ],
1228
+ status: "update-actions",
1229
+ ...READOUT
1230
+ },
1231
+ {
1232
+ id: "update-channel",
1233
+ path: "updateChannel",
1234
+ title: "Channel",
1235
+ description: "Track tagged releases or the upstream of the checked-out branch.",
1236
+ control: "select",
1237
+ options: capitalizedOptions(UPDATE_CHANNELS),
1238
+ keywords: ["release", "branch"],
1239
+ defaultValue: "release",
1240
+ ...MACHINE_NOW
1241
+ },
1242
+ {
1243
+ id: "check-updates",
1244
+ path: "checkForUpdates",
1245
+ title: "Check for updates automatically",
1246
+ description: "Check for a newer Glimmervoid at launch, while a dashboard is connected, and when this page opens.",
1247
+ control: "toggle",
1248
+ keywords: ["release", "github"],
1249
+ defaultValue: true,
1250
+ ...MACHINE_NOW
1251
+ }
1252
+ ]
1253
+ },
1254
+ {
1255
+ id: "machine-usage",
1256
+ level: "machine",
1257
+ title: "Usage",
1258
+ description: "Local transcript accounting and estimated-cost budgets.",
1259
+ legacyIds: ["usage"],
1260
+ settings: [
1261
+ {
1262
+ id: "usage-enabled",
1263
+ path: "usage.enabled",
1264
+ title: "Track token usage",
1265
+ description: "Read local CLI transcripts and roll them into the Usage view and session chips.",
1266
+ control: "toggle",
1267
+ keywords: ["tokens", "cost"],
1268
+ defaultValue: true,
1269
+ status: "usage-last-report",
1270
+ ...MACHINE_NOW
1271
+ },
1272
+ {
1273
+ id: "usage-retain-days",
1274
+ path: "usage.retainDays",
1275
+ title: "Retain transcript detail",
1276
+ description: "How long live transcript detail stays available. A monthly budget keeps detail from the start of the current month when that is longer.",
1277
+ control: "number",
1278
+ range: USAGE_RETAIN_DAYS_RANGE,
1279
+ unit: DAYS,
1280
+ keywords: ["history", "retention"],
1281
+ defaultValue: 90,
1282
+ ...MACHINE_NOW
1283
+ },
1284
+ {
1285
+ id: "usage-daily-budget",
1286
+ path: "usage.budget.dailyUsd",
1287
+ title: "Daily budget",
1288
+ description: "Estimated daily spend alert threshold. Alerts at 50%, 75% and 100% do not stop spending. Zero or below disables the threshold.",
1289
+ control: "number",
1290
+ range: USAGE_BUDGET_RANGE,
1291
+ unit: USD,
1292
+ keywords: ["spend", "alert"],
1293
+ defaultValue: null,
1294
+ integer: false,
1295
+ nullable: true,
1296
+ zeroIsNull: true,
1297
+ step: .01,
1298
+ ...MACHINE_NOW
1299
+ },
1300
+ {
1301
+ id: "usage-monthly-budget",
1302
+ path: "usage.budget.monthlyUsd",
1303
+ title: "Monthly budget",
1304
+ description: "Estimated monthly spend alert threshold. Alerts at 50%, 75% and 100% do not stop spending. Zero or below disables the threshold.",
1305
+ control: "number",
1306
+ range: USAGE_BUDGET_RANGE,
1307
+ unit: USD,
1308
+ keywords: ["spend", "alert"],
1309
+ defaultValue: null,
1310
+ integer: false,
1311
+ nullable: true,
1312
+ zeroIsNull: true,
1313
+ step: .01,
1314
+ ...MACHINE_NOW
1315
+ }
1316
+ ]
1317
+ },
1318
+ {
1319
+ id: LANES_SECTION_ID,
1320
+ level: "lanes",
1321
+ title: "Lanes",
1322
+ description: "The background work Glimmervoid runs for you, one sub-section per lane.",
1323
+ settings: []
1324
+ },
1325
+ {
1326
+ id: "lanes-visions",
1327
+ level: "lanes",
1328
+ parent: LANES_SECTION_ID,
1329
+ title: "Visions",
1330
+ description: "Editor-buffer findings, bounded model comments, and the machine-context ingest behind them.",
1331
+ legacyIds: [
1332
+ "visions",
1333
+ "lanes-ingest",
1334
+ "ingest"
1335
+ ],
1336
+ settings: [
1337
+ {
1338
+ id: "visions-enabled",
1339
+ path: "visions.enabled",
1340
+ title: "Enable Visions",
1341
+ description: "Set up supported editors detected on this machine, then show their buffer findings in the Visions view.",
1342
+ control: "toggle",
1343
+ keywords: [
1344
+ "editor",
1345
+ "findings",
1346
+ "lsp"
1347
+ ],
1348
+ defaultValue: false,
1349
+ ...MACHINE_NOW
1350
+ },
1351
+ {
1352
+ id: "visions-dispatch-enabled",
1353
+ path: "visions.dispatch.enabled",
1354
+ title: "Enable model comments",
1355
+ description: "Start bounded review sessions for local editor buffers.",
1356
+ control: "toggle",
1357
+ keywords: ["review", "dispatch"],
1358
+ defaultValue: false,
1359
+ ...MACHINE_NOW
1360
+ },
1361
+ {
1362
+ id: "visions-max-per-hour",
1363
+ path: "visions.dispatch.maxPerHour",
1364
+ title: "Max per hour",
1365
+ description: "Maximum review sessions dispatched per hour. One slot is reserved for edit reviews; activity-driven reviews take the rest, up to the activity cap set by visions.dispatch.activityMaxPerHour.",
1366
+ control: "number",
1367
+ range: POSITIVE_NUMBER_RANGE,
1368
+ unit: REVIEWS_PER_HOUR,
1369
+ keywords: ["rate limit", "reviews"],
1370
+ defaultValue: 6,
1371
+ ...MACHINE_NOW
1372
+ },
1373
+ {
1374
+ id: "visions-model",
1375
+ path: "visions.dispatch.model",
1376
+ title: "Model override",
1377
+ description: "Leave blank to use the opus model alias.",
1378
+ control: "text",
1379
+ keywords: ["claude", "override"],
1380
+ defaultValue: "",
1381
+ ...MACHINE_NOW
1382
+ },
1383
+ {
1384
+ id: "visions-projects",
1385
+ path: "visions.projects",
1386
+ title: "Projects",
1387
+ description: "Clear every selection, including unavailable projects, to accept buffers from every configured project.",
1388
+ control: "projects",
1389
+ keywords: ["repositories", "filter"],
1390
+ defaultValue: [],
1391
+ ...MACHINE_NOW
1392
+ },
1393
+ {
1394
+ id: "visions-auto-fix",
1395
+ path: "visions.autoFix",
1396
+ title: "Apply tier 1 fixes",
1397
+ description: "Allow Visions to edit eligible open Markdown buffers, including inactive buffers, without asking.",
1398
+ control: "toggle",
1399
+ keywords: ["automatic", "edits"],
1400
+ danger: true,
1401
+ warning: "Enabling this control lets Visions edit eligible buffers without a carbon unit present.",
1402
+ defaultValue: false,
1403
+ ...MACHINE_NOW
1404
+ },
1405
+ {
1406
+ id: "ingest-enabled",
1407
+ path: "ingest.enabled",
1408
+ title: "Enable machine context ingest",
1409
+ description: "Enable the local activity feed behind Visions. Turning Visions on enables ingest unless ingest.enabled is explicitly false.",
1410
+ control: "toggle",
1411
+ keywords: [
1412
+ "events",
1413
+ "activity",
1414
+ "ingest"
1415
+ ],
1416
+ defaultValue: false,
1417
+ ...MACHINE_NOW
1418
+ }
1419
+ ]
1420
+ },
1421
+ {
1422
+ id: "lanes-posthog",
1423
+ level: "lanes",
1424
+ parent: LANES_SECTION_ID,
1425
+ title: "PostHog",
1426
+ description: "Error monitoring, investigations and traffic-spike alerts.",
1427
+ legacyIds: ["posthog"],
1428
+ settings: [
1429
+ {
1430
+ id: "posthog-enabled",
1431
+ path: "posthog.enabled",
1432
+ title: "Enable PostHog monitoring",
1433
+ description: "Poll PostHog error tracking and investigate issues that move.",
1434
+ control: "toggle",
1435
+ keywords: ["errors", "monitoring"],
1436
+ defaultValue: false,
1437
+ ...MACHINE_NOW
1438
+ },
1439
+ {
1440
+ id: "posthog-host",
1441
+ path: "posthog.host",
1442
+ title: "Host",
1443
+ description: "PostHog cloud or self-hosted HTTP URL.",
1444
+ control: "text",
1445
+ keywords: ["url", "server"],
1446
+ defaultValue: "https://us.posthog.com",
1447
+ ...MACHINE_NOW
1448
+ },
1449
+ {
1450
+ id: "posthog-api-key",
1451
+ path: "posthog.apiKey",
1452
+ title: "Personal API key",
1453
+ description: "Credential with read access to PostHog projects. When GLIMMERVOID_POSTHOG_API_KEY supplies it, this field is read-only.",
1454
+ control: "password",
1455
+ keywords: ["credential", "token"],
1456
+ defaultValue: "",
1457
+ ...MACHINE_NOW
1458
+ },
1459
+ {
1460
+ id: "posthog-projects",
1461
+ path: "posthog.projects",
1462
+ title: "Projects",
1463
+ description: "PostHog project ids to monitor. config.json holds \"all\" or an array of ids, and an empty array monitors no project. In Settings that empty array shows as none, and an empty list saves \"all\".",
1464
+ control: "list",
1465
+ keywords: ["project ids", "filter"],
1466
+ defaultValue: "all",
1467
+ valueKind: "posthog-projects",
1468
+ listPlaceholder: "Project id",
1469
+ listEmptyText: "Every project the key can read.",
1470
+ ...MACHINE_NOW
1471
+ },
1472
+ {
1473
+ id: "posthog-interval",
1474
+ path: "posthog.intervalMinutes",
1475
+ title: "Poll interval",
1476
+ description: "Delay between PostHog polling passes.",
1477
+ control: "number",
1478
+ integer: false,
1479
+ step: "any",
1480
+ range: POSITIVE_NUMBER_RANGE,
1481
+ unit: MINUTES,
1482
+ keywords: ["refresh", "schedule"],
1483
+ defaultValue: 15,
1484
+ ...MACHINE_NOW
1485
+ },
1486
+ {
1487
+ id: "posthog-max-investigations",
1488
+ path: "posthog.maxConcurrentInvestigations",
1489
+ title: "Max concurrent investigations",
1490
+ description: "Maximum investigation sessions running together.",
1491
+ control: "number",
1492
+ range: POSITIVE_NUMBER_RANGE,
1493
+ unit: INVESTIGATIONS,
1494
+ keywords: ["parallel", "workers"],
1495
+ defaultValue: 2,
1496
+ ...MACHINE_NOW
1497
+ },
1498
+ {
1499
+ id: "posthog-min-users",
1500
+ path: "posthog.minUsersToInvestigate",
1501
+ title: "Min users to investigate",
1502
+ description: "Minimum affected users for a new issue investigation. Regressions, worsening issues and eligible error spikes bypass this minimum.",
1503
+ control: "number",
1504
+ range: POSITIVE_NUMBER_RANGE,
1505
+ unit: USERS,
1506
+ keywords: ["threshold", "affected"],
1507
+ defaultValue: 1,
1508
+ ...MACHINE_NOW
1509
+ },
1510
+ {
1511
+ id: "posthog-traffic-enabled",
1512
+ path: "posthog.trafficSpikeEnabled",
1513
+ title: "Traffic spike alerts",
1514
+ description: "Ping when recent unique users exceed a project baseline.",
1515
+ control: "toggle",
1516
+ keywords: ["analytics", "alert"],
1517
+ defaultValue: true,
1518
+ ...MACHINE_NOW
1519
+ },
1520
+ {
1521
+ id: "posthog-auto-fix",
1522
+ path: "posthog.autoFix",
1523
+ title: "Attempt fixes for major issues",
1524
+ description: "Allow an isolated agent to fix an issue, push a branch and open a pull request.",
1525
+ control: "toggle",
1526
+ keywords: ["automatic", "pull request"],
1527
+ danger: true,
1528
+ warning: "Enabling this control lets PostHog fixes commit, push and open pull requests automatically.",
1529
+ defaultValue: false,
1530
+ ...MACHINE_NOW
1531
+ }
1532
+ ]
1533
+ },
1534
+ {
1535
+ id: "lanes-team-review",
1536
+ level: "lanes",
1537
+ parent: LANES_SECTION_ID,
1538
+ title: "Team review",
1539
+ description: "Draft reviews of teammates' pull requests for you to post from the Reviews tab.",
1540
+ legacyIds: ["team-review", "reviews"],
1541
+ settings: [
1542
+ {
1543
+ id: "team-review-enabled",
1544
+ path: "teamReview.enabled",
1545
+ title: "Enable team review",
1546
+ description: "Poll open pull requests from the team and draft a review of each one. Nothing is posted until you choose Approve or Comment.",
1547
+ control: "toggle",
1548
+ keywords: ["pull requests", "github"],
1549
+ defaultValue: false,
1550
+ ...MACHINE_NOW
1551
+ },
1552
+ {
1553
+ id: "github-teams",
1554
+ path: "github.teams",
1555
+ title: "GitHub teams",
1556
+ description: "Teams to search for review requests and members, one org/slug per entry. Leave empty to fall back to the legacy teamReview.org and teamReview.team keys in config.json.",
1557
+ control: "list",
1558
+ keywords: [
1559
+ "github",
1560
+ "teams",
1561
+ "review requests"
1562
+ ],
1563
+ defaultValue: [],
1564
+ listPlaceholder: "org/team-slug",
1565
+ listEmptyText: "No GitHub teams configured.",
1566
+ ...MACHINE_NOW
1567
+ },
1568
+ {
1569
+ id: "team-review-re-review-after-hours",
1570
+ path: "teamReview.reReviewAfterHours",
1571
+ title: "Re-review after",
1572
+ description: "When a reviewed PR has a new head, review it again after this long since its last review. Queue review runs it at any time.",
1573
+ control: "number",
1574
+ range: POSITIVE_NUMBER_RANGE,
1575
+ unit: HOURS,
1576
+ keywords: ["review", "delay"],
1577
+ defaultValue: 24,
1578
+ integer: false,
1579
+ step: .5,
1580
+ ...MACHINE_NOW
1581
+ },
1582
+ {
1583
+ id: "team-review-skip-idle-after-days",
1584
+ path: "teamReview.skipIdleAfterDays",
1585
+ title: "Skip PRs idle for",
1586
+ description: "Leave pull requests without GitHub activity for this long out of automatic review.",
1587
+ control: "number",
1588
+ range: POSITIVE_NUMBER_RANGE,
1589
+ unit: DAYS,
1590
+ keywords: ["idle", "activity"],
1591
+ defaultValue: 14,
1592
+ integer: false,
1593
+ step: .5,
1594
+ ...MACHINE_NOW
1595
+ },
1596
+ {
1597
+ id: "team-review-skill",
1598
+ path: "teamReview.skill",
1599
+ title: "Review skill",
1600
+ description: "Name of a Claude Code skill installed for the review agent, for example your own PR review skill. Leave empty to let the agent review with whatever skills it has.",
1601
+ control: "text",
1602
+ keywords: [
1603
+ "skill",
1604
+ "claude code",
1605
+ "pr review"
1606
+ ],
1607
+ defaultValue: "",
1608
+ ...MACHINE_NOW
1609
+ },
1610
+ {
1611
+ id: "team-review-auto-rebase-my-prs",
1612
+ path: "teamReview.autoRebaseMyPrs",
1613
+ title: "Keep my pull requests rebased",
1614
+ description: "On each poll, rebase your open, non-draft pull requests that are behind their base branch. A pull request with conflicts or checks still running is left alone, and a failed rebase is not retried until you push again.",
1615
+ control: "toggle",
1616
+ keywords: [
1617
+ "rebase",
1618
+ "update branch",
1619
+ "behind",
1620
+ "my prs"
1621
+ ],
1622
+ danger: true,
1623
+ warning: "Enabling this control rewrites your pull request branches on GitHub. Pull before you push from a local checkout of one.",
1624
+ defaultValue: false,
1625
+ ...MACHINE_NOW
1626
+ },
1627
+ {
1628
+ id: "team-review-keep-mergeable-enabled",
1629
+ path: "teamReview.keepMergeableEnabled",
1630
+ title: "Keep mergeable",
1631
+ description: "Offer Keep mergeable on your pull requests: a sandboxed agent repairs conflicts and failing checks and pushes the repair. Off hides the control and starts no repair; the pull requests you chose stay chosen for when you turn it back on.",
1632
+ control: "toggle",
1633
+ keywords: [
1634
+ "repair",
1635
+ "conflicts",
1636
+ "my prs"
1637
+ ],
1638
+ defaultValue: true,
1639
+ ...MACHINE_NOW
1640
+ },
1641
+ {
1642
+ id: "team-review-keep-mergeable-timeout",
1643
+ path: "teamReview.keepMergeableTimeoutMinutes",
1644
+ title: "Keep mergeable deadline",
1645
+ description: "Longest a Keep mergeable repair session may run before it is stopped and nothing is pushed.",
1646
+ control: "number",
1647
+ range: KEEP_MERGEABLE_TIMEOUT_MINUTES_RANGE,
1648
+ unit: MINUTES,
1649
+ keywords: ["timeout", "repair"],
1650
+ defaultValue: 30,
1651
+ ...MACHINE_NOW
1652
+ },
1653
+ {
1654
+ id: "team-review-merge-queue-enabled",
1655
+ path: "teamReview.mergeQueueEnabled",
1656
+ title: "Merge when ready",
1657
+ description: "Offer Merge when ready on your pull requests: queued pull requests are rebased when behind and merged once ready, one per repository per poll. Off hides the control and merges nothing; the queue is kept for when you turn it back on.",
1658
+ control: "toggle",
1659
+ keywords: [
1660
+ "merge queue",
1661
+ "auto merge",
1662
+ "my prs"
1663
+ ],
1664
+ defaultValue: true,
1665
+ ...MACHINE_NOW
1666
+ }
1667
+ ]
1668
+ },
1669
+ {
1670
+ id: "lanes-workflows",
1671
+ level: "lanes",
1672
+ parent: LANES_SECTION_ID,
1673
+ title: "Workflows",
1674
+ description: "Rules that act on pull request events in the repositories you choose.",
1675
+ legacyIds: ["workflows"],
1676
+ settings: [
1677
+ {
1678
+ id: "workflows-enabled",
1679
+ path: "workflows.enabled",
1680
+ title: "Run workflows",
1681
+ description: "Poll the repositories your enabled rules watch and run their actions. Off stops polling and every action, whatever each rule says.",
1682
+ control: "toggle",
1683
+ keywords: [
1684
+ "pull requests",
1685
+ "automation",
1686
+ "master switch"
1687
+ ],
1688
+ defaultValue: true,
1689
+ ...MACHINE_NOW
1690
+ },
1691
+ {
1692
+ id: "workflows-max-concurrent-sessions",
1693
+ path: "workflows.maxConcurrentSessions",
1694
+ title: "Concurrent agent sessions",
1695
+ description: "Most workflow agent sessions running at once. Up to twenty later spawns wait for a free slot; additional spawns are dropped and logged.",
1696
+ control: "number",
1697
+ range: WORKFLOWS_MAX_CONCURRENT_SESSIONS_RANGE,
1698
+ unit: SESSIONS,
1699
+ keywords: ["parallel", "spawn"],
1700
+ defaultValue: 2,
1701
+ ...MACHINE_NOW
1702
+ },
1703
+ {
1704
+ id: "workflows-rules",
1705
+ path: "status:workflow-rules",
1706
+ title: "Workflow rules",
1707
+ description: "Each rule watches repositories for one pull request event (opened, checks-failed, review-requested, approved, commented or merged), narrows it by author, labels, base branch, your own pull requests or a review request to your team, and then notifies you, labels or comments on the pull request, or starts a sandboxed agent session that pushes nothing. Rules start disabled. Turn each rule on or off here; everything else about a rule is set in config.json under workflows.rules.",
1708
+ control: "readonly",
1709
+ keywords: [
1710
+ "pull requests",
1711
+ "automation",
1712
+ "github",
1713
+ "rules"
1714
+ ],
1715
+ status: "workflows",
1716
+ ...READOUT
1717
+ }
1718
+ ]
1719
+ },
1720
+ {
1721
+ id: "lanes-factory",
1722
+ level: "lanes",
1723
+ parent: LANES_SECTION_ID,
1724
+ title: "Factory",
1725
+ description: "Read Coherence reports from repositories whose integration branch holds a coherence.config.json and show their status in the Factory tab.",
1726
+ legacyIds: ["factory"],
1727
+ settings: [
1728
+ {
1729
+ id: "factory-enabled",
1730
+ path: "factory.enabled",
1731
+ title: "Enable the factory",
1732
+ description: "Enable Coherence status polling and show the Factory tab. Off by default.",
1733
+ control: "toggle",
1734
+ keywords: [
1735
+ "orchestrator",
1736
+ "intents",
1737
+ "queue",
1738
+ "coherence",
1739
+ "guardrails"
1740
+ ],
1741
+ defaultValue: false,
1742
+ ...MACHINE_NOW
1743
+ },
1744
+ {
1745
+ id: "factory-max-risk",
1746
+ path: "factory.maxRisk",
1747
+ title: "Worker risk ceiling",
1748
+ description: "Highest risk admitted for a ticket.",
1749
+ control: "select",
1750
+ defaultValue: "medium",
1751
+ options: [
1752
+ "low",
1753
+ "medium",
1754
+ "high",
1755
+ "critical"
1756
+ ].map((value) => ({
1757
+ value,
1758
+ label: value
1759
+ })),
1760
+ keywords: ["risk", "admission"],
1761
+ ...MACHINE_NOW
1762
+ },
1763
+ {
1764
+ id: "factory-max-live-workers",
1765
+ path: "factory.maxLiveWorkers",
1766
+ title: "Live worker cap",
1767
+ description: "Most live workers per factory project.",
1768
+ control: "number",
1769
+ range: FACTORY_MAX_LIVE_WORKERS_RANGE,
1770
+ unit: WORKERS,
1771
+ defaultValue: 2,
1772
+ keywords: ["workers", "parallel"],
1773
+ ...MACHINE_NOW
1774
+ },
1775
+ {
1776
+ id: "factory-checks",
1777
+ path: "factory.checks",
1778
+ title: "Worker checks",
1779
+ description: "Commands glimmervoid runs before merging worker commits.",
1780
+ control: "list",
1781
+ defaultValue: [
1782
+ "npm run typecheck",
1783
+ "npm run lint",
1784
+ "npm test"
1785
+ ],
1786
+ keywords: ["tests", "checks"],
1787
+ listPlaceholder: "npm test",
1788
+ listEmptyText: "No checks run before a merge.",
1789
+ ...MACHINE_NOW
1790
+ },
1791
+ {
1792
+ id: "factory-protected-paths",
1793
+ path: "factory.protectedPaths",
1794
+ title: "Protected paths",
1795
+ description: "Repo-relative files, directory prefixes ending in /, or **/NAME basenames workers cannot change.",
1796
+ control: "list",
1797
+ defaultValue: [
1798
+ "coherence.config.json",
1799
+ ".coherence/",
1800
+ "package.json",
1801
+ "package-lock.json",
1802
+ "**/AGENTS.md",
1803
+ "**/CLAUDE.md",
1804
+ "**/CLAUDE.local.md",
1805
+ ".claude/",
1806
+ ".github/"
1807
+ ],
1808
+ keywords: ["fence", "scope"],
1809
+ listPlaceholder: "path, prefix/ or **/NAME",
1810
+ listEmptyText: "Workers may change any path.",
1811
+ ...MACHINE_NOW
1812
+ },
1813
+ {
1814
+ id: "factory-reviewer-model",
1815
+ path: "factory.reviewerModel",
1816
+ title: "Reviewer model",
1817
+ description: "Claude model for independent worker review. Blank uses the CLI default.",
1818
+ control: "text",
1819
+ defaultValue: null,
1820
+ nullable: true,
1821
+ keywords: ["claude", "review"],
1822
+ ...MACHINE_NOW
1823
+ },
1824
+ {
1825
+ id: "factory-daily-budget",
1826
+ path: "factory.dailyBudgetUsd",
1827
+ title: "Factory daily budget",
1828
+ description: "Factory lane spend ceiling. Blank means no ceiling. Needs usage tracking: with it off, dispatch refuses.",
1829
+ control: "number",
1830
+ range: NON_NEGATIVE_NUMBER_RANGE,
1831
+ unit: USD,
1832
+ integer: false,
1833
+ nullable: true,
1834
+ step: .01,
1835
+ defaultValue: null,
1836
+ keywords: ["spend", "budget"],
1837
+ ...MACHINE_NOW
1838
+ },
1839
+ {
1840
+ id: "factory-verifier-model",
1841
+ path: "factory.verifierModel",
1842
+ title: "Verifier model",
1843
+ description: "Claude model for independent intent verification. Blank uses the CLI default.",
1844
+ control: "text",
1845
+ nullable: true,
1846
+ defaultValue: null,
1847
+ keywords: ["claude", "verification"],
1848
+ ...MACHINE_NOW
1849
+ },
1850
+ {
1851
+ id: "factory-mirror-harness-runs",
1852
+ path: "factory.mirrorHarnessRuns",
1853
+ title: "Mirror harness runs",
1854
+ description: "Show the latest factory harness run as a read-only project in the Factory tab while it runs and for a day after. Needs factory.enabled on. The harness turns it off on the instance it boots.",
1855
+ control: "toggle",
1856
+ defaultValue: true,
1857
+ keywords: [
1858
+ "harness",
1859
+ "test",
1860
+ "read-only"
1861
+ ],
1862
+ ...MACHINE_NOW
1863
+ }
1864
+ ]
1865
+ },
1866
+ {
1867
+ id: "lanes-benchmarks",
1868
+ level: "lanes",
1869
+ parent: LANES_SECTION_ID,
1870
+ title: "Benchmarks",
1871
+ description: "Compare agent setups on frozen cases from the suites in the benchmarks folder of the Glimmervoid home.",
1872
+ legacyIds: ["benchmarks"],
1873
+ settings: [{
1874
+ id: "benchmarks-enabled",
1875
+ path: "benchmarks.enabled",
1876
+ title: "Enable benchmarks",
1877
+ description: "Show the Bench tab. Nothing runs until you choose Mine or Run there.",
1878
+ control: "toggle",
1879
+ keywords: [
1880
+ "eval",
1881
+ "arms",
1882
+ "recall",
1883
+ "compare"
1884
+ ],
1885
+ defaultValue: false,
1886
+ ...MACHINE_NOW
1887
+ }]
1888
+ },
1889
+ {
1890
+ id: "lanes-knowledge-graph",
1891
+ level: "lanes",
1892
+ parent: LANES_SECTION_ID,
1893
+ title: "Knowledge graph (experimental)",
1894
+ description: "A typed local graph of projects, tasks, notes, references and questions, driven from the glimmervoid kg command. Its SQLite database defaults to the Glimmervoid home; --db selects another path.",
1895
+ legacyIds: ["knowledge-graph"],
1896
+ settings: [{
1897
+ id: "knowledge-graph-enabled",
1898
+ path: "knowledgeGraph.enabled",
1899
+ title: "Enable the knowledge graph",
1900
+ description: "Allow the glimmervoid kg command. It reads Coherence ledgers to report where your tasks and the agents' tickets disagree, without modifying those ledgers. Graph writes go to the selected SQLite database, which can be inside a repository when --db names that path. Off by default.",
1901
+ control: "toggle",
1902
+ keywords: [
1903
+ "experimental",
1904
+ "kg",
1905
+ "tasks",
1906
+ "notes",
1907
+ "references",
1908
+ "coherence"
1909
+ ],
1910
+ defaultValue: false,
1911
+ ...MACHINE_NOW
1912
+ }]
1913
+ },
1914
+ {
1915
+ id: "lanes-change-map",
1916
+ level: "lanes",
1917
+ parent: LANES_SECTION_ID,
1918
+ title: "Change map",
1919
+ description: "Optional model narration for deterministic change facts.",
1920
+ legacyIds: ["change-map"],
1921
+ settings: [
1922
+ {
1923
+ id: "change-map-narrator-enabled",
1924
+ path: "changeMap.narrator.enabled",
1925
+ title: "Enable narration",
1926
+ description: "Add cited model claims to the change map.",
1927
+ control: "toggle",
1928
+ keywords: ["review", "claims"],
1929
+ defaultValue: false,
1930
+ ...MACHINE_NOW
1931
+ },
1932
+ {
1933
+ id: "change-map-narrator-engine",
1934
+ path: "changeMap.narrator.engine",
1935
+ title: "Narrator engine",
1936
+ description: "CLI used to generate change map claims.",
1937
+ control: "select",
1938
+ options: capitalizedOptions(CHANGE_MAP_NARRATOR_ENGINES),
1939
+ keywords: ["claude", "codex"],
1940
+ defaultValue: "claude",
1941
+ ...MACHINE_NOW
1942
+ },
1943
+ {
1944
+ id: "change-map-narrator-model",
1945
+ path: "changeMap.narrator.model",
1946
+ title: "Narrator model",
1947
+ description: "Blank uses the engine default: haiku for Claude, the configured Codex model for Codex.",
1948
+ control: "text",
1949
+ keywords: [
1950
+ "claude",
1951
+ "codex",
1952
+ "haiku"
1953
+ ],
1954
+ defaultValue: "",
1955
+ ...MACHINE_NOW
1956
+ }
1957
+ ]
1958
+ },
1959
+ {
1960
+ id: "machine-privacy",
1961
+ level: "machine",
1962
+ title: "Privacy",
1963
+ description: "What Glimmervoid sends about itself.",
1964
+ legacyIds: ["privacy", "telemetry"],
1965
+ settings: [{
1966
+ id: "telemetry-enabled",
1967
+ path: "telemetry.enabled",
1968
+ title: "Send anonymous usage and error data",
1969
+ description: "Version, platform, app starts, daily activity, session starts and ends with agent kind, exit kind and duration, errors as their type, error code and scrubbed stack frames, per session token counts, models and estimated costs from the usage scan, and factory outcomes as the kind of decision, how it came out, its duration and attempt number, under a random install id. Never error messages, paths, repository or branch names, prompts, objectives or terminal output. GLIMMERVOID_TELEMETRY=0, DO_NOT_TRACK=1 or CI=true turn it off regardless.",
1970
+ control: "toggle",
1971
+ keywords: [
1972
+ "telemetry",
1973
+ "analytics",
1974
+ "tracking",
1975
+ "opt out"
1976
+ ],
1977
+ defaultValue: true,
1978
+ ...MACHINE_NOW
1979
+ }]
1980
+ },
1981
+ {
1982
+ id: "browser-shortcuts",
1983
+ level: "browser",
1984
+ title: "Shortcuts and about",
1985
+ description: "Keyboard reference and build information.",
1986
+ legacyIds: ["shortcuts"],
1987
+ settings: []
1988
+ }
1989
+ ]);
1990
+ var FILE_ONLY_SETTINGS = Object.freeze([
1991
+ {
1992
+ id: "file-debug-mode",
1993
+ path: "debugMode",
1994
+ title: "Debug mode",
1995
+ group: "machine-sessions",
1996
+ description: "Show session-card diagnostics for state, transitions and detection signals.",
1997
+ control: "toggle",
1998
+ keywords: ["diagnostics", "state"],
1999
+ defaultValue: false,
2000
+ takesEffect: "now",
2001
+ fileOnlyReason: DIAGNOSTIC,
2002
+ ...FILE_ONLY
2003
+ },
2004
+ {
2005
+ id: "file-replay-buffer",
2006
+ path: "replayBufferKB",
2007
+ title: "Replay buffer",
2008
+ group: "machine-terminal",
2009
+ description: "PTY output retained for clients that reconnect or join late.",
2010
+ control: "number",
2011
+ range: REPLAY_BUFFER_KB_RANGE,
2012
+ unit: {
2013
+ label: "KB",
2014
+ storedPerDisplay: 1
2015
+ },
2016
+ keywords: ["history", "backfill"],
2017
+ defaultValue: 512,
2018
+ takesEffect: "new-sessions",
2019
+ fileOnlyReason: TUNING_KNOB,
2020
+ ...FILE_ONLY
2021
+ },
2022
+ {
2023
+ id: "file-cursor-blink",
2024
+ path: "cursorBlink",
2025
+ title: "Cursor blink",
2026
+ group: "machine-terminal",
2027
+ description: "Blink the cursor in every terminal.",
2028
+ control: "toggle",
2029
+ keywords: ["caret", "terminal"],
2030
+ defaultValue: false,
2031
+ takesEffect: "now",
2032
+ fileOnlyReason: TUNING_KNOB,
2033
+ ...FILE_ONLY
2034
+ },
2035
+ {
2036
+ id: "file-detect-background-agents",
2037
+ path: "detectBackgroundAgents",
2038
+ title: "Detect background agents",
2039
+ group: "machine-detection-sessions",
2040
+ description: "Hold completion while tracked background work is active.",
2041
+ control: "toggle",
2042
+ keywords: ["subagents", "completion"],
2043
+ defaultValue: true,
2044
+ takesEffect: "new-sessions",
2045
+ fileOnlyReason: DIAGNOSTIC,
2046
+ ...FILE_ONLY
2047
+ },
2048
+ {
2049
+ id: "file-record-signals",
2050
+ path: "recordSignals",
2051
+ title: "Record structural signals",
2052
+ group: "machine-detection-sessions",
2053
+ description: "Keep forensic status and hook recordings for each session. Full capture (capture.enabled) records these signals even when this is off.",
2054
+ control: "toggle",
2055
+ keywords: ["recordings", "diagnostics"],
2056
+ defaultValue: true,
2057
+ takesEffect: "new-sessions",
2058
+ fileOnlyReason: DIAGNOSTIC,
2059
+ ...FILE_ONLY
2060
+ },
2061
+ {
2062
+ id: "file-trace-enabled",
2063
+ path: "trace.enabled",
2064
+ title: "Capture session traces",
2065
+ group: "machine-detection-sessions",
2066
+ description: "Keep normalized Claude transcript traces for session debugging.",
2067
+ control: "toggle",
2068
+ keywords: ["transcripts", "diagnostics"],
2069
+ defaultValue: true,
2070
+ takesEffect: "restart",
2071
+ fileOnlyReason: DIAGNOSTIC,
2072
+ ...FILE_ONLY
2073
+ },
2074
+ {
2075
+ id: "file-detect-scheduled-wakeups",
2076
+ path: "detectScheduledWakeups",
2077
+ title: "Detect scheduled wakeups",
2078
+ group: "machine-detection-sessions",
2079
+ description: "Surface advisory wakeup timing for scheduled sessions.",
2080
+ control: "toggle",
2081
+ keywords: ["schedule", "sleep"],
2082
+ defaultValue: true,
2083
+ takesEffect: "restart",
2084
+ fileOnlyReason: DIAGNOSTIC,
2085
+ ...FILE_ONLY
2086
+ },
2087
+ {
2088
+ id: "file-worktree-auto-rebase",
2089
+ path: "worktreeAutoRebase",
2090
+ title: "Worktree auto-rebase",
2091
+ group: "machine-detection-sessions",
2092
+ description: "Rebase eligible session worktrees when the integration branch moves.",
2093
+ control: "toggle",
2094
+ keywords: ["git", "branch"],
2095
+ defaultValue: true,
2096
+ takesEffect: "new-sessions",
2097
+ fileOnlyReason: PROCESS_SETTING,
2098
+ ...FILE_ONLY
2099
+ },
2100
+ {
2101
+ id: "file-worktree-sync-on-start",
2102
+ path: "worktreeSyncOnStart",
2103
+ title: "Worktree sync on start",
2104
+ group: "machine-detection-sessions",
2105
+ description: "Fetch origin and fast-forward the local integration branch before a session starts its worktree.",
2106
+ control: "toggle",
2107
+ keywords: [
2108
+ "git",
2109
+ "fetch",
2110
+ "fast-forward"
2111
+ ],
2112
+ defaultValue: true,
2113
+ takesEffect: "new-sessions",
2114
+ fileOnlyReason: PROCESS_SETTING,
2115
+ ...FILE_ONLY
2116
+ },
2117
+ {
2118
+ id: "file-worktree-rerere",
2119
+ path: "worktreeRerere",
2120
+ title: "Worktree rerere",
2121
+ group: "machine-detection-sessions",
2122
+ description: "Reuse recorded Git conflict resolutions.",
2123
+ control: "toggle",
2124
+ keywords: ["git", "conflicts"],
2125
+ defaultValue: true,
2126
+ takesEffect: "restart",
2127
+ fileOnlyReason: PROCESS_SETTING,
2128
+ ...FILE_ONLY
2129
+ },
2130
+ {
2131
+ id: "file-integration-branch",
2132
+ path: "integrationBranch",
2133
+ title: "Integration branch",
2134
+ group: "machine-detection-sessions",
2135
+ description: "Base branch for session worktrees. Empty = each repo's default branch. Existing sessions are never retargeted.",
2136
+ control: "text",
2137
+ nullable: true,
2138
+ keywords: ["git", "merge"],
2139
+ defaultValue: null,
2140
+ takesEffect: "new-sessions",
2141
+ fileOnlyReason: PROCESS_SETTING,
2142
+ ...FILE_ONLY
2143
+ },
2144
+ {
2145
+ id: "file-worktree-root",
2146
+ path: "worktreeRoot",
2147
+ title: "Worktree root",
2148
+ group: "machine-detection-sessions",
2149
+ description: "Directory that contains session worktrees. Existing worktrees are never relocated.",
2150
+ control: "text",
2151
+ keywords: ["git", "directory"],
2152
+ defaultValue: "",
2153
+ takesEffect: "new-sessions",
2154
+ fileOnlyReason: PROCESS_SETTING,
2155
+ ...FILE_ONLY
2156
+ },
2157
+ {
2158
+ id: "file-worktree-share",
2159
+ path: "worktreeShare",
2160
+ title: "Shared worktree paths",
2161
+ group: "machine-detection-sessions",
2162
+ description: "Local paths copied or linked during worktree preparation.",
2163
+ control: "list",
2164
+ keywords: ["files", "context"],
2165
+ defaultValue: [
2166
+ "node_modules",
2167
+ ".env",
2168
+ ".env.local",
2169
+ ".claude"
2170
+ ],
2171
+ takesEffect: "new-sessions",
2172
+ fileOnlyReason: BY_RULE,
2173
+ ...FILE_ONLY
2174
+ },
2175
+ {
2176
+ id: "file-port",
2177
+ path: "port",
2178
+ title: "Local port",
2179
+ group: "machine-detection-sessions",
2180
+ description: "Port used by the local dashboard listener.",
2181
+ control: "number",
2182
+ range: PORT_RANGE,
2183
+ unit: {
2184
+ label: "port",
2185
+ storedPerDisplay: 1
2186
+ },
2187
+ keywords: ["server", "listener"],
2188
+ defaultValue: 3e3,
2189
+ takesEffect: "restart",
2190
+ fileOnlyReason: BY_RULE,
2191
+ ...FILE_ONLY
2192
+ },
2193
+ {
2194
+ id: "file-auto-recover-seconds",
2195
+ path: "autoRecoverSeconds",
2196
+ title: "Auto-recovery delay",
2197
+ group: "machine-detection-sessions",
2198
+ description: "Delay before an interrupted state can recover.",
2199
+ control: "number",
2200
+ integer: false,
2201
+ step: "any",
2202
+ range: POSITIVE_NUMBER_RANGE,
2203
+ unit: SECONDS,
2204
+ keywords: ["timer", "recovery"],
2205
+ defaultValue: 3,
2206
+ takesEffect: "new-sessions",
2207
+ fileOnlyReason: TUNING_KNOB,
2208
+ ...FILE_ONLY
2209
+ },
2210
+ {
2211
+ id: "file-input-grace-seconds",
2212
+ path: "inputGraceSeconds",
2213
+ title: "Input grace period",
2214
+ group: "machine-detection-sessions",
2215
+ description: "Grace window around operator input.",
2216
+ control: "number",
2217
+ integer: false,
2218
+ step: "any",
2219
+ range: POSITIVE_NUMBER_RANGE,
2220
+ unit: SECONDS,
2221
+ keywords: ["timer", "prompt"],
2222
+ defaultValue: 5,
2223
+ takesEffect: "new-sessions",
2224
+ fileOnlyReason: TUNING_KNOB,
2225
+ ...FILE_ONLY
2226
+ },
2227
+ {
2228
+ id: "file-prompt-detection-ms",
2229
+ path: "promptDetectionMs",
2230
+ title: "Prompt detection delay",
2231
+ group: "machine-detection-sessions",
2232
+ description: "Timing threshold used by prompt detection.",
2233
+ control: "number",
2234
+ integer: false,
2235
+ step: "any",
2236
+ range: POSITIVE_NUMBER_RANGE,
2237
+ unit: MILLISECONDS,
2238
+ keywords: ["timer", "detection"],
2239
+ defaultValue: 1500,
2240
+ takesEffect: "new-sessions",
2241
+ fileOnlyReason: TUNING_KNOB,
2242
+ ...FILE_ONLY
2243
+ },
2244
+ {
2245
+ id: "file-notify-debounce-ms",
2246
+ path: "notifyDebounceMs",
2247
+ title: "Notification debounce",
2248
+ group: "machine-detection-sessions",
2249
+ description: "Delay used to coalesce notification state changes.",
2250
+ control: "number",
2251
+ integer: false,
2252
+ step: "any",
2253
+ range: POSITIVE_NUMBER_RANGE,
2254
+ unit: SECONDS_STORED_AS_MS,
2255
+ keywords: ["timer", "alerts"],
2256
+ defaultValue: 3e3,
2257
+ takesEffect: "new-sessions",
2258
+ fileOnlyReason: TUNING_KNOB,
2259
+ ...FILE_ONLY
2260
+ },
2261
+ {
2262
+ id: "file-phone-escalation-ms",
2263
+ path: "phoneEscalationMs",
2264
+ title: "Phone escalation delay",
2265
+ group: "machine-detection-sessions",
2266
+ description: "Delay before off-dashboard escalation.",
2267
+ control: "number",
2268
+ integer: false,
2269
+ step: "any",
2270
+ range: POSITIVE_NUMBER_RANGE,
2271
+ unit: MINUTES_STORED_AS_MS,
2272
+ keywords: ["timer", "telegram"],
2273
+ defaultValue: 3e5,
2274
+ takesEffect: "now",
2275
+ fileOnlyReason: TUNING_KNOB,
2276
+ ...FILE_ONLY
2277
+ },
2278
+ {
2279
+ id: "file-post-turn-checks",
2280
+ path: "postTurnChecks",
2281
+ title: "Post-turn checks",
2282
+ group: "machine-detection-sessions",
2283
+ description: "Deterministic checks run after eligible turns. The global mode is a default; project and individual rule modes can select fixes even when the global mode is report.",
2284
+ control: "json",
2285
+ keywords: ["quality", "fixes"],
2286
+ defaultValue: {
2287
+ enabled: true,
2288
+ mode: "report",
2289
+ rules: {
2290
+ trailingWs: true,
2291
+ finalNewline: true,
2292
+ bom: true,
2293
+ slop: false
2294
+ }
2295
+ },
2296
+ takesEffect: "now",
2297
+ fileOnlyReason: STRUCTURED_VALUE,
2298
+ ...FILE_ONLY
2299
+ },
2300
+ {
2301
+ id: "file-branch-gc-enabled",
2302
+ path: "branchGc.enabled",
2303
+ title: "Branch cleanup",
2304
+ group: "machine-detection-sessions",
2305
+ description: "Enable cleanup of eligible session branches.",
2306
+ control: "toggle",
2307
+ keywords: ["git", "cleanup"],
2308
+ defaultValue: true,
2309
+ takesEffect: "now",
2310
+ fileOnlyReason: PROCESS_SETTING,
2311
+ ...FILE_ONLY
2312
+ },
2313
+ {
2314
+ id: "file-branch-gc-worktrees",
2315
+ path: "branchGc.worktrees",
2316
+ title: "Local worktree cleanup",
2317
+ group: "machine-detection-sessions",
2318
+ description: "Enable cleanup of eligible local worktrees.",
2319
+ control: "toggle",
2320
+ keywords: [
2321
+ "git",
2322
+ "worktree",
2323
+ "cleanup"
2324
+ ],
2325
+ defaultValue: true,
2326
+ takesEffect: "now",
2327
+ fileOnlyReason: PROCESS_SETTING,
2328
+ ...FILE_ONLY
2329
+ },
2330
+ {
2331
+ id: "file-branch-gc-prefixes",
2332
+ path: "branchGc.prefixes",
2333
+ title: "Branch cleanup prefixes",
2334
+ group: "machine-detection-sessions",
2335
+ description: "Branch prefixes that filter both remote branch cleanup and local worktree cleanup.",
2336
+ control: "list",
2337
+ keywords: ["git", "branch"],
2338
+ defaultValue: ["glimmervoid/session/", "worktree-agent-"],
2339
+ takesEffect: "now",
2340
+ fileOnlyReason: PROCESS_SETTING,
2341
+ ...FILE_ONLY
2342
+ },
2343
+ {
2344
+ id: "file-branch-gc-dry-run",
2345
+ path: "branchGc.dryRun",
2346
+ title: "Branch cleanup dry run",
2347
+ group: "machine-detection-sessions",
2348
+ description: "Report planned cleanup without deleting remote branches.",
2349
+ control: "toggle",
2350
+ keywords: ["git", "safety"],
2351
+ defaultValue: false,
2352
+ takesEffect: "now",
2353
+ fileOnlyReason: PROCESS_SETTING,
2354
+ ...FILE_ONLY
2355
+ },
2356
+ {
2357
+ id: "file-branch-gc-stale-days",
2358
+ path: "branchGc.staleDays",
2359
+ title: "Branch stale days",
2360
+ group: "machine-detection-sessions",
2361
+ description: "Age threshold for orphan branch cleanup when unmerged deletion is on.",
2362
+ control: "number",
2363
+ integer: false,
2364
+ step: "any",
2365
+ range: POSITIVE_NUMBER_RANGE,
2366
+ unit: DAYS,
2367
+ keywords: ["git", "retention"],
2368
+ defaultValue: 14,
2369
+ takesEffect: "now",
2370
+ fileOnlyReason: TUNING_KNOB,
2371
+ ...FILE_ONLY
2372
+ },
2373
+ {
2374
+ id: "file-branch-gc-interval-ms",
2375
+ path: "branchGc.intervalMs",
2376
+ title: "Branch cleanup interval",
2377
+ group: "machine-detection-sessions",
2378
+ description: "Delay between branch cleanup passes.",
2379
+ control: "number",
2380
+ integer: false,
2381
+ step: "any",
2382
+ range: POSITIVE_NUMBER_RANGE,
2383
+ unit: HOURS_STORED_AS_MS,
2384
+ keywords: ["git", "schedule"],
2385
+ defaultValue: 216e5,
2386
+ takesEffect: "now",
2387
+ fileOnlyReason: TUNING_KNOB,
2388
+ ...FILE_ONLY
2389
+ },
2390
+ {
2391
+ id: "file-custom-agents",
2392
+ path: "customAgents",
2393
+ title: "Custom agents",
2394
+ group: "machine-detection-sessions",
2395
+ description: "Extra agent CLIs a session can be spawned with. The Sessions section lists the declared agents and whether each command resolves.",
2396
+ control: "json",
2397
+ keywords: ["adapters", "cli"],
2398
+ defaultValue: [],
2399
+ takesEffect: "new-sessions",
2400
+ fileOnlyReason: BY_RULE,
2401
+ ...FILE_ONLY
2402
+ },
2403
+ {
2404
+ id: "file-task-title-refiner-enabled",
2405
+ path: "taskTitle.refiner.enabled",
2406
+ title: "Refine task titles",
2407
+ group: "machine-detection-sessions",
2408
+ description: "Keep session card titles useful when prompts change topic by asking a model for a short title.",
2409
+ control: "toggle",
2410
+ keywords: [
2411
+ "title",
2412
+ "refiner",
2413
+ "card"
2414
+ ],
2415
+ defaultValue: true,
2416
+ takesEffect: "now",
2417
+ fileOnlyReason: TUNING_KNOB,
2418
+ ...FILE_ONLY
2419
+ },
2420
+ {
2421
+ id: "file-task-title-refiner-model",
2422
+ path: "taskTitle.refiner.model",
2423
+ title: "Task title model",
2424
+ group: "machine-detection-sessions",
2425
+ description: "Claude model alias the title refiner asks.",
2426
+ control: "text",
2427
+ keywords: [
2428
+ "title",
2429
+ "refiner",
2430
+ "model"
2431
+ ],
2432
+ defaultValue: "haiku",
2433
+ takesEffect: "now",
2434
+ fileOnlyReason: TUNING_KNOB,
2435
+ ...FILE_ONLY
2436
+ },
2437
+ {
2438
+ id: "file-task-title-refiner-min-interval",
2439
+ path: "taskTitle.refiner.minIntervalSeconds",
2440
+ title: "Task title refresh interval",
2441
+ group: "machine-detection-sessions",
2442
+ description: "Shortest pause between two title refinements of one session.",
2443
+ control: "number",
2444
+ integer: false,
2445
+ step: "any",
2446
+ range: NON_NEGATIVE_NUMBER_RANGE,
2447
+ unit: SECONDS,
2448
+ keywords: [
2449
+ "title",
2450
+ "refiner",
2451
+ "interval"
2452
+ ],
2453
+ defaultValue: 60,
2454
+ takesEffect: "now",
2455
+ fileOnlyReason: TUNING_KNOB,
2456
+ ...FILE_ONLY
2457
+ },
2458
+ {
2459
+ id: "file-task-title-refiner-timeout",
2460
+ path: "taskTitle.refiner.timeoutSeconds",
2461
+ title: "Task title timeout",
2462
+ group: "machine-detection-sessions",
2463
+ description: "Maximum time allowed for one title refinement.",
2464
+ control: "number",
2465
+ integer: false,
2466
+ step: "any",
2467
+ range: TASK_TITLE_TIMEOUT_RANGE,
2468
+ unit: SECONDS,
2469
+ keywords: [
2470
+ "title",
2471
+ "refiner",
2472
+ "deadline"
2473
+ ],
2474
+ defaultValue: 60,
2475
+ takesEffect: "now",
2476
+ fileOnlyReason: TUNING_KNOB,
2477
+ ...FILE_ONLY
2478
+ },
2479
+ {
2480
+ id: "file-usage-codex",
2481
+ path: "usage.vendors.codex",
2482
+ title: "Track Codex usage",
2483
+ group: "machine-usage",
2484
+ description: "Include local Codex CLI transcripts in usage reports.",
2485
+ control: "toggle",
2486
+ keywords: ["openai", "vendor"],
2487
+ defaultValue: true,
2488
+ takesEffect: "now",
2489
+ fileOnlyReason: TUNING_KNOB,
2490
+ ...FILE_ONLY
2491
+ },
2492
+ {
2493
+ id: "file-usage-grok",
2494
+ path: "usage.vendors.grok",
2495
+ title: "Track Grok usage",
2496
+ group: "machine-usage",
2497
+ description: "Include local Grok CLI transcripts in usage reports.",
2498
+ control: "toggle",
2499
+ keywords: ["xai", "vendor"],
2500
+ defaultValue: true,
2501
+ takesEffect: "now",
2502
+ fileOnlyReason: TUNING_KNOB,
2503
+ ...FILE_ONLY
2504
+ },
2505
+ {
2506
+ id: "file-usage-fetch-pricing",
2507
+ path: "usage.fetchPricing",
2508
+ title: "Fetch current model prices",
2509
+ group: "machine-usage",
2510
+ description: "Load public model prices when usage tracking starts, using a cached table for up to one day. Falls back to cached or bundled prices if the fetch fails; no daily refresh runs while tracking continues.",
2511
+ control: "toggle",
2512
+ keywords: ["rates", "models"],
2513
+ defaultValue: true,
2514
+ takesEffect: "restart",
2515
+ fileOnlyReason: TUNING_KNOB,
2516
+ ...FILE_ONLY
2517
+ },
2518
+ {
2519
+ id: "file-usage-plan-limits",
2520
+ path: "usage.planLimits",
2521
+ title: "Show plan limits",
2522
+ group: "machine-usage",
2523
+ description: "Read the Claude plan limit the CLI reports and show it beside usage.",
2524
+ control: "toggle",
2525
+ keywords: [
2526
+ "plan",
2527
+ "limits",
2528
+ "quota"
2529
+ ],
2530
+ defaultValue: true,
2531
+ takesEffect: "now",
2532
+ fileOnlyReason: TUNING_KNOB,
2533
+ ...FILE_ONLY
2534
+ },
2535
+ {
2536
+ id: "file-usage-rtk-savings",
2537
+ path: "usage.rtkSavings",
2538
+ title: "Estimate rtk savings",
2539
+ group: "machine-usage",
2540
+ description: "Estimate the tokens rtk output compression saved and show it in the Usage view.",
2541
+ control: "toggle",
2542
+ keywords: [
2543
+ "rtk",
2544
+ "savings",
2545
+ "tokens"
2546
+ ],
2547
+ defaultValue: true,
2548
+ takesEffect: "now",
2549
+ fileOnlyReason: TUNING_KNOB,
2550
+ ...FILE_ONLY
2551
+ },
2552
+ {
2553
+ id: "file-usage-scan-interval",
2554
+ path: "usage.scanIntervalMinutes",
2555
+ title: "Scan interval",
2556
+ group: "machine-usage",
2557
+ description: "Periodic transcript polling interval. Session activity can also trigger scans between timer ticks.",
2558
+ control: "number",
2559
+ range: USAGE_SCAN_INTERVAL_RANGE,
2560
+ unit: MINUTES,
2561
+ keywords: ["poll", "refresh"],
2562
+ defaultValue: 5,
2563
+ takesEffect: "now",
2564
+ fileOnlyReason: TUNING_KNOB,
2565
+ ...FILE_ONLY
2566
+ },
2567
+ {
2568
+ id: "file-usage-warehouse-retain-days",
2569
+ path: "usage.warehouseRetainDays",
2570
+ title: "Retain usage history",
2571
+ group: "machine-usage",
2572
+ description: "How long the daily usage warehouse keeps rolled-up totals after transcript detail expires.",
2573
+ control: "number",
2574
+ range: USAGE_INTEGER_RANGES.warehouseRetainDays,
2575
+ unit: DAYS,
2576
+ keywords: [
2577
+ "history",
2578
+ "warehouse",
2579
+ "retention"
2580
+ ],
2581
+ defaultValue: 365,
2582
+ takesEffect: "now",
2583
+ fileOnlyReason: TUNING_KNOB,
2584
+ ...FILE_ONLY
2585
+ },
2586
+ {
2587
+ id: "file-usage-session-block-hours",
2588
+ path: "usage.sessionBlockHours",
2589
+ title: "Session block length",
2590
+ group: "machine-usage",
2591
+ description: "Length of the rolling block the Usage view groups activity into.",
2592
+ control: "number",
2593
+ range: USAGE_INTEGER_RANGES.sessionBlockHours,
2594
+ unit: HOURS,
2595
+ keywords: [
2596
+ "block",
2597
+ "window",
2598
+ "hours"
2599
+ ],
2600
+ defaultValue: 5,
2601
+ takesEffect: "now",
2602
+ fileOnlyReason: TUNING_KNOB,
2603
+ ...FILE_ONLY
2604
+ },
2605
+ {
2606
+ id: "file-usage-cost-mode",
2607
+ path: "usage.costMode",
2608
+ title: "Cost mode",
2609
+ group: "machine-usage",
2610
+ description: "Choose whether recorded costs, calculated costs or both can appear.",
2611
+ control: "select",
2612
+ options: capitalizedOptions(USAGE_COST_MODES),
2613
+ keywords: ["pricing", "estimate"],
2614
+ defaultValue: "auto",
2615
+ takesEffect: "now",
2616
+ fileOnlyReason: TUNING_KNOB,
2617
+ ...FILE_ONLY
2618
+ },
2619
+ {
2620
+ id: "file-usage-extra-projects-dirs",
2621
+ path: "usage.extraProjectsDirs",
2622
+ title: "Extra transcript folders",
2623
+ group: "machine-usage",
2624
+ description: "Absolute paths of additional Claude projects folders the usage scan reads.",
2625
+ control: "list",
2626
+ keywords: [
2627
+ "transcripts",
2628
+ "folders",
2629
+ "scan"
2630
+ ],
2631
+ defaultValue: [],
2632
+ takesEffect: "now",
2633
+ fileOnlyReason: TUNING_KNOB,
2634
+ ...FILE_ONLY
2635
+ },
2636
+ {
2637
+ id: "file-change-map-narrator-timeout",
2638
+ path: "changeMap.narrator.timeoutSeconds",
2639
+ title: "Narrator timeout",
2640
+ group: "lanes-change-map",
2641
+ description: "Maximum time allowed for one narration.",
2642
+ control: "number",
2643
+ range: CHANGE_MAP_NARRATOR_TIMEOUT_RANGE,
2644
+ unit: SECONDS,
2645
+ keywords: ["deadline", "narration"],
2646
+ defaultValue: 90,
2647
+ takesEffect: "now",
2648
+ fileOnlyReason: TUNING_KNOB,
2649
+ ...FILE_ONLY
2650
+ },
2651
+ {
2652
+ id: "file-visions-quiet-delay",
2653
+ path: "visions.dispatch.quietMs",
2654
+ title: "Quiet delay",
2655
+ group: "lanes-visions",
2656
+ description: "Editor quiet time before a scheduled review. Saves and blank-line boundaries can dispatch immediately, subject to the other review limits.",
2657
+ control: "number",
2658
+ range: POSITIVE_NUMBER_RANGE,
2659
+ unit: SECONDS_STORED_AS_MS,
2660
+ keywords: ["debounce", "idle"],
2661
+ defaultValue: 3e4,
2662
+ takesEffect: "now",
2663
+ fileOnlyReason: TUNING_KNOB,
2664
+ ...FILE_ONLY
2665
+ },
2666
+ {
2667
+ id: "file-visions-cooldown",
2668
+ path: "visions.dispatch.cooldownMs",
2669
+ title: "Cooldown",
2670
+ group: "lanes-visions",
2671
+ description: "Minimum delay between reviews of the same document. Initial orientation reviews bypass this delay.",
2672
+ control: "number",
2673
+ range: POSITIVE_NUMBER_RANGE,
2674
+ unit: MINUTES_STORED_AS_MS,
2675
+ keywords: ["rate limit", "delay"],
2676
+ defaultValue: 3e5,
2677
+ takesEffect: "now",
2678
+ fileOnlyReason: TUNING_KNOB,
2679
+ ...FILE_ONLY
2680
+ },
2681
+ {
2682
+ id: "file-visions-activity-max-per-hour",
2683
+ path: "visions.dispatch.activityMaxPerHour",
2684
+ title: "Activity max per hour",
2685
+ group: "lanes-visions",
2686
+ description: "Maximum activity-driven reviews dispatched per hour, capped at the overall hourly limit minus one to reserve an edit-review slot.",
2687
+ control: "number",
2688
+ range: NON_NEGATIVE_NUMBER_RANGE,
2689
+ unit: REVIEWS_PER_HOUR,
2690
+ keywords: ["rate limit", "events"],
2691
+ defaultValue: 2,
2692
+ takesEffect: "now",
2693
+ fileOnlyReason: TUNING_KNOB,
2694
+ ...FILE_ONLY
2695
+ },
2696
+ {
2697
+ id: "file-visions-dispatch-timeout",
2698
+ path: "visions.dispatch.dispatchTimeoutSeconds",
2699
+ title: "Dispatch timeout",
2700
+ group: "lanes-visions",
2701
+ description: "Maximum time allowed for one review session.",
2702
+ control: "number",
2703
+ range: POSITIVE_NUMBER_RANGE,
2704
+ unit: MINUTES_STORED_AS_SECONDS,
2705
+ keywords: ["deadline", "review"],
2706
+ defaultValue: 180,
2707
+ takesEffect: "now",
2708
+ fileOnlyReason: TUNING_KNOB,
2709
+ ...FILE_ONLY
2710
+ },
2711
+ {
2712
+ id: "file-visions-intent-thread-ttl",
2713
+ path: "visions.intent.threadTtlMs",
2714
+ title: "Intent thread lifetime",
2715
+ group: "lanes-visions",
2716
+ description: "How long an intent thread nobody advanced stays live before it retires.",
2717
+ control: "number",
2718
+ range: POSITIVE_NUMBER_RANGE,
2719
+ unit: DAYS_STORED_AS_MS,
2720
+ keywords: [
2721
+ "intent",
2722
+ "decay",
2723
+ "thread"
2724
+ ],
2725
+ defaultValue: 2592e5,
2726
+ takesEffect: "now",
2727
+ fileOnlyReason: TUNING_KNOB,
2728
+ ...FILE_ONLY
2729
+ },
2730
+ {
2731
+ id: "file-ingest-terminal",
2732
+ path: "ingest.sources.terminal.enabled",
2733
+ title: "Terminal output source",
2734
+ group: "lanes-ingest",
2735
+ description: "Include terminal output in the ingest feed.",
2736
+ control: "toggle",
2737
+ keywords: ["pty", "source"],
2738
+ defaultValue: false,
2739
+ takesEffect: "now",
2740
+ fileOnlyReason: TUNING_KNOB,
2741
+ ...FILE_ONLY
2742
+ },
2743
+ {
2744
+ id: "file-ingest-agent-logs",
2745
+ path: "ingest.sources.agentLogs.enabled",
2746
+ title: "Agent logs source",
2747
+ group: "lanes-ingest",
2748
+ description: "Include local agent logs in the ingest feed.",
2749
+ control: "toggle",
2750
+ keywords: ["transcript", "source"],
2751
+ defaultValue: false,
2752
+ takesEffect: "now",
2753
+ fileOnlyReason: TUNING_KNOB,
2754
+ ...FILE_ONLY
2755
+ },
2756
+ {
2757
+ id: "file-ingest-git",
2758
+ path: "ingest.sources.git.enabled",
2759
+ title: "Git activity source",
2760
+ group: "lanes-ingest",
2761
+ description: "Include local Git activity in the ingest feed.",
2762
+ control: "toggle",
2763
+ keywords: ["commits", "source"],
2764
+ defaultValue: false,
2765
+ takesEffect: "now",
2766
+ fileOnlyReason: TUNING_KNOB,
2767
+ ...FILE_ONLY
2768
+ },
2769
+ {
2770
+ id: "file-ingest-fs",
2771
+ path: "ingest.sources.fs.enabled",
2772
+ title: "File changes source",
2773
+ group: "lanes-ingest",
2774
+ description: "Include watched file changes in the ingest feed.",
2775
+ control: "toggle",
2776
+ keywords: ["filesystem", "source"],
2777
+ defaultValue: false,
2778
+ takesEffect: "now",
2779
+ fileOnlyReason: TUNING_KNOB,
2780
+ ...FILE_ONLY
2781
+ },
2782
+ {
2783
+ id: "file-ingest-shell-history",
2784
+ path: "ingest.sources.shellHistory.enabled",
2785
+ title: "Shell history source",
2786
+ group: "lanes-ingest",
2787
+ description: "Include local shell history in the ingest feed.",
2788
+ control: "toggle",
2789
+ keywords: ["commands", "source"],
2790
+ defaultValue: false,
2791
+ takesEffect: "now",
2792
+ fileOnlyReason: TUNING_KNOB,
2793
+ ...FILE_ONLY
2794
+ },
2795
+ {
2796
+ id: "file-ingest-editor",
2797
+ path: "ingest.sources.editor.enabled",
2798
+ title: "Editor events source",
2799
+ group: "lanes-ingest",
2800
+ description: "Include local editor events in the ingest feed.",
2801
+ control: "toggle",
2802
+ keywords: ["buffers", "source"],
2803
+ defaultValue: false,
2804
+ takesEffect: "now",
2805
+ fileOnlyReason: TUNING_KNOB,
2806
+ ...FILE_ONLY
2807
+ },
2808
+ {
2809
+ id: "file-posthog-investigation-timeout",
2810
+ path: "posthog.investigationTimeoutSeconds",
2811
+ title: "Investigation timeout",
2812
+ group: "lanes-posthog",
2813
+ description: "Maximum time allowed for one investigation.",
2814
+ control: "number",
2815
+ integer: false,
2816
+ step: "any",
2817
+ range: POSITIVE_NUMBER_RANGE,
2818
+ unit: MINUTES_STORED_AS_SECONDS,
2819
+ keywords: ["deadline", "session"],
2820
+ defaultValue: 900,
2821
+ takesEffect: "now",
2822
+ fileOnlyReason: TUNING_KNOB,
2823
+ ...FILE_ONLY
2824
+ },
2825
+ {
2826
+ id: "file-posthog-escalation",
2827
+ path: "posthog.userEscalationThreshold",
2828
+ title: "Escalation threshold",
2829
+ group: "lanes-posthog",
2830
+ description: "Affected-user count that escalates an issue.",
2831
+ control: "number",
2832
+ range: POSITIVE_NUMBER_RANGE,
2833
+ unit: USERS,
2834
+ keywords: ["threshold", "severity"],
2835
+ defaultValue: 25,
2836
+ takesEffect: "now",
2837
+ fileOnlyReason: TUNING_KNOB,
2838
+ ...FILE_ONLY
2839
+ },
2840
+ {
2841
+ id: "file-posthog-fix-timeout",
2842
+ path: "posthog.fixTimeoutSeconds",
2843
+ title: "Fix timeout",
2844
+ group: "lanes-posthog",
2845
+ description: "Maximum time allowed for one fix session.",
2846
+ control: "number",
2847
+ integer: false,
2848
+ step: "any",
2849
+ range: POSTHOG_FIX_TIMEOUT_RANGE,
2850
+ unit: MINUTES_STORED_AS_SECONDS,
2851
+ keywords: ["deadline", "repair"],
2852
+ defaultValue: 1800,
2853
+ takesEffect: "now",
2854
+ fileOnlyReason: TUNING_KNOB,
2855
+ ...FILE_ONLY
2856
+ },
2857
+ {
2858
+ id: "file-posthog-traffic-multiplier",
2859
+ path: "posthog.trafficSpikeMultiplier",
2860
+ title: "Spike multiplier",
2861
+ group: "lanes-posthog",
2862
+ description: "Baseline multiplier required to classify a traffic spike.",
2863
+ control: "number",
2864
+ integer: false,
2865
+ step: "any",
2866
+ range: POSTHOG_TRAFFIC_MULTIPLIER_RANGE,
2867
+ unit: MULTIPLIER,
2868
+ keywords: ["baseline", "ratio"],
2869
+ defaultValue: 3,
2870
+ takesEffect: "now",
2871
+ fileOnlyReason: TUNING_KNOB,
2872
+ ...FILE_ONLY
2873
+ },
2874
+ {
2875
+ id: "file-posthog-traffic-min-users",
2876
+ path: "posthog.trafficSpikeMinUsers",
2877
+ title: "Min users to alert",
2878
+ group: "lanes-posthog",
2879
+ description: "Minimum recent unique users for an initial traffic-spike alert. Escalation and recovery alerts bypass this minimum.",
2880
+ control: "number",
2881
+ range: POSTHOG_TRAFFIC_MIN_USERS_RANGE,
2882
+ unit: USERS,
2883
+ keywords: ["threshold", "analytics"],
2884
+ defaultValue: 10,
2885
+ takesEffect: "now",
2886
+ fileOnlyReason: TUNING_KNOB,
2887
+ ...FILE_ONLY
2888
+ },
2889
+ {
2890
+ id: "file-posthog-traffic-cooldown",
2891
+ path: "posthog.trafficSpikeCooldownMinutes",
2892
+ title: "Spike cooldown",
2893
+ group: "lanes-posthog",
2894
+ description: "Cooldown for new traffic spikes. Escalation and recovery alerts bypass it. Zero removes the time delay; repeated active-spike alerts are still suppressed.",
2895
+ control: "number",
2896
+ integer: false,
2897
+ step: "any",
2898
+ range: NON_NEGATIVE_NUMBER_RANGE,
2899
+ unit: HOURS_STORED_AS_MINUTES,
2900
+ keywords: ["silence", "delay"],
2901
+ defaultValue: 360,
2902
+ takesEffect: "now",
2903
+ fileOnlyReason: TUNING_KNOB,
2904
+ ...FILE_ONLY
2905
+ },
2906
+ {
2907
+ id: "file-posthog-traffic-baseline",
2908
+ path: "posthog.trafficSpikeBaselineDays",
2909
+ title: "Baseline window",
2910
+ group: "lanes-posthog",
2911
+ description: "Historical window used to calculate normal hourly traffic.",
2912
+ control: "number",
2913
+ range: POSTHOG_TRAFFIC_BASELINE_RANGE,
2914
+ unit: DAYS,
2915
+ keywords: ["history", "analytics"],
2916
+ defaultValue: 7,
2917
+ takesEffect: "now",
2918
+ fileOnlyReason: TUNING_KNOB,
2919
+ ...FILE_ONLY
2920
+ },
2921
+ {
2922
+ id: "file-posthog-recurrence-dedupe",
2923
+ path: "posthog.recurrenceDedupe",
2924
+ title: "Deduplicate recurring issues",
2925
+ group: "lanes-posthog",
2926
+ description: "Treat an issue seen again inside the recurrence window as the same investigation instead of opening a new one.",
2927
+ control: "toggle",
2928
+ keywords: [
2929
+ "recurrence",
2930
+ "dedupe",
2931
+ "issues"
2932
+ ],
2933
+ defaultValue: true,
2934
+ takesEffect: "now",
2935
+ fileOnlyReason: TUNING_KNOB,
2936
+ ...FILE_ONLY
2937
+ },
2938
+ {
2939
+ id: "file-posthog-recurrence-window-days",
2940
+ path: "posthog.recurrenceWindowDays",
2941
+ title: "Recurrence window",
2942
+ group: "lanes-posthog",
2943
+ description: "How far back an issue counts as a recurrence of an investigated one.",
2944
+ control: "number",
2945
+ integer: false,
2946
+ step: "any",
2947
+ range: POSITIVE_NUMBER_RANGE,
2948
+ unit: DAYS,
2949
+ keywords: ["recurrence", "window"],
2950
+ defaultValue: 7,
2951
+ takesEffect: "now",
2952
+ fileOnlyReason: TUNING_KNOB,
2953
+ ...FILE_ONLY
2954
+ },
2955
+ {
2956
+ id: "file-posthog-transient-recurrence-limit",
2957
+ path: "posthog.transientRecurrenceLimit",
2958
+ title: "Transient recurrence limit",
2959
+ group: "lanes-posthog",
2960
+ description: "How many times a transient issue may recur before it is investigated again.",
2961
+ control: "number",
2962
+ integer: false,
2963
+ step: "any",
2964
+ range: POSITIVE_NUMBER_RANGE,
2965
+ unit: RECURRENCES,
2966
+ keywords: [
2967
+ "recurrence",
2968
+ "transient",
2969
+ "limit"
2970
+ ],
2971
+ defaultValue: 3,
2972
+ takesEffect: "now",
2973
+ fileOnlyReason: TUNING_KNOB,
2974
+ ...FILE_ONLY
2975
+ },
2976
+ {
2977
+ id: "file-posthog-repo-path",
2978
+ path: "posthog.repoPath",
2979
+ title: "Repository path",
2980
+ group: "lanes-posthog",
2981
+ description: "Absolute path of the local checkout investigations read and fixes branch from, for every project the project map does not name.",
2982
+ control: "text",
2983
+ keywords: [
2984
+ "repository",
2985
+ "checkout",
2986
+ "path"
2987
+ ],
2988
+ defaultValue: "",
2989
+ takesEffect: "now",
2990
+ fileOnlyReason: TUNING_KNOB,
2991
+ ...FILE_ONLY
2992
+ },
2993
+ {
2994
+ id: "file-posthog-project-map",
2995
+ path: "posthog.projectMap",
2996
+ title: "Project map",
2997
+ group: "lanes-posthog",
2998
+ description: "PostHog project id to a label: the name shown for that project, and, when it is an absolute path to a checkout, the repository its issues are investigated in.",
2999
+ control: "json",
3000
+ keywords: [
3001
+ "projects",
3002
+ "repositories",
3003
+ "map"
3004
+ ],
3005
+ defaultValue: {},
3006
+ takesEffect: "now",
3007
+ fileOnlyReason: STRUCTURED_VALUE,
3008
+ ...FILE_ONLY
3009
+ },
3010
+ {
3011
+ id: "file-team-review-org",
3012
+ path: "teamReview.org",
3013
+ title: "GitHub organization (legacy)",
3014
+ group: "lanes-team-review",
3015
+ description: "Organization login that owns the team, for example PostHog. Read only when github.teams is empty.",
3016
+ control: "text",
3017
+ keywords: [
3018
+ "github",
3019
+ "owner",
3020
+ "legacy"
3021
+ ],
3022
+ defaultValue: "",
3023
+ takesEffect: "now",
3024
+ fileOnlyReason: "A legacy key: github.teams names teams as org/slug entries and wins whenever it is set.",
3025
+ ...FILE_ONLY
3026
+ },
3027
+ {
3028
+ id: "file-team-review-team",
3029
+ path: "teamReview.team",
3030
+ title: "GitHub team (legacy)",
3031
+ group: "lanes-team-review",
3032
+ description: "Team slug whose members and review requests are polled. Read only when github.teams is empty.",
3033
+ control: "text",
3034
+ keywords: [
3035
+ "github",
3036
+ "slug",
3037
+ "legacy"
3038
+ ],
3039
+ defaultValue: "",
3040
+ takesEffect: "now",
3041
+ fileOnlyReason: "A legacy key: github.teams names teams as org/slug entries and wins whenever it is set.",
3042
+ ...FILE_ONLY
3043
+ },
3044
+ {
3045
+ id: "file-workflows-max-actions-per-poll",
3046
+ path: "workflows.maxActionsPerPoll",
3047
+ title: "Actions per poll",
3048
+ group: "lanes-workflows",
3049
+ description: "Most actions run in one poll. The rest are dropped and logged.",
3050
+ control: "number",
3051
+ range: WORKFLOWS_MAX_ACTIONS_PER_POLL_RANGE,
3052
+ unit: ACTIONS,
3053
+ keywords: ["cap", "rate limit"],
3054
+ defaultValue: 20,
3055
+ takesEffect: "now",
3056
+ fileOnlyReason: TUNING_KNOB,
3057
+ ...FILE_ONLY
3058
+ },
3059
+ {
3060
+ id: "file-workflows-rules",
3061
+ path: "workflows.rules",
3062
+ title: "Workflow rule definitions",
3063
+ group: "lanes-workflows",
3064
+ description: "The rules the Workflows section switches on and off: each names its repositories, one pull request event, its filters and its actions.",
3065
+ control: "json",
3066
+ keywords: [
3067
+ "pull requests",
3068
+ "automation",
3069
+ "rules"
3070
+ ],
3071
+ defaultValue: [],
3072
+ takesEffect: "now",
3073
+ fileOnlyReason: "Rule definitions are written in config.json; the Workflows section only switches each rule on or off.",
3074
+ ...FILE_ONLY
3075
+ }
3076
+ ]);
3077
+ var SETTINGS_REGISTRY = Object.freeze({
3078
+ sections: SECTIONS,
3079
+ fileOnly: FILE_ONLY_SETTINGS
3080
+ });
3081
+ Object.freeze(Object.fromEntries(SECTIONS.flatMap((section) => (section.legacyIds ?? []).map((legacyId) => [legacyId, section.id]))));
3082
+ Object.freeze(SECTIONS.reduce((moved, section) => {
3083
+ for (const setting of section.settings) for (const legacy of setting.legacyIds ?? []) moved[legacy.sectionId] = {
3084
+ ...moved[legacy.sectionId],
3085
+ [legacy.settingId]: section.id
3086
+ };
3087
+ return moved;
3088
+ }, {}));
3089
+ //#endregion
3090
+ //#region shared/settings/registry-core.ts
3091
+ function cloneValue(value) {
3092
+ const containersToFill = [];
3093
+ const startCopyOf = (original) => {
3094
+ if (!original || typeof original !== "object") return original;
3095
+ const copy = Array.isArray(original) ? [] : {};
3096
+ containersToFill.push([original, copy]);
3097
+ return copy;
3098
+ };
3099
+ const root = startCopyOf(value);
3100
+ for (let next = containersToFill.pop(); next; next = containersToFill.pop()) {
3101
+ const [original, copy] = next;
3102
+ for (const [key, nested] of Object.entries(original)) Object.defineProperty(copy, key, {
3103
+ value: startCopyOf(nested),
3104
+ writable: true,
3105
+ enumerable: true,
3106
+ configurable: true
3107
+ });
3108
+ }
3109
+ return root;
3110
+ }
3111
+ function valueAtPath(source, path) {
3112
+ let value = source;
3113
+ for (const part of path.split(".")) {
3114
+ if (value == null || typeof value !== "object") return void 0;
3115
+ value = value[part];
3116
+ }
3117
+ return value;
3118
+ }
3119
+ function deleteValueAtPath(target, path) {
3120
+ const parts = path.split(".");
3121
+ let cursor = target;
3122
+ for (const part of parts.slice(0, -1)) {
3123
+ if (!cursor || typeof cursor !== "object") return;
3124
+ cursor = cursor[part];
3125
+ }
3126
+ if (cursor && typeof cursor === "object") delete cursor[parts.at(-1)];
3127
+ }
3128
+ function setValueAtPath(target, path, value) {
3129
+ const parts = path.split(".");
3130
+ let cursor = target;
3131
+ for (const part of parts.slice(0, -1)) {
3132
+ if (!cursor[part] || typeof cursor[part] !== "object" || Array.isArray(cursor[part])) cursor[part] = {};
3133
+ cursor = cursor[part];
3134
+ }
3135
+ cursor[parts.at(-1)] = cloneValue(value);
3136
+ }
3137
+ function settingsOf(sections) {
3138
+ return sections.flatMap((section) => section.settings);
3139
+ }
3140
+ function parsePosthogProjectSelection(value) {
3141
+ const text = String(value ?? "").trim().toLowerCase();
3142
+ if (!text || text === "all") return "all";
3143
+ if (text === "none") return [];
3144
+ const tokens = text.split(",").map((token) => token.trim());
3145
+ if (!tokens.every((token) => /^\d+$/.test(token) && Number.isSafeInteger(Number(token)) && Number(token) > 0)) return null;
3146
+ return tokens.map(Number);
3147
+ }
3148
+ var DISPLAY_DECIMALS = 6;
3149
+ function displayValue(setting, value) {
3150
+ if (setting.control === "number" && typeof value === "number") return value / (setting.unit?.storedPerDisplay ?? 1);
3151
+ if (setting.valueKind !== "posthog-projects") return cloneValue(value);
3152
+ if (!Array.isArray(value)) return [];
3153
+ if (value.length === 0) return ["none"];
3154
+ return value.map(String);
3155
+ }
3156
+ function hasScaledUnit(setting) {
3157
+ return setting.control === "number" && (setting.unit?.storedPerDisplay ?? 1) !== 1;
3158
+ }
3159
+ function formatDisplayNumber(value) {
3160
+ return String(Number(value.toFixed(DISPLAY_DECIMALS)));
3161
+ }
3162
+ function displayText(setting, value) {
3163
+ const shown = displayValue(setting, value);
3164
+ if (setting.valueKind === "posthog-projects" && Array.isArray(shown)) return shown.join(", ") || "all";
3165
+ if (typeof shown === "number" && setting.unit) return `${formatDisplayNumber(shown)} ${setting.unit.label}`;
3166
+ return JSON.stringify(shown ?? null);
3167
+ }
3168
+ function searchTokens(value) {
3169
+ return String(value || "").toLowerCase().match(/[a-z0-9]+/g) || [];
3170
+ }
3171
+ var EXACT_MATCH_QUALITY = 3;
3172
+ var PREFIX_MATCH_QUALITY = 2;
3173
+ var INFIX_MATCH_QUALITY = 1;
3174
+ var MIN_INFIX_QUERY_LENGTH = 3;
3175
+ var SEARCH_RESULT_LIMIT = 30;
3176
+ var MAX_SEARCH_QUERY_TOKENS = 16;
3177
+ function tokenMatchQuality(fieldToken, queryToken) {
3178
+ if (fieldToken === queryToken) return EXACT_MATCH_QUALITY;
3179
+ if (fieldToken.startsWith(queryToken)) return PREFIX_MATCH_QUALITY;
3180
+ if (queryToken.length >= MIN_INFIX_QUERY_LENGTH && fieldToken.includes(queryToken)) return INFIX_MATCH_QUALITY;
3181
+ return 0;
3182
+ }
3183
+ function fieldScore(tokens, queryTokens, weight) {
3184
+ let totalMatchQuality = 0;
3185
+ for (const queryToken of queryTokens) {
3186
+ const bestMatchQuality = Math.max(0, ...tokens.map((fieldToken) => tokenMatchQuality(fieldToken, queryToken)));
3187
+ if (bestMatchQuality === 0) return 0;
3188
+ totalMatchQuality += bestMatchQuality;
3189
+ }
3190
+ return totalMatchQuality * weight;
3191
+ }
3192
+ function optionLabelsText(setting) {
3193
+ return setting.options?.map((option) => option.label).join(", ") ?? "";
3194
+ }
3195
+ function fileOnlyGroupTitle(setting) {
3196
+ return settingsGroupOf(setting.group ?? "")?.title ?? "";
3197
+ }
3198
+ function settingScore(setting, sectionTokens, queryTokens) {
3199
+ const titleScore = fieldScore(searchTokens(setting.title), queryTokens, 1e3);
3200
+ const keywordScore = fieldScore(searchTokens(setting.keywords.join(" ")), queryTokens, 100);
3201
+ const sectionScore = fieldScore(sectionTokens, queryTokens, 10);
3202
+ const pathScore = fieldScore(searchTokens(setting.path), queryTokens, 10);
3203
+ const optionScore = fieldScore(searchTokens(optionLabelsText(setting)), queryTokens, 10);
3204
+ const descriptionScore = fieldScore(searchTokens(setting.description), queryTokens, 1);
3205
+ return titleScore + keywordScore + sectionScore + pathScore + optionScore + descriptionScore;
3206
+ }
3207
+ function resultGroupTitle(result) {
3208
+ return result.section?.title ?? "";
3209
+ }
3210
+ function compareEditableFirst(left, right) {
3211
+ return Number(left.section === null) - Number(right.section === null);
3212
+ }
3213
+ function searchMatches(setting, section, queryTokens) {
3214
+ return [
3215
+ ["title", setting.title],
3216
+ ["keywords", setting.keywords.join(" ")],
3217
+ ["section", section?.title ?? fileOnlyGroupTitle(setting)],
3218
+ ["description", setting.description],
3219
+ ["path", setting.path],
3220
+ ["options", optionLabelsText(setting)]
3221
+ ].flatMap(([field, text]) => {
3222
+ const spans = [];
3223
+ for (const token of text.matchAll(/[a-z0-9]+/gi)) {
3224
+ const fieldToken = token[0].toLowerCase();
3225
+ for (const queryToken of queryTokens) {
3226
+ if (tokenMatchQuality(fieldToken, queryToken) === 0) continue;
3227
+ const start = token.index + fieldToken.indexOf(queryToken);
3228
+ spans.push({
3229
+ start,
3230
+ end: start + queryToken.length
3231
+ });
3232
+ }
3233
+ }
3234
+ const mergedSpans = [];
3235
+ for (const span of spans.sort((left, right) => left.start - right.start || left.end - right.end)) {
3236
+ const previous = mergedSpans.at(-1);
3237
+ if (previous && span.start <= previous.end) {
3238
+ previous.end = Math.max(previous.end, span.end);
3239
+ continue;
3240
+ }
3241
+ mergedSpans.push({ ...span });
3242
+ }
3243
+ return mergedSpans.length ? [{
3244
+ field,
3245
+ text,
3246
+ spans: mergedSpans
3247
+ }] : [];
3248
+ });
3249
+ }
3250
+ function scoreSettingsSearch(sections, query, fileOnly = []) {
3251
+ const queryTokens = searchTokens(query).slice(0, MAX_SEARCH_QUERY_TOKENS);
3252
+ if (queryTokens.length === 0) return [];
3253
+ const results = [];
3254
+ for (const section of sections) {
3255
+ const sectionTokens = searchTokens(section.title);
3256
+ for (const setting of section.settings) {
3257
+ const score = settingScore(setting, sectionTokens, queryTokens);
3258
+ if (score === 0) continue;
3259
+ results.push({
3260
+ section,
3261
+ setting,
3262
+ score,
3263
+ matches: searchMatches(setting, section, queryTokens)
3264
+ });
3265
+ }
3266
+ }
3267
+ for (const setting of fileOnly) {
3268
+ const score = settingScore(setting, searchTokens(fileOnlyGroupTitle(setting)), queryTokens);
3269
+ if (score === 0) continue;
3270
+ results.push({
3271
+ section: null,
3272
+ setting,
3273
+ score,
3274
+ matches: searchMatches(setting, null, queryTokens)
3275
+ });
3276
+ }
3277
+ return results.sort((left, right) => right.score - left.score || compareEditableFirst(left, right) || resultGroupTitle(left).localeCompare(resultGroupTitle(right)) || left.setting.title.localeCompare(right.setting.title)).slice(0, SEARCH_RESULT_LIMIT);
3278
+ }
3279
+ function indexSettings(registry) {
3280
+ const byId = /* @__PURE__ */ new Map();
3281
+ const byPath = /* @__PURE__ */ new Map();
3282
+ for (const setting of [...settingsOf(registry.sections), ...registry.fileOnly]) {
3283
+ byId.set(setting.id, setting);
3284
+ byPath.set(setting.path, setting);
3285
+ }
3286
+ return {
3287
+ byId,
3288
+ byPath
3289
+ };
3290
+ }
3291
+ var SETTINGS_INDEX = indexSettings(SETTINGS_REGISTRY);
3292
+ function settingsGroupOf(groupId, registry = SETTINGS_REGISTRY) {
3293
+ const section = registry.sections.find((candidate) => candidate.id === groupId);
3294
+ if (section) return {
3295
+ title: section.title,
3296
+ description: section.description
3297
+ };
3298
+ return FILE_ONLY_GROUPS[groupId] ?? null;
3299
+ }
3300
+ //#endregion
3301
+ //#region server/core/settings-block-core.ts
3302
+ var SECRET_PRESENCE_SUFFIX = "Configured";
3303
+ var NO_VALUES = Object.freeze({});
3304
+ var MAX_SETTINGS_VALUE_DEPTH = 32;
3305
+ function valueSpecOf(setting, control) {
3306
+ const spec = {
3307
+ control,
3308
+ nullable: setting.nullable === true
3309
+ };
3310
+ if (setting.valueKind) spec.valueKind = setting.valueKind;
3311
+ return spec;
3312
+ }
3313
+ function freezeSpec(spec) {
3314
+ for (const nested of Object.values(spec.blocks)) freezeSpec(nested);
3315
+ Object.freeze(spec.booleans);
3316
+ Object.freeze(spec.blocks);
3317
+ if (spec.values) Object.freeze(spec.values);
3318
+ return Object.freeze(spec);
3319
+ }
3320
+ function buildBlockSpec(topLevelKey, registry) {
3321
+ const root = {
3322
+ name: topLevelKey,
3323
+ booleans: [],
3324
+ blocks: {}
3325
+ };
3326
+ const prefix = `${topLevelKey}.`;
3327
+ for (const setting of [...settingsOf(registry.sections), ...registry.fileOnly]) {
3328
+ const control = setting.control;
3329
+ if (control === "readonly") continue;
3330
+ if (setting.path === topLevelKey) {
3331
+ root.passthrough = true;
3332
+ continue;
3333
+ }
3334
+ if (!setting.path.startsWith(prefix)) continue;
3335
+ const parts = setting.path.slice(prefix.length).split(".");
3336
+ let node = root;
3337
+ for (const part of parts.slice(0, -1)) {
3338
+ node.blocks[part] ??= {
3339
+ name: `${node.name}.${part}`,
3340
+ booleans: [],
3341
+ blocks: {}
3342
+ };
3343
+ node = node.blocks[part];
3344
+ }
3345
+ const leaf = parts.at(-1);
3346
+ if (control === "toggle") {
3347
+ node.booleans.push(leaf);
3348
+ continue;
3349
+ }
3350
+ node.values ??= {};
3351
+ node.values[leaf] = valueSpecOf(setting, control);
3352
+ }
3353
+ return freezeSpec(root);
3354
+ }
3355
+ var registrySpecs = /* @__PURE__ */ new Map();
3356
+ function settingsBlockSpecFor(topLevelKey, registry = SETTINGS_REGISTRY) {
3357
+ if (registry !== SETTINGS_REGISTRY) return buildBlockSpec(topLevelKey, registry);
3358
+ const cached = registrySpecs.get(topLevelKey);
3359
+ if (cached) return cached;
3360
+ const built = buildBlockSpec(topLevelKey, registry);
3361
+ registrySpecs.set(topLevelKey, built);
3362
+ return built;
3363
+ }
3364
+ settingsBlockSpecFor("ingest");
3365
+ function isNestedDeeperThan(value, maxDepth) {
3366
+ const pending = [[value, 0]];
3367
+ for (let next = pending.pop(); next; next = pending.pop()) {
3368
+ const [node, depth] = next;
3369
+ if (!node || typeof node !== "object") continue;
3370
+ if (depth >= maxDepth) return true;
3371
+ for (const nested of Object.values(node)) pending.push([nested, depth + 1]);
3372
+ }
3373
+ return false;
3374
+ }
3375
+ function isStringArray(value) {
3376
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
3377
+ }
3378
+ function valueError(path, spec, value) {
3379
+ if (value === null) return spec.nullable ? null : `${path} must not be null`;
3380
+ if (isNestedDeeperThan(value, MAX_SETTINGS_VALUE_DEPTH)) return `${path} is nested too deeply`;
3381
+ if (spec.valueKind) return null;
3382
+ if (spec.control === "number") return typeof value === "number" && Number.isFinite(value) ? null : `${path} must be a number`;
3383
+ if (spec.control === "list" || spec.control === "projects") return isStringArray(value) ? null : `${path} must be an array of strings`;
3384
+ if (spec.control === "json") return isRecord(value) || Array.isArray(value) ? null : `${path} must be an object`;
3385
+ return typeof value === "string" ? null : `${path} must be a string`;
3386
+ }
3387
+ function validateSettingsBlock(block, spec) {
3388
+ if (block == null) return null;
3389
+ if (!isRecord(block)) return `${spec.name} must be an object`;
3390
+ const values = spec.values ?? NO_VALUES;
3391
+ for (const [key, value] of Object.entries(block)) {
3392
+ if (value === void 0) continue;
3393
+ if (Object.hasOwn(values, key)) {
3394
+ const error = valueError(`${spec.name}.${key}`, values[key], value);
3395
+ if (error) return error;
3396
+ continue;
3397
+ }
3398
+ if (value === null) continue;
3399
+ if (spec.booleans.includes(key)) {
3400
+ if (typeof value !== "boolean") return `${spec.name}.${key} must be a boolean`;
3401
+ continue;
3402
+ }
3403
+ if (Object.hasOwn(spec.blocks, key)) {
3404
+ const error = validateSettingsBlock(value, spec.blocks[key]);
3405
+ if (error) return error;
3406
+ continue;
3407
+ }
3408
+ if (!spec.passthrough) return `${spec.name}.${key} is not settable from the dashboard`;
3409
+ if (isNestedDeeperThan(value, MAX_SETTINGS_VALUE_DEPTH)) return `${spec.name}.${key} is nested too deeply`;
3410
+ }
3411
+ return null;
3412
+ }
3413
+ function isStoredSecret(value) {
3414
+ return typeof value === "string" && value.length > 0;
3415
+ }
3416
+ function clonedEntries(source) {
3417
+ const out = {};
3418
+ for (const [key, value] of Object.entries(source)) if (value !== void 0) out[key] = cloneValue(value);
3419
+ return out;
3420
+ }
3421
+ function isCopiedIntoPick(key, spec) {
3422
+ const values = spec.values ?? NO_VALUES;
3423
+ if (!Object.hasOwn(values, key)) return spec.passthrough === true;
3424
+ return values[key].control !== "password";
3425
+ }
3426
+ function pickSettingsBlock(stored, spec, tooDeepPaths = []) {
3427
+ if (!isRecord(stored)) return null;
3428
+ const tooDeepKeys = Object.keys(stored).filter((key) => isCopiedIntoPick(key, spec) && isNestedDeeperThan(stored[key], MAX_SETTINGS_VALUE_DEPTH));
3429
+ tooDeepPaths.push(...tooDeepKeys.map((key) => `${spec.name}.${key}`));
3430
+ const pickable = Object.fromEntries(Object.entries(stored).filter(([key]) => !tooDeepKeys.includes(key)));
3431
+ const out = spec.passthrough ? clonedEntries(pickable) : {};
3432
+ for (const key of spec.booleans) if (typeof pickable[key] === "boolean") out[key] = pickable[key];
3433
+ for (const [key, value] of Object.entries(spec.values ?? NO_VALUES)) {
3434
+ if (value.control === "password") {
3435
+ delete out[key];
3436
+ out[`${key}${SECRET_PRESENCE_SUFFIX}`] = isStoredSecret(pickable[key]);
3437
+ continue;
3438
+ }
3439
+ if (pickable[key] !== void 0) out[key] = cloneValue(pickable[key]);
3440
+ }
3441
+ for (const [key, nested] of Object.entries(spec.blocks)) {
3442
+ const picked = pickSettingsBlock(pickable[key], nested, tooDeepPaths);
3443
+ if (picked) out[key] = picked;
3444
+ }
3445
+ return out;
3446
+ }
3447
+ function mergeSettingsBlock(stored, incoming, spec) {
3448
+ const out = isRecord(stored) ? { ...stored } : {};
3449
+ if (!isRecord(incoming)) return out;
3450
+ if (spec.passthrough) Object.assign(out, clonedEntries(incoming));
3451
+ for (const key of spec.booleans) if (incoming[key] != null) out[key] = !!incoming[key];
3452
+ for (const [key, value] of Object.entries(spec.values ?? NO_VALUES)) {
3453
+ if (incoming[key] === void 0) continue;
3454
+ if (incoming[key] === null) {
3455
+ if (value.nullable) out[key] = null;
3456
+ continue;
3457
+ }
3458
+ out[key] = cloneValue(incoming[key]);
3459
+ }
3460
+ for (const [key, nested] of Object.entries(spec.blocks)) {
3461
+ if (incoming[key] === void 0) continue;
3462
+ out[key] = incoming[key] === null ? null : mergeSettingsBlock(out[key], incoming[key], nested);
3463
+ }
3464
+ return out;
3465
+ }
3466
+ //#endregion
3467
+ //#region server/core/rtk-install-core.ts
3468
+ var RTK_VERSION = "0.45.0";
3469
+ var RTK_RELEASE_BASE_URL = "https://github.com/rtk-ai/rtk/releases/download/v0.45.0/";
3470
+ var RTK_ASSETS = {
3471
+ "linux-x64": {
3472
+ file: "rtk-x86_64-unknown-linux-musl.tar.gz",
3473
+ sha256: "c4c036fbf181fc55ef329786c8c17e0d427972b053b825944d968a6aafef1ba4"
3474
+ },
3475
+ "linux-arm64": {
3476
+ file: "rtk-aarch64-unknown-linux-gnu.tar.gz",
3477
+ sha256: "80a746dd305ef944ff50ef011ae4ce3878dd5ba88dfe35d859d05498191637c3"
3478
+ },
3479
+ "win32-x64": {
3480
+ file: "rtk-x86_64-pc-windows-msvc.zip",
3481
+ sha256: "34cea9009a8099acdaf85147b971d95f65efabfa63fb3aea7d3e2b73e6f517c3"
3482
+ },
3483
+ "darwin-x64": {
3484
+ file: "rtk-x86_64-apple-darwin.tar.gz",
3485
+ sha256: "9ea02f889d5a2779e4fb700df4587824303c5a57cda22e903e30058079fca0ef"
3486
+ },
3487
+ "darwin-arm64": {
3488
+ file: "rtk-aarch64-apple-darwin.tar.gz",
3489
+ sha256: "064151cfc2d50b24d810b06a0af2e41b9c945e83534e4c438c3d3eae607fc3f4"
3490
+ }
3491
+ };
3492
+ var INSTALL_FAILURE_COOLDOWN_MS = 6e5;
3493
+ function assetForPlatform(platform, arch) {
3494
+ const entry = RTK_ASSETS[`${platform}-${arch}`];
3495
+ if (!entry) return null;
3496
+ return {
3497
+ ...entry,
3498
+ version: RTK_VERSION,
3499
+ url: `${RTK_RELEASE_BASE_URL}${entry.file}`
3500
+ };
3501
+ }
3502
+ function decideRtkInstall({ rtkEnabled, resolvedPath, platform, arch, inFlight = false, lastFailureAt = null, nowMs = 0, cooldownMs = INSTALL_FAILURE_COOLDOWN_MS } = {}) {
3503
+ if (!rtkEnabled) return {
3504
+ action: "skip",
3505
+ reason: "rtk-disabled"
3506
+ };
3507
+ if (resolvedPath) return {
3508
+ action: "skip",
3509
+ reason: "already-resolved"
3510
+ };
3511
+ if (inFlight) return {
3512
+ action: "skip",
3513
+ reason: "install-in-flight"
3514
+ };
3515
+ const asset = assetForPlatform(String(platform), String(arch));
3516
+ if (!asset) return {
3517
+ action: "skip",
3518
+ reason: `unsupported-platform:${platform}-${arch}`
3519
+ };
3520
+ if (typeof lastFailureAt === "number" && nowMs - lastFailureAt < cooldownMs) return {
3521
+ action: "skip",
3522
+ reason: "failure-cooldown"
3523
+ };
3524
+ return {
3525
+ action: "install",
3526
+ reason: "missing-binary",
3527
+ asset
3528
+ };
3529
+ }
3530
+ function verifyDigest(expectedHex, actualHex) {
3531
+ if (typeof expectedHex !== "string" || typeof actualHex !== "string") return false;
3532
+ if (expectedHex.length !== actualHex.length) return false;
3533
+ return crypto.timingSafeEqual(Buffer.from(expectedHex, "utf8"), Buffer.from(actualHex, "utf8"));
3534
+ }
3535
+ function installTargetPath(glimmervoidHome, platform) {
3536
+ const binaryName = platform === "win32" ? "rtk.exe" : "rtk";
3537
+ return path.join(glimmervoidHome, "bin", binaryName);
3538
+ }
3539
+ function isRtkBinaryName(name, platform) {
3540
+ const wanted = platform === "win32" ? "rtk.exe" : "rtk";
3541
+ return name.toLowerCase() === wanted;
3542
+ }
3543
+ function findEscapingArchiveMember(listingText) {
3544
+ const members = String(listingText || "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
3545
+ for (const member of members) {
3546
+ if (member.split(/[\\/]+/).includes("..")) return member;
3547
+ if (member.startsWith("/") || member.startsWith("\\") || /^[A-Za-z]:/.test(member)) return member;
3548
+ }
3549
+ return null;
3550
+ }
3551
+ //#endregion
3552
+ //#region server/config-store.ts
3553
+ var DEFAULT_CONFIG = {
3554
+ port: 3e3,
3555
+ autoRecoverSeconds: 3,
3556
+ inputGraceSeconds: 5,
3557
+ promptDetectionMs: 1500,
3558
+ notifyDebounceMs: 3e3,
3559
+ phoneEscalationMs: 3e5,
3560
+ cursorBlink: false,
3561
+ debugMode: false,
3562
+ conversationView: false,
3563
+ streamerMode: false,
3564
+ detectBackgroundAgents: true,
3565
+ detectScheduledWakeups: true,
3566
+ replayBufferKB: 512,
3567
+ recordSignals: true,
3568
+ trace: { enabled: true },
3569
+ planReview: { enabled: true },
3570
+ taskTitle: { refiner: {
3571
+ enabled: true,
3572
+ model: "haiku",
3573
+ minIntervalSeconds: 60,
3574
+ timeoutSeconds: 60
3575
+ } },
3576
+ changeMap: { narrator: {
3577
+ enabled: false,
3578
+ engine: "claude",
3579
+ model: "",
3580
+ timeoutSeconds: 90
3581
+ } },
3582
+ factory: {
3583
+ enabled: false,
3584
+ maxRisk: "medium",
3585
+ maxLiveWorkers: 2,
3586
+ checks: DEFAULT_FACTORY_CHECKS,
3587
+ protectedPaths: DEFAULT_FACTORY_PROTECTED_PATHS,
3588
+ reviewerModel: null,
3589
+ dailyBudgetUsd: null,
3590
+ verifierModel: null,
3591
+ mirrorHarnessRuns: true
3592
+ },
3593
+ guardrails: {
3594
+ unguardedRepos: [],
3595
+ blockedPaths: [],
3596
+ unreadablePaths: []
3597
+ },
3598
+ telemetry: { enabled: true },
3599
+ checkForUpdates: true,
3600
+ updateChannel: "release",
3601
+ autoResume: true,
3602
+ skipPermissionsByDefault: false,
3603
+ rtk: false,
3604
+ saneYolo: true,
3605
+ telegramNotifications: false,
3606
+ integrationBranch: null,
3607
+ worktreeRoot: "",
3608
+ worktreeShare: [
3609
+ "node_modules",
3610
+ ".env",
3611
+ ".env.local",
3612
+ ".claude"
3613
+ ],
3614
+ worktreeAutoRebase: true,
3615
+ worktreeSyncOnStart: true,
3616
+ worktreeRerere: true,
3617
+ branchGc: {
3618
+ enabled: true,
3619
+ worktrees: true,
3620
+ prefixes: [...DEFAULT_BRANCH_GC_PREFIXES],
3621
+ dryRun: false,
3622
+ staleDays: 14,
3623
+ deleteUnmerged: false,
3624
+ intervalMs: 216e5
3625
+ },
3626
+ repoRoots: [],
3627
+ customAgents: [],
3628
+ postTurnChecks: {
3629
+ enabled: true,
3630
+ mode: "report",
3631
+ rules: {
3632
+ trailingWs: true,
3633
+ finalNewline: true,
3634
+ bom: true,
3635
+ slop: false
3636
+ }
3637
+ },
3638
+ remote: {
3639
+ enabled: false,
3640
+ port: null,
3641
+ publicHost: "",
3642
+ allowedOrigins: []
3643
+ },
3644
+ workflows: {
3645
+ enabled: true,
3646
+ maxConcurrentSessions: 2,
3647
+ maxActionsPerPoll: 20,
3648
+ rules: []
3649
+ },
3650
+ projects: []
3651
+ };
3652
+ var DEFAULT_CONFIG_BY_KEY = DEFAULT_CONFIG;
3653
+ var ABORT_CONFIG_SAVE = "abort-config-save";
3654
+ var CONFIG_DIR_MODE = 448;
3655
+ function glimmervoidHomeDir() {
3656
+ return glimmervoidHomeDir$1(os.homedir(), process.env);
3657
+ }
3658
+ function restrictMode(target, mode) {
3659
+ try {
3660
+ fs.chmodSync(target, mode);
3661
+ } catch {}
3662
+ }
3663
+ function resolveConfigPath() {
3664
+ const homeDir = glimmervoidHomeDir();
3665
+ const decided = decideConfigPath({
3666
+ env: process.env,
3667
+ homeDir
3668
+ }, (candidate) => fs.existsSync(candidate));
3669
+ if (decided.path) return decided.path;
3670
+ if (decided.source === "env") {
3671
+ console.error(`Config file not found: ${decided.envPath}`);
3672
+ process.exit(1);
3673
+ }
3674
+ const homeConfig = decided.homePath;
3675
+ fs.mkdirSync(homeDir, {
3676
+ recursive: true,
3677
+ mode: CONFIG_DIR_MODE
3678
+ });
3679
+ restrictMode(homeDir, CONFIG_DIR_MODE);
3680
+ writeJsonAtomicSync(homeConfig, DEFAULT_CONFIG, {
3681
+ mode: 384,
3682
+ enforceMode: true
3683
+ });
3684
+ console.log(`Created default config at ${homeConfig}`);
3685
+ return homeConfig;
3686
+ }
3687
+ function generateProjectId() {
3688
+ return crypto.randomUUID();
3689
+ }
3690
+ function validateConfig(candidate) {
3691
+ if (!isRecord(candidate)) return {
3692
+ ok: false,
3693
+ errors: ["config must be a plain object"]
3694
+ };
3695
+ const parsedConfig = Config.safeParse(candidate);
3696
+ if (!parsedConfig.success) return {
3697
+ ok: false,
3698
+ errors: parsedConfig.error.issues.map((issue) => {
3699
+ if (issue.code === "custom") return issue.message;
3700
+ const [root, index, field] = issue.path;
3701
+ if (root === "port") return "port must be an integer from 0 to 65535";
3702
+ if (root === "replayBufferKB") return issue.message;
3703
+ if (root === "repoRoots" || root === "worktreeShare") return `${root} must be an array of strings`;
3704
+ if (root === "remote") return "remote must be a plain object";
3705
+ if (root !== "projects" && typeof DEFAULT_CONFIG_BY_KEY[String(root)] === "number") return `${String(root)} must be a finite number greater than or equal to 0`;
3706
+ if (root !== "projects") return issue.message;
3707
+ if (typeof index !== "number") return "projects must be an array";
3708
+ if (!field) return `projects[${index}] must be a plain object`;
3709
+ if (field === "agent") return `projects[${index}].agent must be ${AGENT_ID_SHAPE_MESSAGE}`;
3710
+ if (field === "codexBypassHookTrust") return `projects[${index}].codexBypassHookTrust must be a boolean`;
3711
+ if (field === "conversationView") return `projects[${index}].conversationView must be a boolean`;
3712
+ return `projects[${index}].${String(field)} must be a string`;
3713
+ })
3714
+ };
3715
+ return {
3716
+ ok: true,
3717
+ config: parsedConfig.data
3718
+ };
3719
+ }
3720
+ function normalizeConfigFile(candidate) {
3721
+ if (!isRecord(candidate)) throw new Error("config must be a plain object");
3722
+ const draft = normalizeLegacyConfigNumbers(candidate);
3723
+ for (const [key, fallback] of Object.entries(DEFAULT_CONFIG_BY_KEY)) {
3724
+ if (fallback === null || typeof fallback === "object") continue;
3725
+ if (!Object.hasOwn(draft, key)) continue;
3726
+ const fieldSchema = Config.shape[key];
3727
+ if (!fieldSchema || fieldSchema.safeParse(draft[key]).success) continue;
3728
+ console.warn(`[config] ${key} value ${JSON.stringify(draft[key])} is invalid; using ${JSON.stringify(fallback)}`);
3729
+ draft[key] = fallback;
3730
+ }
3731
+ const validation = validateConfig(draft);
3732
+ if (!validation.ok) throw new Error(`validation failed: ${validation.errors.join("; ")}`);
3733
+ return validation.config;
3734
+ }
3735
+ function writeBackupContent(backupPath, content) {
3736
+ try {
3737
+ writeTextAtomicSync(backupPath, content, {
3738
+ mode: 384,
3739
+ enforceMode: true
3740
+ });
3741
+ } catch (err) {
3742
+ console.warn(`[config] Failed to write backup ${backupPath}:`, errorLabel(err));
3743
+ }
3744
+ }
3745
+ function loadConfigFile(configPath, { exitOnError = true, envSecrets = readEnvSecrets(process.env) } = {}) {
3746
+ const loadedContent = fs.readFileSync(configPath, "utf8");
3747
+ try {
3748
+ return {
3749
+ config: withEnvSecrets(normalizeConfigFile(JSON.parse(loadedContent)), envSecrets),
3750
+ loadedContent
3751
+ };
3752
+ } catch (err) {
3753
+ const invalidBackupPath = `${configPath}.invalid.bak`;
3754
+ try {
3755
+ writeTextAtomicSync(invalidBackupPath, loadedContent, {
3756
+ mode: 384,
3757
+ enforceMode: true
3758
+ });
3759
+ } catch (backupErr) {
3760
+ console.warn(`[config] Failed to save invalid config copy ${invalidBackupPath}:`, errorLabel(backupErr));
3761
+ }
3762
+ const message = `[config] Could not load ${configPath}: ${errorMessage(err)}. The broken file was copied to ${invalidBackupPath} when possible. Restore from ${configPath}.boot.bak or ${configPath}.bak, then restart Glimmervoid.`;
3763
+ if (!exitOnError) return {
3764
+ error: err,
3765
+ message,
3766
+ invalidBackupPath
3767
+ };
3768
+ console.error(message);
3769
+ process.exit(1);
3770
+ }
3771
+ }
3772
+ function writeMutatedConfigFile(configPath, mutatorFn) {
3773
+ const loaded = loadConfigFile(configPath, {
3774
+ exitOnError: false,
3775
+ envSecrets: []
3776
+ });
3777
+ if ("error" in loaded) return {
3778
+ ok: false,
3779
+ message: loaded.message
3780
+ };
3781
+ mutatorFn(loaded.config);
3782
+ const validation = validateConfig(loaded.config);
3783
+ if (!validation.ok) return {
3784
+ ok: false,
3785
+ message: `config.json at ${configPath} was not written; validation failed: ${validation.errors.join("; ")}`
3786
+ };
3787
+ const nextContent = JSON.stringify(validation.config, null, 2);
3788
+ if (nextContent !== loaded.loadedContent) writeBackupContent(`${configPath}.bak`, loaded.loadedContent);
3789
+ writeTextAtomicSync(configPath, nextContent, { mode: 384 });
3790
+ return {
3791
+ ok: true,
3792
+ config: validation.config
3793
+ };
3794
+ }
3795
+ function saveConfigFileOnce(configPath, mutatorFn) {
3796
+ try {
3797
+ return writeMutatedConfigFile(configPath, mutatorFn);
3798
+ } catch (error) {
3799
+ return {
3800
+ ok: false,
3801
+ message: `config.json at ${configPath} was not written: ${errorMessage(error)}`
3802
+ };
3803
+ }
3804
+ }
3805
+ function topLevelKeyCount(candidate) {
3806
+ if (!isRecord(candidate)) return 0;
3807
+ return Object.keys(candidate).length;
3808
+ }
3809
+ function isSuspectedExternalWipe(candidate, currentConfig) {
3810
+ const currentKeyCount = topLevelKeyCount(currentConfig);
3811
+ if (currentKeyCount === 0) return false;
3812
+ return topLevelKeyCount({
3813
+ ...candidate,
3814
+ branchGc: resolveBranchGc(candidate.branchGc)
3815
+ }) * 2 < currentKeyCount;
3816
+ }
3817
+ function warnInvalidConfig(action, validation) {
3818
+ console.warn(`[config] Refusing to ${action}; validation failed: ${validation.errors.join("; ")}. Recovery sources: config.json.bak and config.json.boot.bak.`);
3819
+ }
3820
+ function warnSuspectedWipe(action) {
3821
+ console.warn(`[config] Refusing to ${action}; config.json has fewer than half the top-level keys of the in-memory config. This looks like an external wipe. Recovery sources: config.json.bak and config.json.boot.bak.`);
3822
+ }
3823
+ function ensureProjectIds(projects) {
3824
+ let changed = false;
3825
+ for (const p of projects) if (!p.id) {
3826
+ p.id = generateProjectId();
3827
+ changed = true;
3828
+ }
3829
+ return changed;
3830
+ }
3831
+ function collectBranchGcIssues(branchGc) {
3832
+ if (!isRecord(branchGc)) return {
3833
+ block: { ...DEFAULT_CONFIG.branchGc },
3834
+ issues: []
3835
+ };
3836
+ const accepted = {};
3837
+ const issues = [];
3838
+ for (const [field, value] of Object.entries(branchGc)) {
3839
+ if (!Object.hasOwn(BranchGcFileSettings.shape, field)) {
3840
+ issues.push(`[config] Ignoring unknown branchGc.${field}; it is not a branchGc setting, so the default block applies.`);
3841
+ continue;
3842
+ }
3843
+ const parsed = BranchGcFileSettings.safeParse({ [field]: value });
3844
+ if (!parsed.success) {
3845
+ issues.push(`[config] Ignoring invalid branchGc.${field}; using the default instead: ${configIssueMessage(parsed.error)}`);
3846
+ continue;
3847
+ }
3848
+ accepted[field] = value;
3849
+ }
3850
+ return {
3851
+ block: {
3852
+ ...DEFAULT_CONFIG.branchGc,
3853
+ ...accepted
3854
+ },
3855
+ issues
3856
+ };
3857
+ }
3858
+ function resolveBranchGc(branchGc) {
3859
+ return collectBranchGcIssues(branchGc).block;
3860
+ }
3861
+ function resolveBranchGcAndWarn(branchGc) {
3862
+ const resolved = collectBranchGcIssues(branchGc);
3863
+ for (const issue of resolved.issues) console.warn(issue);
3864
+ return resolved.block;
3865
+ }
3866
+ function secretSourcesOf(env) {
3867
+ return Object.fromEntries(readEnvSecrets(env).map((secret) => [`${secret.blockName}.${secret.secretKey}`, secret.environmentVariable]));
3868
+ }
3869
+ function retiredGuardrailKeysWarning(config) {
3870
+ const projects = Array.isArray(config.projects) ? config.projects : [];
3871
+ const retiredKeys = [...Object.hasOwn(config, "sandbox") ? ["sandbox (move it to guardrails, renaming unconfinedRepos to unguardedRepos)"] : [], ...projects.some((project) => isRecord(project) && Object.hasOwn(project, "unconfined")) ? ["projects[].unconfined (rename it to unguarded)"] : []];
3872
+ if (retiredKeys.length === 0) return null;
3873
+ return `[guardrails] config.json still has ${retiredKeys.join(" and ")}. These keys are ignored, so their unreadable paths and opt-outs do not apply until you move them.`;
3874
+ }
3875
+ function createConfigStore({ settingsDefaults } = {}) {
3876
+ const configPath = resolveConfigPath();
3877
+ const effectiveDefaults = {
3878
+ ...DEFAULT_CONFIG,
3879
+ ...settingsDefaults || {}
3880
+ };
3881
+ const launchDefaultKeys = new Set(Object.keys(settingsDefaults || {}));
3882
+ const envSecrets = readEnvSecrets(process.env);
3883
+ const loadedConfig = loadConfigFile(configPath);
3884
+ const config = loadedConfig.config;
3885
+ writeBackupContent(`${configPath}.boot.bak`, loadedConfig.loadedContent);
3886
+ config.repoRoots = config.repoRoots || [];
3887
+ config.branchGc = resolveBranchGcAndWarn(config.branchGc);
3888
+ const retiredGuardrailKeys = retiredGuardrailKeysWarning(config);
3889
+ if (retiredGuardrailKeys !== null) console.warn(retiredGuardrailKeys);
3890
+ if (Array.isArray(config.projects) && ensureProjectIds(config.projects)) try {
3891
+ writeJsonAtomicSync(configPath, withoutEnvSecrets(config, envSecrets), { mode: 384 });
3892
+ console.log("[config] Auto-assigned IDs to projects missing them");
3893
+ } catch (err) {
3894
+ console.warn("[config] Failed to persist auto-assigned project IDs:", errorMessage(err));
3895
+ }
3896
+ let _lastWrittenContent = null;
3897
+ let _lastAppliedContent = loadedConfig.loadedContent;
3898
+ let synchronousWriteRevision = 0;
3899
+ let lastSynchronousContent = null;
3900
+ let pendingSaves = Promise.resolve();
3901
+ function prepareSave(freshConfig, mutatorFn) {
3902
+ const freshValidation = validateConfig(freshConfig);
3903
+ if (!freshValidation.ok) {
3904
+ warnInvalidConfig("save config.json", freshValidation);
3905
+ return null;
3906
+ }
3907
+ if (isSuspectedExternalWipe(freshConfig, config)) {
3908
+ warnSuspectedWipe("save config.json");
3909
+ return null;
3910
+ }
3911
+ if (mutatorFn(freshConfig) === "abort-config-save") return null;
3912
+ const mutatedValidation = validateConfig(withEnvSecrets(freshConfig, envSecrets));
3913
+ if (!mutatedValidation.ok) {
3914
+ warnInvalidConfig("save config.json", mutatedValidation);
3915
+ return null;
3916
+ }
3917
+ return mutatedValidation.config;
3918
+ }
3919
+ function save(mutatorFn) {
3920
+ let loaded;
3921
+ try {
3922
+ loaded = loadConfigFile(configPath, { exitOnError: false });
3923
+ } catch (err) {
3924
+ console.warn("[config] Failed to read config.json for save:", errorLabel(err));
3925
+ return null;
3926
+ }
3927
+ if ("error" in loaded) {
3928
+ console.warn(loaded.message);
3929
+ return null;
3930
+ }
3931
+ const freshConfig = loaded.config;
3932
+ const freshContent = loaded.loadedContent;
3933
+ const effectiveConfig = prepareSave(freshConfig, mutatorFn);
3934
+ if (!effectiveConfig) return null;
3935
+ try {
3936
+ const nextContent = JSON.stringify(withoutEnvSecrets(effectiveConfig, envSecrets), null, 2);
3937
+ if (freshContent !== nextContent) writeBackupContent(`${configPath}.bak`, freshContent);
3938
+ _lastWrittenContent = nextContent;
3939
+ _lastAppliedContent = null;
3940
+ writeTextAtomicSync(configPath, nextContent, { mode: 384 });
3941
+ lastSynchronousContent = nextContent;
3942
+ synchronousWriteRevision++;
3943
+ } catch (err) {
3944
+ console.warn("[config] Failed to write config.json:", errorLabel(err));
3945
+ return null;
3946
+ }
3947
+ return effectiveConfig;
3948
+ }
3949
+ function saveAsync(mutatorFn) {
3950
+ const scheduledSave = pendingSaves.then(async () => {
3951
+ let concurrentContent = null;
3952
+ for (;;) {
3953
+ const revision = synchronousWriteRevision;
3954
+ try {
3955
+ const diskContent = await fs.promises.readFile(configPath, "utf8");
3956
+ if (revision !== synchronousWriteRevision) {
3957
+ concurrentContent = lastSynchronousContent;
3958
+ continue;
3959
+ }
3960
+ const freshContent = concurrentContent ?? diskContent;
3961
+ const effectiveConfig = prepareSave(withEnvSecrets(normalizeConfigFile(JSON.parse(freshContent)), envSecrets), mutatorFn);
3962
+ if (!effectiveConfig) return null;
3963
+ const nextContent = JSON.stringify(withoutEnvSecrets(effectiveConfig, envSecrets), null, 2);
3964
+ if (freshContent !== nextContent) try {
3965
+ await writeTextAtomic(`${configPath}.bak`, freshContent, {
3966
+ mode: 384,
3967
+ enforceMode: process.platform !== "win32"
3968
+ });
3969
+ } catch (error) {
3970
+ console.warn(`[config] Failed to write backup ${configPath}.bak:`, errorLabel(error));
3971
+ }
3972
+ if (revision !== synchronousWriteRevision) {
3973
+ concurrentContent = lastSynchronousContent;
3974
+ continue;
3975
+ }
3976
+ await writeTextAtomic(configPath, nextContent, { mode: 384 });
3977
+ if (revision !== synchronousWriteRevision) {
3978
+ concurrentContent = lastSynchronousContent;
3979
+ continue;
3980
+ }
3981
+ _lastWrittenContent = nextContent;
3982
+ _lastAppliedContent = null;
3983
+ return effectiveConfig;
3984
+ } catch (error) {
3985
+ console.warn("[config] Failed to save config.json asynchronously:", errorMessage(error));
3986
+ return null;
3987
+ }
3988
+ }
3989
+ });
3990
+ pendingSaves = scheduledSave;
3991
+ return scheduledSave;
3992
+ }
3993
+ const warnedTooDeepPaths = /* @__PURE__ */ new Set();
3994
+ function pickBlock(blockName, stored) {
3995
+ const tooDeepPaths = [];
3996
+ const picked = pickSettingsBlock(stored, settingsBlockSpecFor(blockName), tooDeepPaths);
3997
+ for (const tooDeepPath of tooDeepPaths) {
3998
+ if (warnedTooDeepPaths.has(tooDeepPath)) continue;
3999
+ warnedTooDeepPaths.add(tooDeepPath);
4000
+ console.warn(`[config] ${tooDeepPath} is nested too deeply to send to the dashboard, so Settings leaves it out; config.json keeps it as it is.`);
4001
+ }
4002
+ return picked;
4003
+ }
4004
+ function getSettings() {
4005
+ return {
4006
+ port: config.port,
4007
+ autoRecoverSeconds: config.autoRecoverSeconds,
4008
+ inputGraceSeconds: config.inputGraceSeconds,
4009
+ promptDetectionMs: config.promptDetectionMs,
4010
+ notifyDebounceMs: config.notifyDebounceMs,
4011
+ phoneEscalationMs: config.phoneEscalationMs ?? DEFAULT_CONFIG.phoneEscalationMs,
4012
+ replayBufferKB: config.replayBufferKB,
4013
+ cursorBlink: config.cursorBlink ?? effectiveDefaults.cursorBlink,
4014
+ debugMode: config.debugMode ?? effectiveDefaults.debugMode,
4015
+ conversationView: config.conversationView ?? effectiveDefaults.conversationView,
4016
+ streamerMode: config.streamerMode ?? effectiveDefaults.streamerMode,
4017
+ detectBackgroundAgents: config.detectBackgroundAgents ?? effectiveDefaults.detectBackgroundAgents,
4018
+ recordSignals: config.recordSignals ?? effectiveDefaults.recordSignals,
4019
+ trace: pickBlock("trace", {
4020
+ ...DEFAULT_CONFIG.trace,
4021
+ ...config.trace
4022
+ }),
4023
+ guardrails: pickBlock("guardrails", {
4024
+ ...DEFAULT_CONFIG.guardrails,
4025
+ ...config.guardrails
4026
+ }),
4027
+ telemetry: pickBlock("telemetry", {
4028
+ ...DEFAULT_CONFIG.telemetry,
4029
+ ...config.telemetry
4030
+ }),
4031
+ telemetryForcedOff: isTelemetryForcedOff(process.env),
4032
+ rtk: config.rtk ?? effectiveDefaults.rtk,
4033
+ rtkInstallSupported: assetForPlatform(process.platform, process.arch) !== null,
4034
+ saneYolo: config.saneYolo ?? effectiveDefaults.saneYolo,
4035
+ checkForUpdates: config.checkForUpdates ?? effectiveDefaults.checkForUpdates,
4036
+ updateChannel: config.updateChannel ?? effectiveDefaults.updateChannel,
4037
+ autoResume: config.autoResume ?? effectiveDefaults.autoResume,
4038
+ skipPermissionsByDefault: config.skipPermissionsByDefault ?? effectiveDefaults.skipPermissionsByDefault,
4039
+ telegramNotifications: config.telegramNotifications ?? effectiveDefaults.telegramNotifications,
4040
+ integrationBranch: config.integrationBranch === void 0 ? effectiveDefaults.integrationBranch : config.integrationBranch,
4041
+ worktreeRoot: config.worktreeRoot ?? effectiveDefaults.worktreeRoot,
4042
+ worktreeShare: config.worktreeShare ?? effectiveDefaults.worktreeShare,
4043
+ repoRoots: config.repoRoots,
4044
+ repositoryRootCaseInsensitive: equalsIgnoringCaseOnWindows("root", "ROOT"),
4045
+ taskTitle: pickBlock("taskTitle", config.taskTitle ?? DEFAULT_CONFIG.taskTitle),
4046
+ changeMap: pickBlock("changeMap", config.changeMap),
4047
+ branchGc: pickBlock("branchGc", config.branchGc),
4048
+ postTurnChecks: pickBlock("postTurnChecks", config.postTurnChecks ?? DEFAULT_CONFIG.postTurnChecks),
4049
+ visions: pickBlock("visions", config.visions),
4050
+ github: pickBlock("github", config.github),
4051
+ teamReview: pickBlock("teamReview", config.teamReview),
4052
+ benchmarks: pickBlock("benchmarks", config.benchmarks),
4053
+ factory: pickBlock("factory", config.factory),
4054
+ knowledgeGraph: pickBlock("knowledgeGraph", config.knowledgeGraph),
4055
+ workflows: config.workflows ?? null,
4056
+ posthog: pickBlock("posthog", config.posthog),
4057
+ secretSources: secretSourcesOf(process.env),
4058
+ usage: pickBlock("usage", config.usage),
4059
+ telegram: pickBlock("telegram", config.telegram),
4060
+ ingest: pickBlock("ingest", config.ingest),
4061
+ projectChoices: (config.projects || []).map((p) => ({
4062
+ id: p.id,
4063
+ name: p.name,
4064
+ path: p.path
4065
+ }))
4066
+ };
4067
+ }
4068
+ function isUnchosenLaunchDefault(target, key, value) {
4069
+ if (!launchDefaultKeys.has(key)) return false;
4070
+ if (key in target) return false;
4071
+ return value === effectiveDefaults[key];
4072
+ }
4073
+ function applySettings(newConfig) {
4074
+ for (const key of RUNTIME_CONFIG_SCALAR_KEYS) {
4075
+ if (key === "integrationBranch" && newConfig[key] !== void 0) {
4076
+ config[key] = newConfig[key] === null || String(newConfig[key]) === "" ? null : String(newConfig[key]);
4077
+ continue;
4078
+ }
4079
+ if (newConfig[key] == null) {
4080
+ delete config[key];
4081
+ continue;
4082
+ }
4083
+ let nextValue = newConfig[key];
4084
+ if (typeof DEFAULT_CONFIG_BY_KEY[key] === "boolean") nextValue = !!nextValue;
4085
+ if (typeof DEFAULT_CONFIG_BY_KEY[key] === "string") nextValue = String(nextValue);
4086
+ if (typeof nextValue === "boolean" && isUnchosenLaunchDefault(config, key, nextValue)) continue;
4087
+ config[key] = nextValue;
4088
+ }
4089
+ config.repoRoots = newConfig.repoRoots || [];
4090
+ if (newConfig.postTurnChecks == null) delete config.postTurnChecks;
4091
+ if (newConfig.postTurnChecks != null) config.postTurnChecks = newConfig.postTurnChecks;
4092
+ if (newConfig.worktreeShare == null) delete config.worktreeShare;
4093
+ if (newConfig.worktreeShare != null) config.worktreeShare = newConfig.worktreeShare;
4094
+ if (newConfig.taskTitle != null) config.taskTitle = newConfig.taskTitle;
4095
+ if (newConfig.changeMap != null) config.changeMap = newConfig.changeMap;
4096
+ if (newConfig.branchGc != null) config.branchGc = resolveBranchGc(newConfig.branchGc);
4097
+ if (newConfig.visions != null) config.visions = newConfig.visions;
4098
+ if (newConfig.github != null) config.github = newConfig.github;
4099
+ if (newConfig.teamReview != null) config.teamReview = newConfig.teamReview;
4100
+ if (newConfig.benchmarks != null) config.benchmarks = newConfig.benchmarks;
4101
+ if (newConfig.factory != null) config.factory = newConfig.factory;
4102
+ if (newConfig.knowledgeGraph != null) config.knowledgeGraph = newConfig.knowledgeGraph;
4103
+ if (newConfig.posthog != null) config.posthog = newConfig.posthog;
4104
+ if (newConfig.usage != null) config.usage = newConfig.usage;
4105
+ if (newConfig.telegram != null) config.telegram = newConfig.telegram;
4106
+ if (newConfig.ingest != null) config.ingest = newConfig.ingest;
4107
+ if (newConfig.guardrails != null) config.guardrails = newConfig.guardrails;
4108
+ if (newConfig.telemetry != null) config.telemetry = newConfig.telemetry;
4109
+ if (newConfig.workflows == null) delete config.workflows;
4110
+ if (newConfig.workflows != null) config.workflows = newConfig.workflows;
4111
+ if (newConfig.coder == null) delete config.coder;
4112
+ if (newConfig.coder != null) config.coder = newConfig.coder;
4113
+ config.customAgents = newConfig.customAgents ?? [];
4114
+ config.hooks = Array.isArray(newConfig.hooks) ? newConfig.hooks : [];
4115
+ if (newConfig.port != null && newConfig.port !== config.port) console.log(`[settings] Port changed to ${newConfig.port} - restart required to take effect`);
4116
+ }
4117
+ function watchForChanges(callback) {
4118
+ let reloadTimer = null;
4119
+ let watcher = null;
4120
+ function handleConfigChange(err, data) {
4121
+ if (err) {
4122
+ console.warn("[config] Failed to read config.json:", err.code);
4123
+ return;
4124
+ }
4125
+ if (_lastWrittenContent !== null && data === _lastWrittenContent) return;
4126
+ if (_lastAppliedContent !== null && data === _lastAppliedContent) return;
4127
+ let newConfig;
4128
+ try {
4129
+ newConfig = withEnvSecrets(normalizeConfigFile(JSON.parse(data)), envSecrets);
4130
+ } catch (parseErr) {
4131
+ console.warn("[config] Invalid config.json:", errorMessage(parseErr));
4132
+ return;
4133
+ }
4134
+ newConfig.branchGc = resolveBranchGcAndWarn(newConfig.branchGc);
4135
+ if (isSuspectedExternalWipe(newConfig, config)) {
4136
+ warnSuspectedWipe("reload config.json");
4137
+ return;
4138
+ }
4139
+ _lastAppliedContent = data;
4140
+ _lastWrittenContent = null;
4141
+ callback(newConfig);
4142
+ console.log("[config] Reloaded config.json");
4143
+ }
4144
+ try {
4145
+ const canonicalConfigPath = canonicalizePath(configPath);
4146
+ const watchDir = path.dirname(canonicalConfigPath);
4147
+ const targetName = path.basename(canonicalConfigPath);
4148
+ watcher = fs.watch(watchDir, (_event, filename) => {
4149
+ if (filename != null && !equalsIgnoringCaseOnWindows(path.basename(String(filename)), targetName)) return;
4150
+ if (reloadTimer) clearTimeout(reloadTimer);
4151
+ reloadTimer = setTimeout(() => {
4152
+ fs.readFile(configPath, "utf8", handleConfigChange);
4153
+ }, 500);
4154
+ });
4155
+ console.log("[config] Watching config.json for changes");
4156
+ } catch (watchErr) {
4157
+ console.warn("[config] Failed to watch config.json:", errorMessage(watchErr));
4158
+ }
4159
+ return function stop() {
4160
+ if (reloadTimer) clearTimeout(reloadTimer);
4161
+ if (watcher) try {
4162
+ watcher.close();
4163
+ } catch {}
4164
+ watcher = null;
4165
+ };
4166
+ }
4167
+ return {
4168
+ config,
4169
+ configPath,
4170
+ save,
4171
+ saveAsync,
4172
+ idle: () => pendingSaves,
4173
+ getSettings,
4174
+ applySettings,
4175
+ isUnchosenLaunchDefault,
4176
+ watchForChanges,
4177
+ DEFAULT_CONFIG
4178
+ };
4179
+ }
4180
+ //#endregion
4181
+ export { withEnvSecrets as $, settingsOf as A, buildExceptionProperties as B, deleteValueAtPath as C, scoreSettingsSearch as D, parsePosthogProjectSelection as E, TELEMETRY_BATCH_URL as F, decideTelemetryConsent as G, classifySessionExit as H, TELEMETRY_FLAGS_URL as I, parsePosthogReleaseId as J, exceptionFingerprint as K, adapterBucket as L, FILE_ONLY_GROUPS as M, SETTINGS_REGISTRY as N, setValueAtPath as O, FIRST_RUN_NOTICE as P, stableConfigKey as Q, buildAiGenerationEvents as R, cloneValue as S, hasScaledUnit as T, decideIsDevInstall as U, buildFactoryDecisionEvent as V, decideRemoteTelemetryState as W, ENV_SECRET_BINDINGS as X, resolveProjectToken as Y, readEnvSecrets as Z, SECRET_PRESENCE_SUFFIX as _, generateProjectId as a, usablePrefixes as at, validateSettingsBlock as b, resolveConfigPath as c, comparableDirectoryPath as ct, assetForPlatform as d, DEFAULT_BRANCH_GC_PREFIXES as et, decideRtkInstall as f, verifyDigest as g, isRtkBinaryName as h, ensureProjectIds as i, sessionIdFromBranch as it, valueAtPath as j, settingsGroupOf as k, saveConfigFileOnce as l, isSameDirectoryPath as lt, installTargetPath as m, DEFAULT_CONFIG as n, planBranchGc as nt, glimmervoidHomeDir as o, worktreeIntegrationTips as ot, findEscapingArchiveMember as p, nodeMajorVersion as q, createConfigStore as r, planWorktreeGc as rt, loadConfigFile as s, canonicalizePath as st, ABORT_CONFIG_SAVE as t, matchedPrefix as tt, validateConfig as u, mergeSettingsBlock as v, displayText as w, SETTINGS_INDEX as x, settingsBlockSpecFor as y, buildBrowserExceptionProperties as z };