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.
- package/README.md +2 -0
- package/dist/bin/glimmervoid.js +17 -12
- package/dist/chunks/{adapters-B37wqNti.js → adapters-BVozg-Lg.js} +1 -1
- package/dist/chunks/{adapters-CADejSpq.js → adapters-BdnLyJjf.js} +10 -49
- package/dist/chunks/{agent-api-cli-CRj_FYbz.js → agent-api-cli-DIqRLvu3.js} +1 -1
- package/dist/chunks/{agent-api-cli-vGwstJ1j.js → agent-api-cli-d-Bgsshx.js} +3 -3
- package/dist/chunks/{agent-api-core-D1233u1U.js → agent-api-core-BUC9WND7.js} +2 -2
- package/dist/chunks/{agent-setup-cli-BTAFcckd.js → agent-setup-cli-CH7EHsAX.js} +1 -1
- package/dist/chunks/{config-rPbbL4nW.js → config-CuZ16F9W.js} +47 -771
- package/dist/chunks/config-store-LqQ7MuyV.js +4181 -0
- package/dist/chunks/config-store-tVmt_nfm.js +1 -0
- package/dist/chunks/{contracts-DjIgIuDk.js → contracts-BLZAOP57.js} +55 -6
- package/dist/chunks/{grok-KKgdSe-7.js → grok-Ch4-35_r.js} +3 -3
- package/dist/chunks/{hook-command-core-DclEoE9e.js → hook-command-core-qTNK-xMF.js} +4 -1
- package/dist/chunks/{hook-tools-UX2fmt81.js → hook-tools-BkxllUHE.js} +9 -16
- package/dist/chunks/{knowledge-graph-cli-Rj5nj87L.js → knowledge-graph-cli-DD7QvcFd.js} +1 -1
- package/dist/chunks/{main-C-T2Uk20.js → main-Rmo83Ow-.js} +3075 -832
- package/dist/chunks/{pair-cli-Dcyzv3In.js → pair-cli-DzFeAmq5.js} +2 -2
- package/dist/chunks/{remote-config-Dd1T-VJA.js → remote-config-CPdSF2Xi.js} +1 -1
- package/dist/chunks/{rtk-hook-core-BmEz2aq9.js → rtk-hook-core-VrUafGAp.js} +15 -5
- package/dist/chunks/rtk-resolver-B8BPgN9L.js +1 -0
- package/dist/chunks/{rtk-resolver-B9pzjLsM.js → rtk-resolver-C_micGrL.js} +1 -1
- package/dist/chunks/session-B8RhqQJe.js +986 -0
- package/dist/chunks/{session-routes-BYy3am9m.js → session-routes-DB5aDKsx.js} +1 -1
- package/dist/chunks/settings-cli-C2TY6hl-.js +484 -0
- package/dist/chunks/{ssh-sign-cli-DPHpT22B.js → ssh-sign-cli-C2nUwJC-.js} +2 -2
- package/dist/client/assets/index-BIaSyXwH.css +1 -0
- package/dist/client/assets/index-Ddu7M4Ad.js +259 -0
- package/dist/client/index.html +5 -3
- package/dist/server/index.js +1 -1
- package/dist/session/hook-tool-relay.js +3 -3
- package/package.json +3 -3
- package/dist/chunks/config-store-BeIxAZNn.js +0 -1
- package/dist/chunks/config-store-BtsYbNC3.js +0 -1713
- package/dist/chunks/rtk-resolver-SWU5VVM7.js +0 -1
- package/dist/chunks/session-D6xXN0UU.js +0 -232
- package/dist/client/assets/index-CUxRHbCS.js +0 -254
- 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 };
|