night-orch 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -108
  3. package/dist/cli/commands/monitoring.d.ts.map +1 -1
  4. package/dist/cli/commands/monitoring.js +7 -3
  5. package/dist/cli/commands/monitoring.js.map +1 -1
  6. package/dist/cli/commands/settings.d.ts.map +1 -1
  7. package/dist/cli/commands/settings.js +40 -4
  8. package/dist/cli/commands/settings.js.map +1 -1
  9. package/dist/cli/index.js +1 -1
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/cli/tui/app.d.ts.map +1 -1
  12. package/dist/cli/tui/app.js +52 -3
  13. package/dist/cli/tui/app.js.map +1 -1
  14. package/dist/cli/tui/settings-view.d.ts.map +1 -1
  15. package/dist/cli/tui/settings-view.js +22 -5
  16. package/dist/cli/tui/settings-view.js.map +1 -1
  17. package/dist/config/loader.d.ts.map +1 -1
  18. package/dist/config/loader.js +141 -13
  19. package/dist/config/loader.js.map +1 -1
  20. package/dist/config/schema.d.ts +266 -1
  21. package/dist/config/schema.d.ts.map +1 -1
  22. package/dist/config/schema.js +38 -5
  23. package/dist/config/schema.js.map +1 -1
  24. package/dist/mcp/tools/index.js +23 -1
  25. package/dist/mcp/tools/index.js.map +1 -1
  26. package/dist/settings/registry.d.ts +23 -7
  27. package/dist/settings/registry.d.ts.map +1 -1
  28. package/dist/settings/registry.js +802 -172
  29. package/dist/settings/registry.js.map +1 -1
  30. package/dist/settings/runtime.d.ts +5 -1
  31. package/dist/settings/runtime.d.ts.map +1 -1
  32. package/dist/settings/runtime.js +46 -9
  33. package/dist/settings/runtime.js.map +1 -1
  34. package/dist/web/server.d.ts.map +1 -1
  35. package/dist/web/server.js +244 -9
  36. package/dist/web/server.js.map +1 -1
  37. package/dist/web/shell-session.d.ts +74 -0
  38. package/dist/web/shell-session.d.ts.map +1 -0
  39. package/dist/web/shell-session.js +278 -0
  40. package/dist/web/shell-session.js.map +1 -0
  41. package/package.json +8 -2
  42. package/web/dist/assets/index-BIrXUwFe.css +1 -0
  43. package/web/dist/assets/index-BzM2M-8S.js +26 -0
  44. package/web/dist/index.html +2 -2
  45. package/web/dist/assets/index-k6kgdnzy.js +0 -9
  46. package/web/dist/assets/index-xm9qPlYB.css +0 -1
@@ -1,207 +1,671 @@
1
- const SETTING_DEFINITIONS = {
2
- 'github.pollIntervalSeconds': {
3
- key: 'github.pollIntervalSeconds',
4
- label: 'Poll Interval (seconds)',
5
- description: 'Delay between automatic poll cycles.',
6
- details: 'Controls how often night-orch checks configured repos for work. Lower values react faster but increase API traffic; higher values reduce background load.',
7
- type: 'number',
8
- defaultValue: 300,
9
- yamlPath: ['github', 'pollIntervalSeconds'],
10
- min: 5,
11
- max: 3600,
12
- step: 5,
13
- read: (config) => config.github.pollIntervalSeconds,
14
- apply: (config, value) => ({
15
- ...config,
16
- github: {
17
- ...config.github,
18
- pollIntervalSeconds: value,
1
+ import { z } from 'zod';
2
+ import { AppMentionSchema, CostPricingModelSchema, NotificationChannelSchema, WorkerProfileSchema, WorkflowSchema, } from '../config/schema.js';
3
+ const SETTING_DEFINITIONS = buildSettingDefinitions();
4
+ const SETTING_KEYS = Object.keys(SETTING_DEFINITIONS);
5
+ export function listSettingDefinitions() {
6
+ return SETTING_KEYS.map((key) => SETTING_DEFINITIONS[key]);
7
+ }
8
+ export function getSettingDefinition(key) {
9
+ return SETTING_DEFINITIONS[key] ?? null;
10
+ }
11
+ export function sanitizeSettingValueForDisplay(definition, value) {
12
+ if (value === null) {
13
+ return null;
14
+ }
15
+ const sanitize = definition.sanitizeForDisplay;
16
+ return sanitize(value);
17
+ }
18
+ export function resolveSettingYamlValue(definition, rawConfig, baseConfig) {
19
+ if (!hasValueAtPath(rawConfig, definition.yamlPath)) {
20
+ return {
21
+ hasYamlValue: false,
22
+ yamlValue: null,
23
+ };
24
+ }
25
+ return {
26
+ hasYamlValue: true,
27
+ yamlValue: definition.read(baseConfig),
28
+ };
29
+ }
30
+ const GithubAppMentionsOverrideSchema = z.record(AppMentionSchema);
31
+ const NotificationsChannelsOverrideSchema = z.array(NotificationChannelSchema);
32
+ const CostPricingModelsOverrideSchema = z.record(CostPricingModelSchema);
33
+ const WorkerProfilesOverrideSchema = z.record(WorkerProfileSchema);
34
+ const WorkflowsOverrideSchema = z.record(WorkflowSchema);
35
+ function buildSettingDefinitions() {
36
+ const definitions = [
37
+ stringSetting({
38
+ key: 'github.tokenEnv',
39
+ label: 'GitHub Token Env Var',
40
+ description: 'Environment variable name used for GitHub authentication.',
41
+ details: 'Set the env var name that stores the GitHub token. This must be a variable name, not a literal token value.',
42
+ defaultValue: null,
43
+ yamlPath: ['github', 'tokenEnv'],
44
+ minLength: 1,
45
+ validate: (value) => {
46
+ if (value.startsWith('ghp_') || value.startsWith('ghs_') || value.startsWith('github_pat_')) {
47
+ return 'github.tokenEnv should be an environment variable name, not a literal token';
48
+ }
49
+ return null;
19
50
  },
20
51
  }),
21
- parseInput: (raw) => parseNumberInput(raw, {
52
+ stringSetting({
53
+ key: 'github.apiBaseUrl',
54
+ label: 'GitHub API Base URL',
55
+ description: 'Base URL for GitHub API requests.',
56
+ details: 'Override the API base URL used for GitHub repos. Useful for GitHub Enterprise deployments.',
57
+ defaultValue: 'https://api.github.com',
58
+ yamlPath: ['github', 'apiBaseUrl'],
59
+ minLength: 1,
60
+ url: true,
61
+ }),
62
+ numberSetting({
22
63
  key: 'github.pollIntervalSeconds',
64
+ label: 'Poll Interval (seconds)',
65
+ description: 'Delay between automatic poll cycles.',
66
+ details: 'Controls how often night-orch checks configured repos for work. Lower values react faster but increase API traffic.',
67
+ defaultValue: 300,
68
+ yamlPath: ['github', 'pollIntervalSeconds'],
23
69
  integer: true,
24
70
  min: 5,
25
71
  max: 3600,
72
+ step: 5,
26
73
  }),
27
- parseStored: (raw) => parseStoredNumber(raw, {
28
- key: 'github.pollIntervalSeconds',
74
+ jsonSetting({
75
+ key: 'github.appMentions',
76
+ label: 'GitHub App Mentions',
77
+ description: 'Mention template map used for app-trigger comments.',
78
+ details: 'Record keyed by mention alias with enabled flag and comment template payloads.',
79
+ defaultValue: {},
80
+ yamlPath: ['github', 'appMentions'],
81
+ normalize: (value) => validateJsonSettingShape(value, GithubAppMentionsOverrideSchema, 'github.appMentions'),
82
+ }),
83
+ stringSetting({
84
+ key: 'storage.dbPath',
85
+ label: 'State DB Path',
86
+ description: 'SQLite database file path.',
87
+ details: 'Location of the runtime state database file. Read-only at runtime because DB opens before overrides are loaded.',
88
+ defaultValue: '~/.config/night-orch/state.db',
89
+ yamlPath: ['storage', 'dbPath'],
90
+ minLength: 1,
91
+ mutable: false,
92
+ }),
93
+ stringSetting({
94
+ key: 'storage.worktreeRoot',
95
+ label: 'Worktree Root',
96
+ description: 'Root directory used for issue worktrees.',
97
+ details: 'Base path where dedicated issue worktrees are created.',
98
+ defaultValue: '~/code/.night-orch/worktrees',
99
+ yamlPath: ['storage', 'worktreeRoot'],
100
+ minLength: 1,
101
+ }),
102
+ stringSetting({
103
+ key: 'storage.logsRoot',
104
+ label: 'Logs Root',
105
+ description: 'Root directory used for run/session logs.',
106
+ details: 'Base path where runtime log files are written.',
107
+ defaultValue: '~/code/.night-orch/logs',
108
+ yamlPath: ['storage', 'logsRoot'],
109
+ minLength: 1,
110
+ }),
111
+ booleanSetting({
112
+ key: 'storage.autoCleanup.enabled',
113
+ label: 'Auto Cleanup Enabled',
114
+ description: 'Enable periodic cleanup jobs.',
115
+ details: 'When enabled, stale worktrees and old logs are cleaned up automatically on schedule.',
116
+ defaultValue: true,
117
+ yamlPath: ['storage', 'autoCleanup', 'enabled'],
118
+ }),
119
+ numberSetting({
120
+ key: 'storage.autoCleanup.intervalMinutes',
121
+ label: 'Auto Cleanup Interval (minutes)',
122
+ description: 'How often automatic cleanup runs.',
123
+ details: 'Interval between automatic cleanup runs.',
124
+ defaultValue: 60,
125
+ yamlPath: ['storage', 'autoCleanup', 'intervalMinutes'],
29
126
  integer: true,
30
- min: 5,
31
- max: 3600,
127
+ min: 1,
128
+ step: 5,
32
129
  }),
33
- serialize: (value) => JSON.stringify(value),
34
- },
35
- 'security.maxDailyCostUsd': {
36
- key: 'security.maxDailyCostUsd',
37
- label: 'Daily Cost Budget (USD)',
38
- description: 'Maximum allowed spend per UTC day before new runs are blocked.',
39
- details: 'Sets the global daily spend cap for pay-per-use mode. When the cap is reached, new runs are blocked until the next UTC day or until you raise the limit.',
40
- type: 'number',
41
- defaultValue: 50,
42
- yamlPath: ['security', 'maxDailyCostUsd'],
43
- min: 1,
44
- max: 10000,
45
- step: 1,
46
- read: (config) => config.security.maxDailyCostUsd,
47
- apply: (config, value) => ({
48
- ...config,
49
- security: {
50
- ...config.security,
51
- maxDailyCostUsd: value,
52
- },
130
+ numberSetting({
131
+ key: 'storage.retention.worktreeAgeDays',
132
+ label: 'Worktree Retention (days)',
133
+ description: 'Retention window for stale worktrees.',
134
+ details: 'Completed/error worktrees older than this threshold may be removed during cleanup.',
135
+ defaultValue: 7,
136
+ yamlPath: ['storage', 'retention', 'worktreeAgeDays'],
137
+ integer: true,
138
+ min: 1,
139
+ step: 1,
53
140
  }),
54
- parseInput: (raw) => parseNumberInput(raw, {
55
- key: 'security.maxDailyCostUsd',
56
- integer: false,
141
+ numberSetting({
142
+ key: 'storage.retention.detailDays',
143
+ label: 'Detail Retention (days)',
144
+ description: 'Retention window for detailed run data.',
145
+ details: 'Detailed run artifacts are retained for this many days before archival/cleanup.',
146
+ defaultValue: 30,
147
+ yamlPath: ['storage', 'retention', 'detailDays'],
148
+ integer: true,
57
149
  min: 1,
58
- max: 10000,
150
+ step: 1,
59
151
  }),
60
- parseStored: (raw) => parseStoredNumber(raw, {
61
- key: 'security.maxDailyCostUsd',
62
- integer: false,
152
+ numberSetting({
153
+ key: 'storage.retention.archiveDays',
154
+ label: 'Archive Retention (days)',
155
+ description: 'Retention window for archived run data.',
156
+ details: 'Archived records older than this threshold can be removed.',
157
+ defaultValue: 90,
158
+ yamlPath: ['storage', 'retention', 'archiveDays'],
159
+ integer: true,
63
160
  min: 1,
64
- max: 10000,
161
+ step: 1,
65
162
  }),
66
- serialize: (value) => JSON.stringify(value),
67
- },
68
- 'security.maxCostPerRunUsd': {
69
- key: 'security.maxCostPerRunUsd',
70
- label: 'Per-Run Cost Budget (USD)',
71
- description: 'Maximum allowed spend for a single run before it is blocked.',
72
- details: 'Sets the per-issue run cost ceiling. Useful for preventing one expensive issue from consuming most of the daily budget.',
73
- type: 'number',
74
- defaultValue: 10,
75
- yamlPath: ['security', 'maxCostPerRunUsd'],
76
- min: 0.1,
77
- max: 1000,
78
- step: 0.5,
79
- read: (config) => config.security.maxCostPerRunUsd,
80
- apply: (config, value) => ({
81
- ...config,
82
- security: {
83
- ...config.security,
84
- maxCostPerRunUsd: value,
85
- },
163
+ jsonSetting({
164
+ key: 'notifications.channels',
165
+ label: 'Notification Channels',
166
+ description: 'Configured notification channel list.',
167
+ details: 'Array of notification channel definitions (console/webhook/discord/smtp).',
168
+ defaultValue: [{ type: 'console' }],
169
+ yamlPath: ['notifications', 'channels'],
170
+ normalize: (value) => validateJsonSettingShape(value, NotificationsChannelsOverrideSchema, 'notifications.channels'),
86
171
  }),
87
- parseInput: (raw) => parseNumberInput(raw, {
88
- key: 'security.maxCostPerRunUsd',
172
+ booleanSetting({
173
+ key: 'notifications.events.onRunStarted',
174
+ label: 'Notify: Run Started',
175
+ description: 'Send notifications when runs start.',
176
+ details: 'Controls whether run-started notifications are dispatched.',
177
+ defaultValue: false,
178
+ yamlPath: ['notifications', 'events', 'onRunStarted'],
179
+ }),
180
+ booleanSetting({
181
+ key: 'notifications.events.onBlocked',
182
+ label: 'Notify: Blocked',
183
+ description: 'Send notifications when runs become blocked.',
184
+ details: 'Controls whether blocked-run notifications are dispatched.',
185
+ defaultValue: true,
186
+ yamlPath: ['notifications', 'events', 'onBlocked'],
187
+ }),
188
+ booleanSetting({
189
+ key: 'notifications.events.onPrReady',
190
+ label: 'Notify: PR Ready',
191
+ description: 'Send notifications when PRs are ready.',
192
+ details: 'Controls whether ready-for-review PR notifications are dispatched.',
193
+ defaultValue: true,
194
+ yamlPath: ['notifications', 'events', 'onPrReady'],
195
+ }),
196
+ booleanSetting({
197
+ key: 'notifications.events.onPrUpdated',
198
+ label: 'Notify: PR Updated',
199
+ description: 'Send notifications when PRs are updated.',
200
+ details: 'Controls whether PR update notifications are dispatched.',
201
+ defaultValue: true,
202
+ yamlPath: ['notifications', 'events', 'onPrUpdated'],
203
+ }),
204
+ booleanSetting({
205
+ key: 'notifications.events.onError',
206
+ label: 'Notify: Error',
207
+ description: 'Send notifications on orchestration errors.',
208
+ details: 'Controls whether error notifications are dispatched.',
209
+ defaultValue: true,
210
+ yamlPath: ['notifications', 'events', 'onError'],
211
+ }),
212
+ booleanSetting({
213
+ key: 'notifications.events.onRetryExhausted',
214
+ label: 'Notify: Retry Exhausted',
215
+ description: 'Send notifications when retries are exhausted.',
216
+ details: 'Controls whether retry-exhausted notifications are dispatched.',
217
+ defaultValue: true,
218
+ yamlPath: ['notifications', 'events', 'onRetryExhausted'],
219
+ }),
220
+ numberSetting({
221
+ key: 'loop.maxReviewIterations',
222
+ label: 'Max Review Iterations',
223
+ description: 'Maximum review correction loops per run.',
224
+ details: 'Limits how many review-fix-review cycles a run can execute before stopping.',
225
+ defaultValue: 4,
226
+ yamlPath: ['loop', 'maxReviewIterations'],
227
+ integer: true,
228
+ min: 1,
229
+ step: 1,
230
+ }),
231
+ numberSetting({
232
+ key: 'loop.maxTotalAgentPasses',
233
+ label: 'Max Total Agent Passes',
234
+ description: 'Hard cap on planner/coder/reviewer passes in one run.',
235
+ details: 'Caps total planner/coder/reviewer passes across the full run.',
236
+ defaultValue: 10,
237
+ yamlPath: ['loop', 'maxTotalAgentPasses'],
238
+ integer: true,
239
+ min: 1,
240
+ step: 1,
241
+ }),
242
+ booleanSetting({
243
+ key: 'loop.stopOnPlannerFailure',
244
+ label: 'Stop On Planner Failure',
245
+ description: 'Stop run when planner output fails validation.',
246
+ details: 'If enabled, planner failure ends the run early instead of proceeding.',
247
+ defaultValue: true,
248
+ yamlPath: ['loop', 'stopOnPlannerFailure'],
249
+ }),
250
+ booleanSetting({
251
+ key: 'loop.requireVerificationPass',
252
+ label: 'Require Verification Pass',
253
+ description: 'Require verify commands to pass before completion.',
254
+ details: 'If enabled, failed verification blocks completion.',
255
+ defaultValue: true,
256
+ yamlPath: ['loop', 'requireVerificationPass'],
257
+ }),
258
+ stringSetting({
259
+ key: 'loop.reviewApprovalKeyword',
260
+ label: 'Review Approval Keyword',
261
+ description: 'Reviewer keyword interpreted as approval.',
262
+ details: 'Expected keyword emitted by reviewer agent for approval.',
263
+ defaultValue: 'APPROVED',
264
+ yamlPath: ['loop', 'reviewApprovalKeyword'],
265
+ minLength: 1,
266
+ }),
267
+ stringSetting({
268
+ key: 'loop.reviewNeedsChangesKeyword',
269
+ label: 'Review Needs-Changes Keyword',
270
+ description: 'Reviewer keyword interpreted as changes required.',
271
+ details: 'Expected keyword emitted by reviewer agent for change requests.',
272
+ defaultValue: 'CHANGES_REQUIRED',
273
+ yamlPath: ['loop', 'reviewNeedsChangesKeyword'],
274
+ minLength: 1,
275
+ }),
276
+ booleanSetting({
277
+ key: 'loop.blockOnAmbiguousReview',
278
+ label: 'Block On Ambiguous Review',
279
+ description: 'Block run if review output is ambiguous.',
280
+ details: 'If enabled, unparseable reviewer output results in blocked status.',
281
+ defaultValue: true,
282
+ yamlPath: ['loop', 'blockOnAmbiguousReview'],
283
+ }),
284
+ numberSetting({
285
+ key: 'loop.maxAutoRetries',
286
+ label: 'Max Auto Retries',
287
+ description: 'Automatic retry attempts for infrastructure failures.',
288
+ details: 'Maximum number of automatic retries after transient failures.',
289
+ defaultValue: 3,
290
+ yamlPath: ['loop', 'maxAutoRetries'],
291
+ integer: true,
292
+ min: 0,
293
+ step: 1,
294
+ }),
295
+ booleanSetting({
296
+ key: 'loop.decompose',
297
+ label: 'Enable Decomposition',
298
+ description: 'Allow issue decomposition into sub-tasks.',
299
+ details: 'If enabled, eligible issues can be split into dependent sub-tasks.',
300
+ defaultValue: false,
301
+ yamlPath: ['loop', 'decompose'],
302
+ }),
303
+ numberSetting({
304
+ key: 'loop.maxSubtasks',
305
+ label: 'Max Subtasks',
306
+ description: 'Maximum sub-tasks per decomposition.',
307
+ details: 'Upper bound for generated decomposition sub-tasks.',
308
+ defaultValue: 5,
309
+ yamlPath: ['loop', 'maxSubtasks'],
310
+ integer: true,
311
+ min: 1,
312
+ max: 10,
313
+ step: 1,
314
+ }),
315
+ numberSetting({
316
+ key: 'loop.maxConcurrentSubtasks',
317
+ label: 'Max Concurrent Subtasks',
318
+ description: 'Maximum parallel sub-task executions.',
319
+ details: 'Upper bound for concurrently running sub-task worktrees.',
320
+ defaultValue: 3,
321
+ yamlPath: ['loop', 'maxConcurrentSubtasks'],
322
+ integer: true,
323
+ min: 1,
324
+ max: 10,
325
+ step: 1,
326
+ }),
327
+ numberSetting({
328
+ key: 'security.maxChangedFiles',
329
+ label: 'Max Changed Files',
330
+ description: 'Diff guard threshold for changed files.',
331
+ details: 'Runs are blocked when changed-file count exceeds this threshold.',
332
+ defaultValue: 50,
333
+ yamlPath: ['security', 'maxChangedFiles'],
334
+ integer: true,
335
+ min: 1,
336
+ step: 1,
337
+ }),
338
+ numberSetting({
339
+ key: 'security.maxChangedLines',
340
+ label: 'Max Changed Lines',
341
+ description: 'Diff guard threshold for changed lines.',
342
+ details: 'Runs are blocked when changed-line count exceeds this threshold.',
343
+ defaultValue: 5000,
344
+ yamlPath: ['security', 'maxChangedLines'],
345
+ integer: true,
346
+ min: 1,
347
+ step: 50,
348
+ }),
349
+ numberSetting({
350
+ key: 'security.maxDailyCostUsd',
351
+ label: 'Daily Cost Budget (USD)',
352
+ description: 'Maximum allowed spend per UTC day before new runs are blocked.',
353
+ details: 'Sets the global daily spend cap for pay-per-use mode.',
354
+ defaultValue: 50,
355
+ yamlPath: ['security', 'maxDailyCostUsd'],
89
356
  integer: false,
90
- min: 0.1,
91
- max: 1000,
357
+ min: 0.01,
358
+ step: 1,
92
359
  }),
93
- parseStored: (raw) => parseStoredNumber(raw, {
360
+ numberSetting({
94
361
  key: 'security.maxCostPerRunUsd',
362
+ label: 'Per-Run Cost Budget (USD)',
363
+ description: 'Maximum allowed spend for a single run before it is blocked.',
364
+ details: 'Sets the per-issue run cost ceiling for pay-per-use mode.',
365
+ defaultValue: 10,
366
+ yamlPath: ['security', 'maxCostPerRunUsd'],
95
367
  integer: false,
96
- min: 0.1,
97
- max: 1000,
368
+ min: 0.01,
369
+ step: 0.5,
98
370
  }),
99
- serialize: (value) => JSON.stringify(value),
100
- },
101
- 'loop.maxReviewIterations': {
102
- key: 'loop.maxReviewIterations',
103
- label: 'Max Review Iterations',
104
- description: 'Maximum review correction loops per run.',
105
- details: 'Limits how many review-fix-review cycles a run can execute before stopping. Higher values can improve completion rate, but may increase runtime and cost.',
106
- type: 'number',
107
- defaultValue: 4,
108
- yamlPath: ['loop', 'maxReviewIterations'],
109
- min: 1,
110
- max: 20,
111
- step: 1,
112
- read: (config) => config.loop.maxReviewIterations,
113
- apply: (config, value) => ({
114
- ...config,
115
- loop: {
116
- ...config.loop,
117
- maxReviewIterations: value,
118
- },
371
+ stringSetting({
372
+ key: 'cost.model',
373
+ label: 'Cost Model',
374
+ description: 'Cost enforcement model.',
375
+ details: 'pay-per-use enforces spend caps; subscription treats USD as advisory.',
376
+ defaultValue: 'pay-per-use',
377
+ yamlPath: ['cost', 'model'],
378
+ options: ['pay-per-use', 'subscription'],
119
379
  }),
120
- parseInput: (raw) => parseNumberInput(raw, {
121
- key: 'loop.maxReviewIterations',
380
+ stringSetting({
381
+ key: 'cost.pricing.defaultModel',
382
+ label: 'Default Pricing Model',
383
+ description: 'Fallback model key for pricing lookups.',
384
+ details: 'Used when a worker profile does not specify a pricing model.',
385
+ defaultValue: 'default',
386
+ yamlPath: ['cost', 'pricing', 'defaultModel'],
387
+ minLength: 1,
388
+ }),
389
+ jsonSetting({
390
+ key: 'cost.pricing.models',
391
+ label: 'Pricing Models Map',
392
+ description: 'Model-specific pricing table.',
393
+ details: 'Record keyed by model name with token/minute pricing values.',
394
+ defaultValue: {},
395
+ yamlPath: ['cost', 'pricing', 'models'],
396
+ normalize: (value) => validateJsonSettingShape(value, CostPricingModelsOverrideSchema, 'cost.pricing.models'),
397
+ }),
398
+ jsonSetting({
399
+ key: 'workerProfiles',
400
+ label: 'Worker Profiles',
401
+ description: 'Global worker profile definitions.',
402
+ details: 'Record of worker profile definitions keyed by profile name.',
403
+ defaultValue: {},
404
+ yamlPath: ['workerProfiles'],
405
+ sensitive: true,
406
+ normalize: (value) => validateJsonSettingShape(value, WorkerProfilesOverrideSchema, 'workerProfiles'),
407
+ sanitizeForDisplay: (value) => redactWorkerProfiles(value),
408
+ }),
409
+ booleanSetting({
410
+ key: 'metrics.enabled',
411
+ label: 'Metrics Enabled',
412
+ description: 'Enable Prometheus metrics export.',
413
+ details: 'Turns `/metrics` export on or off.',
414
+ defaultValue: true,
415
+ yamlPath: ['metrics', 'enabled'],
416
+ }),
417
+ numberSetting({
418
+ key: 'metrics.port',
419
+ label: 'Metrics Port',
420
+ description: 'TCP port for Prometheus metrics endpoint.',
421
+ details: 'Port used when metrics exporter is enabled.',
422
+ defaultValue: 9090,
423
+ yamlPath: ['metrics', 'port'],
122
424
  integer: true,
123
425
  min: 1,
124
- max: 20,
426
+ max: 65535,
427
+ step: 1,
125
428
  }),
126
- parseStored: (raw) => parseStoredNumber(raw, {
127
- key: 'loop.maxReviewIterations',
429
+ stringSetting({
430
+ key: 'metrics.host',
431
+ label: 'Metrics Host',
432
+ description: 'Bind address for Prometheus metrics endpoint.',
433
+ details: 'Network interface/address used by metrics exporter.',
434
+ defaultValue: '0.0.0.0',
435
+ yamlPath: ['metrics', 'host'],
436
+ minLength: 1,
437
+ }),
438
+ booleanSetting({
439
+ key: 'observability.agentStreaming',
440
+ label: 'Agent Streaming',
441
+ description: 'Enable in-flight agent event streaming to TUI/Web.',
442
+ details: 'Turns live agent event streaming on or off for terminal and web views.',
443
+ defaultValue: true,
444
+ yamlPath: ['observability', 'agentStreaming'],
445
+ }),
446
+ numberSetting({
447
+ key: 'observability.eventRetention',
448
+ label: 'Event Retention',
449
+ description: 'Maximum in-memory event backlog per run/session.',
450
+ details: 'Controls event retention window for stream history.',
451
+ defaultValue: 1000,
452
+ yamlPath: ['observability', 'eventRetention'],
128
453
  integer: true,
129
- min: 1,
130
- max: 20,
454
+ min: 100,
455
+ max: 10000,
456
+ step: 100,
131
457
  }),
132
- serialize: (value) => JSON.stringify(value),
133
- },
134
- 'loop.maxTotalAgentPasses': {
135
- key: 'loop.maxTotalAgentPasses',
136
- label: 'Max Total Agent Passes',
137
- description: 'Hard cap on planner/coder/reviewer passes in one run.',
138
- details: 'Caps total planner/coder/reviewer passes across the full run. This is a safety guard against long loops and runaway spend.',
139
- type: 'number',
140
- defaultValue: 10,
141
- yamlPath: ['loop', 'maxTotalAgentPasses'],
142
- min: 1,
143
- max: 50,
144
- step: 1,
145
- read: (config) => config.loop.maxTotalAgentPasses,
146
- apply: (config, value) => ({
147
- ...config,
148
- loop: {
149
- ...config.loop,
150
- maxTotalAgentPasses: value,
151
- },
458
+ booleanSetting({
459
+ key: 'observability.sessionLogs',
460
+ label: 'Session Logs Enabled',
461
+ description: 'Persist interactive agent session logs to disk.',
462
+ details: 'If enabled, interactive session logs are written and retained.',
463
+ defaultValue: true,
464
+ yamlPath: ['observability', 'sessionLogs'],
152
465
  }),
153
- parseInput: (raw) => parseNumberInput(raw, {
154
- key: 'loop.maxTotalAgentPasses',
466
+ numberSetting({
467
+ key: 'observability.sessionLogRetention',
468
+ label: 'Session Log Retention (days)',
469
+ description: 'Retention period for interactive session logs.',
470
+ details: 'Session logs older than this many days may be deleted by cleanup.',
471
+ defaultValue: 7,
472
+ yamlPath: ['observability', 'sessionLogRetention'],
155
473
  integer: true,
156
474
  min: 1,
157
- max: 50,
475
+ step: 1,
158
476
  }),
159
- parseStored: (raw) => parseStoredNumber(raw, {
160
- key: 'loop.maxTotalAgentPasses',
477
+ booleanSetting({
478
+ key: 'mcp.enabled',
479
+ label: 'MCP Enabled',
480
+ description: 'Enable MCP server exposure.',
481
+ details: 'Turns MCP server availability on or off for applicable commands/modes.',
482
+ defaultValue: false,
483
+ yamlPath: ['mcp', 'enabled'],
484
+ }),
485
+ stringSetting({
486
+ key: 'mcp.transport',
487
+ label: 'MCP Transport',
488
+ description: 'Transport protocol used by MCP server.',
489
+ details: 'Currently only stdio transport is supported.',
490
+ defaultValue: 'stdio',
491
+ yamlPath: ['mcp', 'transport'],
492
+ options: ['stdio'],
493
+ }),
494
+ stringSetting({
495
+ key: 'mcp.authTokenEnv',
496
+ label: 'MCP Auth Token Env Var',
497
+ description: 'Optional env var name containing MCP mutation auth token.',
498
+ details: 'Set to null (or clear in YAML) to disable MCP mutation token checks.',
499
+ defaultValue: null,
500
+ yamlPath: ['mcp', 'authTokenEnv'],
501
+ allowNull: true,
502
+ minLength: 1,
503
+ }),
504
+ numberSetting({
505
+ key: 'mcp.httpPort',
506
+ label: 'MCP HTTP Port',
507
+ description: 'TCP port for embedded MCP HTTP transport.',
508
+ details: 'Port used when running embedded MCP HTTP/SSE mode.',
509
+ defaultValue: 3100,
510
+ yamlPath: ['mcp', 'httpPort'],
161
511
  integer: true,
162
512
  min: 1,
163
- max: 50,
513
+ max: 65535,
514
+ step: 1,
515
+ }),
516
+ stringSetting({
517
+ key: 'mcp.httpHost',
518
+ label: 'MCP HTTP Host',
519
+ description: 'Bind address for embedded MCP HTTP transport.',
520
+ details: 'Network interface/address used for MCP HTTP endpoint binding.',
521
+ defaultValue: '127.0.0.1',
522
+ yamlPath: ['mcp', 'httpHost'],
523
+ minLength: 1,
524
+ }),
525
+ booleanSetting({
526
+ key: 'commentCommands.enabled',
527
+ label: 'Comment Commands Enabled',
528
+ description: 'Enable issue comment command processing.',
529
+ details: 'If enabled, `/orch` comment commands are parsed and processed.',
530
+ defaultValue: true,
531
+ yamlPath: ['commentCommands', 'enabled'],
532
+ }),
533
+ booleanSetting({
534
+ key: 'commentCommands.requireCollaborator',
535
+ label: 'Require Collaborator For Commands',
536
+ description: 'Require collaborator permission for comment commands.',
537
+ details: 'When enabled, only collaborators can run issue comment commands.',
538
+ defaultValue: true,
539
+ yamlPath: ['commentCommands', 'requireCollaborator'],
540
+ }),
541
+ jsonSetting({
542
+ key: 'workflows',
543
+ label: 'Workflow Definitions',
544
+ description: 'Named workflow definitions.',
545
+ details: 'Record of named workflow graphs (steps/roles/agents) used by workflow selection.',
546
+ defaultValue: {},
547
+ yamlPath: ['workflows'],
548
+ normalize: (value) => validateJsonSettingShape(value, WorkflowsOverrideSchema, 'workflows'),
549
+ }),
550
+ ];
551
+ const entries = new Map();
552
+ for (const definition of definitions) {
553
+ if (entries.has(definition.key)) {
554
+ throw new Error(`Duplicate runtime setting key: ${definition.key}`);
555
+ }
556
+ entries.set(definition.key, definition);
557
+ }
558
+ return Object.fromEntries(entries);
559
+ }
560
+ function numberSetting(options) {
561
+ return {
562
+ key: options.key,
563
+ label: options.label,
564
+ description: options.description,
565
+ details: options.details,
566
+ type: 'number',
567
+ mutable: options.mutable ?? true,
568
+ sensitive: options.sensitive ?? false,
569
+ defaultValue: options.defaultValue,
570
+ yamlPath: options.yamlPath,
571
+ ...(options.min !== undefined ? { min: options.min } : {}),
572
+ ...(options.max !== undefined ? { max: options.max } : {}),
573
+ ...(options.step !== undefined ? { step: options.step } : {}),
574
+ read: (config) => readNumberValue(config, options.yamlPath, options.defaultValue),
575
+ apply: (config, value) => setConfigValue(config, options.yamlPath, value),
576
+ parseInput: (raw) => parseNumberInput(raw, {
577
+ key: options.key,
578
+ integer: options.integer,
579
+ min: options.min,
580
+ max: options.max,
581
+ }),
582
+ parseStored: (raw) => parseStoredNumber(raw, {
583
+ key: options.key,
584
+ integer: options.integer,
585
+ min: options.min,
586
+ max: options.max,
164
587
  }),
165
588
  serialize: (value) => JSON.stringify(value),
166
- },
167
- 'observability.agentStreaming': {
168
- key: 'observability.agentStreaming',
169
- label: 'Agent Streaming',
170
- description: 'Enable in-flight agent event streaming to TUI/Web.',
171
- details: 'Turns live agent event streaming on or off for terminal and web views. Disable this if you want quieter live output while runs are active.',
589
+ sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
590
+ };
591
+ }
592
+ function booleanSetting(options) {
593
+ return {
594
+ key: options.key,
595
+ label: options.label,
596
+ description: options.description,
597
+ details: options.details,
172
598
  type: 'boolean',
173
- defaultValue: true,
174
- yamlPath: ['observability', 'agentStreaming'],
175
- read: (config) => config.observability?.agentStreaming ?? true,
176
- apply: (config, value) => ({
177
- ...config,
178
- observability: {
179
- ...buildDefaultObservability(config),
180
- agentStreaming: value,
181
- },
182
- }),
183
- parseInput: (raw) => parseBooleanInput(raw, 'observability.agentStreaming'),
184
- parseStored: (raw) => parseStoredBoolean(raw, 'observability.agentStreaming'),
599
+ mutable: options.mutable ?? true,
600
+ sensitive: options.sensitive ?? false,
601
+ defaultValue: options.defaultValue,
602
+ yamlPath: options.yamlPath,
603
+ read: (config) => readBooleanValue(config, options.yamlPath, options.defaultValue),
604
+ apply: (config, value) => setConfigValue(config, options.yamlPath, value),
605
+ parseInput: (raw) => parseBooleanInput(raw, options.key),
606
+ parseStored: (raw) => parseStoredBoolean(raw, options.key),
185
607
  serialize: (value) => JSON.stringify(value),
186
- },
187
- };
188
- const SETTING_KEYS = Object.keys(SETTING_DEFINITIONS);
189
- export function listSettingDefinitions() {
190
- return SETTING_KEYS.map((key) => SETTING_DEFINITIONS[key]);
608
+ sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
609
+ };
191
610
  }
192
- export function getSettingDefinition(key) {
193
- return SETTING_DEFINITIONS[key] ?? null;
611
+ function stringSetting(options) {
612
+ return {
613
+ key: options.key,
614
+ label: options.label,
615
+ description: options.description,
616
+ details: options.details,
617
+ type: 'string',
618
+ mutable: options.mutable ?? true,
619
+ sensitive: options.sensitive ?? false,
620
+ defaultValue: options.defaultValue,
621
+ yamlPath: options.yamlPath,
622
+ ...(options.options ? { options: options.options } : {}),
623
+ ...(options.allowNull ? { allowNull: true } : {}),
624
+ read: (config) => readStringValue(config, options.yamlPath, options.defaultValue, options.allowNull ?? false),
625
+ apply: (config, value) => setConfigValue(config, options.yamlPath, value),
626
+ parseInput: (raw) => parseStringInput(raw, {
627
+ key: options.key,
628
+ allowNull: options.allowNull,
629
+ options: options.options,
630
+ minLength: options.minLength,
631
+ url: options.url,
632
+ validate: options.validate,
633
+ }),
634
+ parseStored: (raw) => parseStoredString(raw, {
635
+ key: options.key,
636
+ allowNull: options.allowNull,
637
+ options: options.options,
638
+ minLength: options.minLength,
639
+ url: options.url,
640
+ validate: options.validate,
641
+ }),
642
+ serialize: (value) => JSON.stringify(value),
643
+ sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
644
+ };
194
645
  }
195
- export function resolveSettingYamlValue(definition, rawConfig, baseConfig) {
196
- if (!hasValueAtPath(rawConfig, definition.yamlPath)) {
197
- return {
198
- hasYamlValue: false,
199
- yamlValue: null,
200
- };
201
- }
646
+ function jsonSetting(options) {
202
647
  return {
203
- hasYamlValue: true,
204
- yamlValue: definition.read(baseConfig),
648
+ key: options.key,
649
+ label: options.label,
650
+ description: options.description,
651
+ details: options.details,
652
+ type: 'json',
653
+ mutable: options.mutable ?? true,
654
+ sensitive: options.sensitive ?? false,
655
+ defaultValue: options.defaultValue,
656
+ yamlPath: options.yamlPath,
657
+ read: (config) => readJsonValue(config, options.yamlPath, options.defaultValue),
658
+ apply: (config, value) => setConfigValue(config, options.yamlPath, value),
659
+ parseInput: (raw) => {
660
+ const parsed = parseJsonInput(raw, options.key);
661
+ return options.normalize ? options.normalize(parsed) : parsed;
662
+ },
663
+ parseStored: (raw) => {
664
+ const parsed = parseStoredJson(raw, options.key);
665
+ return options.normalize ? options.normalize(parsed) : parsed;
666
+ },
667
+ serialize: (value) => JSON.stringify(value),
668
+ sanitizeForDisplay: options.sanitizeForDisplay ?? identitySanitize,
205
669
  };
206
670
  }
207
671
  function parseNumberInput(raw, options) {
@@ -231,8 +695,11 @@ function validateNumber(value, options) {
231
695
  if (options.integer && !Number.isInteger(value)) {
232
696
  throw new Error(`${options.key} must be an integer`);
233
697
  }
234
- if (value < options.min || value > options.max) {
235
- throw new Error(`${options.key} must be between ${options.min} and ${options.max}`);
698
+ if (options.min !== undefined && value < options.min) {
699
+ throw new Error(`${options.key} must be >= ${options.min}`);
700
+ }
701
+ if (options.max !== undefined && value > options.max) {
702
+ throw new Error(`${options.key} must be <= ${options.max}`);
236
703
  }
237
704
  return value;
238
705
  }
@@ -275,12 +742,160 @@ function parseStoredBoolean(raw, key) {
275
742
  throw new Error(`Invalid stored value for ${key}: ${err.message}`);
276
743
  }
277
744
  }
278
- function buildDefaultObservability(config) {
745
+ function parseStringInput(raw, options) {
746
+ if (raw === null) {
747
+ if (options.allowNull) {
748
+ return null;
749
+ }
750
+ throw new Error(`${options.key} must be a string`);
751
+ }
752
+ if (typeof raw !== 'string') {
753
+ throw new Error(`${options.key} must be a string`);
754
+ }
755
+ if (options.allowNull && raw.trim().toLowerCase() === 'null') {
756
+ return null;
757
+ }
758
+ const value = raw;
759
+ if (options.minLength !== undefined && value.trim().length < options.minLength) {
760
+ throw new Error(`${options.key} must be at least ${options.minLength} character(s)`);
761
+ }
762
+ if (options.options && !options.options.includes(value)) {
763
+ throw new Error(`${options.key} must be one of: ${options.options.join(', ')}`);
764
+ }
765
+ if (options.url) {
766
+ try {
767
+ void new URL(value);
768
+ }
769
+ catch {
770
+ throw new Error(`${options.key} must be a valid URL`);
771
+ }
772
+ }
773
+ const validationError = options.validate?.(value);
774
+ if (validationError) {
775
+ throw new Error(validationError);
776
+ }
777
+ return value;
778
+ }
779
+ function parseStoredString(raw, options) {
780
+ try {
781
+ const parsed = JSON.parse(raw);
782
+ if (typeof parsed !== 'string' && parsed !== null) {
783
+ throw new Error(`Stored value for ${options.key} is not string/null`);
784
+ }
785
+ return parseStringInput(parsed, options);
786
+ }
787
+ catch (err) {
788
+ throw new Error(`Invalid stored value for ${options.key}: ${err.message}`);
789
+ }
790
+ }
791
+ function parseJsonInput(raw, key) {
792
+ if (typeof raw === 'string') {
793
+ try {
794
+ const parsed = JSON.parse(raw);
795
+ if (!isJsonValue(parsed)) {
796
+ throw new Error('must be valid JSON');
797
+ }
798
+ return parsed;
799
+ }
800
+ catch (err) {
801
+ throw new Error(`${key} must be valid JSON: ${err.message}`);
802
+ }
803
+ }
804
+ if (!isJsonValue(raw)) {
805
+ throw new Error(`${key} must be valid JSON`);
806
+ }
807
+ return raw;
808
+ }
809
+ function parseStoredJson(raw, key) {
810
+ try {
811
+ const parsed = JSON.parse(raw);
812
+ if (!isJsonValue(parsed)) {
813
+ throw new Error(`Stored value for ${key} is not valid JSON`);
814
+ }
815
+ return parsed;
816
+ }
817
+ catch (err) {
818
+ throw new Error(`Invalid stored value for ${key}: ${err.message}`);
819
+ }
820
+ }
821
+ function validateJsonSettingShape(value, schema, key) {
822
+ const result = schema.safeParse(value);
823
+ if (result.success) {
824
+ return result.data;
825
+ }
826
+ const issue = result.error.issues[0];
827
+ const path = issue?.path.length ? issue.path.join('.') : key;
828
+ const message = issue?.message ?? 'invalid structure';
829
+ throw new Error(`${key} has invalid structure (${path}): ${message}`);
830
+ }
831
+ function identitySanitize(value) {
832
+ return value;
833
+ }
834
+ function redactWorkerProfiles(value) {
835
+ if (!isRecord(value)) {
836
+ return value;
837
+ }
838
+ return Object.fromEntries(Object.entries(value).map(([profileName, profile]) => {
839
+ if (!isRecord(profile) || !isRecord(profile['env'])) {
840
+ return [profileName, profile];
841
+ }
842
+ const redactedEnv = Object.fromEntries(Object.keys(profile['env']).map((envKey) => [envKey, '[redacted]']));
843
+ return [profileName, { ...profile, env: redactedEnv }];
844
+ }));
845
+ }
846
+ function readNumberValue(config, path, fallback) {
847
+ const value = readPathValue(config, path);
848
+ return typeof value === 'number' ? value : fallback;
849
+ }
850
+ function readBooleanValue(config, path, fallback) {
851
+ const value = readPathValue(config, path);
852
+ return typeof value === 'boolean' ? value : fallback;
853
+ }
854
+ function readStringValue(config, path, fallback, allowNull) {
855
+ const value = readPathValue(config, path);
856
+ if (typeof value === 'string') {
857
+ return value;
858
+ }
859
+ if (allowNull && value === null) {
860
+ return null;
861
+ }
862
+ return fallback;
863
+ }
864
+ function readJsonValue(config, path, fallback) {
865
+ const value = readPathValue(config, path);
866
+ return isJsonValue(value) ? value : fallback;
867
+ }
868
+ function setConfigValue(config, path, value) {
869
+ return setPathValue(config, path, value);
870
+ }
871
+ function readPathValue(source, path) {
872
+ let current = source;
873
+ for (const segment of path) {
874
+ if (!isRecord(current)) {
875
+ return undefined;
876
+ }
877
+ current = current[segment];
878
+ }
879
+ return current;
880
+ }
881
+ function setPathValue(source, path, value) {
882
+ if (path.length === 0) {
883
+ return isRecord(source) ? { ...source } : {};
884
+ }
885
+ const [segment, ...rest] = path;
886
+ if (segment === undefined) {
887
+ return isRecord(source) ? { ...source } : {};
888
+ }
889
+ const current = isRecord(source) ? source : {};
890
+ if (rest.length === 0) {
891
+ return {
892
+ ...current,
893
+ [segment]: value,
894
+ };
895
+ }
279
896
  return {
280
- agentStreaming: config.observability?.agentStreaming ?? true,
281
- eventRetention: config.observability?.eventRetention ?? 1000,
282
- sessionLogs: config.observability?.sessionLogs ?? true,
283
- sessionLogRetention: config.observability?.sessionLogRetention ?? 7,
897
+ ...current,
898
+ [segment]: setPathValue(current[segment], rest, value),
284
899
  };
285
900
  }
286
901
  function hasValueAtPath(source, path) {
@@ -296,4 +911,19 @@ function hasValueAtPath(source, path) {
296
911
  function isRecord(value) {
297
912
  return typeof value === 'object' && value !== null;
298
913
  }
914
+ function isJsonValue(value) {
915
+ if (value === null
916
+ || typeof value === 'string'
917
+ || typeof value === 'number'
918
+ || typeof value === 'boolean') {
919
+ return true;
920
+ }
921
+ if (Array.isArray(value)) {
922
+ return value.every((entry) => isJsonValue(entry));
923
+ }
924
+ if (!isRecord(value)) {
925
+ return false;
926
+ }
927
+ return Object.values(value).every((entry) => isJsonValue(entry));
928
+ }
299
929
  //# sourceMappingURL=registry.js.map