@zq-silk/yui 0.7.0 → 0.8.1

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 (49) hide show
  1. package/README.md +42 -23
  2. package/dist/cli/commandCatalog.js +234 -122
  3. package/dist/cli/completion.js +3 -3
  4. package/dist/cli/helpRenderer.js +3 -0
  5. package/dist/cli/interactionPolicy.js +44 -23
  6. package/dist/cli/interactiveSelection.js +1 -1
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/roleWizard.js +8 -8
  9. package/dist/cli.js +116 -72
  10. package/dist/commands/agentCommands.js +5 -5
  11. package/dist/commands/configCommands.js +351 -104
  12. package/dist/commands/configOverview.js +60 -0
  13. package/dist/commands/deliveryGuardPreflight.js +2 -2
  14. package/dist/commands/globalRoleCommands.js +9 -9
  15. package/dist/commands/profileCommands.js +8 -8
  16. package/dist/commands/resourcesCommands.js +6 -5
  17. package/dist/commands/taskCommands.js +3 -6
  18. package/dist/commands/taskRoleRuntimeStatus.js +3 -1
  19. package/dist/commands/telemetryCommands.js +11 -6
  20. package/dist/config/configCatalog.js +42 -0
  21. package/dist/config/yuiConfig.js +80 -35
  22. package/dist/context/sessionBootstrapManifest.js +1 -1
  23. package/dist/controller/clientRuntime.js +0 -2
  24. package/dist/controller/controller.js +21 -9
  25. package/dist/controller/fileSchedulerStoreAdapter.js +20 -9
  26. package/dist/controller/runtime.js +32 -18
  27. package/dist/coordination/workMailbox.js +25 -22
  28. package/dist/doctor/doctor.js +2 -2
  29. package/dist/resources/autoResourceGc.js +3 -1
  30. package/dist/review/reviewConfig.js +0 -2
  31. package/dist/run/providerRetry.js +29 -16
  32. package/dist/run/providerRetryConfig.js +5 -3
  33. package/dist/runtime/launchDiagnostics.js +1 -1
  34. package/dist/scheduler/roleRunStall.js +12 -9
  35. package/dist/setup/setupCommand.js +153 -492
  36. package/dist/storage/compatibleTaskStore.js +9 -5
  37. package/dist/storage/migration/productionRegistry.js +58 -0
  38. package/dist/storage/sqliteStore.js +9 -2
  39. package/dist/storage/taskStore.js +21 -2
  40. package/dist/telemetry/sqliteTelemetryStore.js +9 -1
  41. package/dist/telemetry/telemetryConfig.js +1 -18
  42. package/dist/telemetry/telemetryStore.js +2 -2
  43. package/dist/telemetry/telemetryWiring.js +6 -5
  44. package/dist/web/webSnapshot.js +5 -3
  45. package/i18n/README.zh-CN.md +37 -32
  46. package/package.json +1 -1
  47. package/skills/yui-leader/SKILL.md +12 -5
  48. package/skills/yui-operator/SKILL.md +44 -6
  49. package/skills/yui-runtime/SKILL.md +1 -1
@@ -1,82 +1,97 @@
1
+ import { mkdirSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve } from "node:path";
1
3
  import { usageError } from "../errors/cliError.js";
2
- import { DEFAULT_CONTEXT_HARD_TOKENS, DEFAULT_CONTEXT_SOFT_TOKENS, DEFAULT_LEADER_NEXT_ACTION_MODE, DEFAULT_PROVIDER_RETRY_MODE, DEFAULT_RECONCILIATION_INTERVAL_SECONDS, DEFAULT_RESOURCES_GC_MODE, LEADER_NEXT_ACTION_MODES, PROVIDER_RETRY_MODES, reconciliationIntervalMilliseconds, resolveContextBudget, resolveGitBin, resolveLeaderNextActionMode, resolveProviderRetryAdapters, resolveProviderRetryMaxWindowMs, resolveProviderRetryMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveTelemetryMode, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveYieldReceiptReplay } from "../config/yuiConfig.js";
4
+ import { DEFAULT_CONTEXT_HARD_TOKENS, DEFAULT_CONTEXT_SOFT_TOKENS, DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS, DEFAULT_CONTROLLER_TASK_CONCURRENCY, DEFAULT_DELIVERY_TIMEOUT_SECONDS, DEFAULT_LEADER_NEXT_ACTION_MODE, DEFAULT_LEADER_SEMANTIC_BUDGET_TURNS, DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS, DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS, DEFAULT_PROVIDER_RETRY_MODE, DEFAULT_RECONCILIATION_INTERVAL_SECONDS, DEFAULT_RESOURCES_GC_MODE, DEFAULT_RESOURCES_QUARANTINE_TTL_HOURS, DEFAULT_TMUX_HISTORY_LIMIT, LEADER_NEXT_ACTION_MODES, PROVIDER_RETRY_MODES, reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
5
+ import { CONFIG_DEFINITIONS, CONFIG_KEYS, configDefinition, configDefinitionsForDomain } from "../config/configCatalog.js";
3
6
  import { resolveTimeZone } from "../output/timePresentation.js";
4
7
  import { defaultTableWidth, renderTable } from "../output/table.js";
5
8
  import { DEFAULT_DELTA_RECHECK_MAX_CHANGED_FILES, DEFAULT_DELTA_RECHECK_MAX_CHANGED_LINES, REVIEW_FINDING_LEDGER_MODES, REVIEW_DELTA_RECHECK_MODES, REVIEW_TRIGGERS } from "../review/reviewConfig.js";
6
9
  /**
7
- * One uniform key model for every durable Yui setting. `config show` displays
8
- * each key's effective value, `config set <key> <value...>` updates one key,
9
- * and `config clear <key>` resets it to its documented default. Strategy
10
- * settings such as `review` and `leader-next-action` are keys like any other;
11
- * they deliberately have no separate command trees. The stored YuiConfig
12
- * shape is unchanged, so existing Homes keep working without migration.
10
+ * One uniform key model for every durable setting. Each catalog domain uses
11
+ * the same show/set/clear contract; strategy settings such as `review` and
12
+ * `leader-next-action` are keys like any other.
13
13
  */
14
- export const CONFIG_KEYS = [
15
- "time-zone",
16
- "reconciliation-interval-seconds",
17
- "leader-next-action",
18
- "context-budget",
19
- "resources-gc-mode",
20
- "resources-gc-auto-quarantine",
21
- "provider-retry-mode",
22
- "provider-retry-adapters",
23
- "provider-retry-max-window-ms",
24
- "yield-receipt-replay",
25
- "tmux-bin",
26
- "git-bin",
27
- "telemetry-mode",
28
- "telemetry-terminal-keep",
29
- "telemetry-run-cap",
30
- "review"
31
- ];
32
- const CONFIG_USAGE = "Config usage: yui config show | yui config set <key> <value...> | yui config clear <key>.";
33
- const CONFIG_SET_USAGE = `Config set usage: yui config set <key> <value...>; keys: ${CONFIG_KEYS.join(", ")}.`;
34
- const CONFIG_CLEAR_USAGE = `Config clear usage: yui config clear <key>; keys: ${CONFIG_KEYS.join(", ")}.`;
35
- const TIME_ZONE_SET_USAGE = "Config set usage: yui config set time-zone <IANA timezone>.";
36
- const RECONCILIATION_SET_USAGE = "Config set usage: yui config set reconciliation-interval-seconds <5-300>.";
37
- const RESOURCES_GC_MODE_SET_USAGE = "Config set usage: yui config set resources-gc-mode <report|quarantine>.";
38
- const RESOURCES_GC_AUTO_QUARANTINE_SET_USAGE = "Config set usage: yui config set resources-gc-auto-quarantine <true|false>.";
39
- const LEADER_NEXT_ACTION_SET_USAGE = `Config set usage: yui config set leader-next-action <${LEADER_NEXT_ACTION_MODES.join("|")}>.`;
40
- const CONTEXT_BUDGET_SET_USAGE = "Config set usage: yui config set context-budget [--soft-tokens <n>] [--hard-tokens <n>].";
41
- const REVIEW_SET_USAGE = "Config set usage: yui config set review --role <global-role> --trigger <always|leader|final> "
14
+ export { CONFIG_KEYS } from "../config/configCatalog.js";
15
+ const CONFIG_PROPERTIES = Object.fromEntries(CONFIG_DEFINITIONS.map(({ key, property }) => [
16
+ key,
17
+ property
18
+ ]));
19
+ const TIME_ZONE_SET_USAGE = "System config set usage: yui config system set time-zone <IANA timezone>.";
20
+ const RECONCILIATION_SET_USAGE = "Runtime config set usage: yui config runtime set reconciliation-interval-seconds <5-300>.";
21
+ const RESOURCES_GC_MODE_SET_USAGE = "Resources config set usage: yui config resources set resources-gc-mode <report|quarantine>.";
22
+ const RESOURCES_GC_AUTO_QUARANTINE_SET_USAGE = "Resources config set usage: yui config resources set resources-gc-auto-quarantine <true|false>.";
23
+ const LEADER_NEXT_ACTION_SET_USAGE = `Workflow config set usage: yui config workflow set leader-next-action <${LEADER_NEXT_ACTION_MODES.join("|")}>.`;
24
+ const CONTEXT_BUDGET_SET_USAGE = "Workflow config set usage: yui config workflow set context-budget [--soft-tokens <n>] [--hard-tokens <n>].";
25
+ const REVIEW_SET_USAGE = "Workflow config set usage: yui config workflow set review --role <global-role> --trigger <always|leader|final> "
42
26
  + "[--finding-ledger <shadow|enforce>] [--delta-recheck <enabled|disabled>] "
43
27
  + "[--delta-recheck-max-lines <n>] [--delta-recheck-max-files <n>].";
44
- export function runConfigCommand(args, store) {
28
+ export function runConfigCommand(domain, args, store) {
29
+ const keys = configDefinitionsForDomain(domain).map(({ key }) => key);
30
+ const usage = `${capitalize(domain)} config usage: yui config ${domain} show | yui config ${domain} set <key> <value...> | yui config ${domain} clear <key>.`;
45
31
  const [command, ...rest] = args;
46
32
  if (command === "show") {
47
33
  if (rest.length !== 0)
48
- throw usageError("Config show usage: yui config show.");
34
+ throw usageError(`${capitalize(domain)} config show usage: yui config ${domain} show.`);
49
35
  const config = store.getConfig();
50
36
  reconciliationIntervalMilliseconds(config.reconciliationIntervalSeconds);
51
- return { output: renderConfigShow(config), data: effectiveConfigData(config) };
37
+ return { output: renderConfigShow(domain, config), data: effectiveConfigData(config, domain) };
52
38
  }
53
39
  if (command === "set")
54
- return { output: runConfigSet(rest, store) };
40
+ return { output: runConfigSet(domain, keys, rest, store) };
55
41
  if (command === "clear")
56
- return { output: runConfigClear(rest, store) };
57
- throw usageError(command === undefined ? "Config command is required." : `Unknown command: config ${command}`, CONFIG_USAGE);
42
+ return { output: runConfigClear(domain, keys, rest, store) };
43
+ throw usageError(command === undefined ? `${capitalize(domain)} config command is required.` : `Unknown command: config ${domain} ${command}`, usage);
58
44
  }
59
45
  /**
60
- * `config show` is row/column data, so it renders through the shared
46
+ * A domain `show` is row/column data, so it renders through the shared
61
47
  * `renderTable` like every other list command. The column contract follows
62
48
  * the shared rules: left-aligned cells, widths fitted between min/max,
63
49
  * terminal width from `defaultTableWidth()`, and empty values as empty cells.
64
50
  */
65
- function renderConfigShow(config) {
66
- return renderTable("Yui configuration", [
51
+ function renderConfigShow(domain, config) {
52
+ return renderTable(`Yui ${domain} configuration`, [
67
53
  { header: "Setting", minWidth: 20, maxWidth: 32 },
68
54
  { header: "Value", minWidth: 10, maxWidth: 60 }
69
- ], CONFIG_KEY_HANDLERS.map((handler) => [handler.showLabel, handler.showValue(config)]), defaultTableWidth());
55
+ ], CONFIG_KEY_HANDLERS
56
+ .filter(({ key }) => configDefinition(key)?.domain === domain)
57
+ .map((handler) => [handler.showLabel, handler.showValue(config)]), defaultTableWidth());
70
58
  }
71
- /** Effective values under the same field names as the stored YuiConfig. */
72
- function effectiveConfigData(config) {
73
- return {
59
+ /** Effective values for every user-configurable system field. */
60
+ export function effectiveConfigData(config, domain) {
61
+ const budget = resolveContextBudget(config.contextBudget);
62
+ const health = resolveRuntimeHealth(config.runtimeHealth);
63
+ const all = {
64
+ schemaVersion: config.schemaVersion,
65
+ defaultAgent: config.defaultAgent ?? null,
66
+ defaultWorkspace: config.defaultWorkspace ?? null,
74
67
  timeZone: resolveTimeZone(config.timeZone),
68
+ currentTaskId: config.currentTaskId ?? null,
69
+ lastTaskId: config.lastTaskId ?? null,
75
70
  reconciliationIntervalSeconds: config.reconciliationIntervalSeconds
76
71
  ?? DEFAULT_RECONCILIATION_INTERVAL_SECONDS,
77
72
  leaderNextActionMode: resolveLeaderNextActionMode(config.leaderNextActionMode),
73
+ contextBudget: { softTokens: budget.softTokens, hardTokens: budget.hardTokens },
78
74
  resourcesGcMode: resolveResourcesGcMode(config.resourcesGcMode),
79
75
  resourcesGcAutoQuarantine: resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine),
76
+ resourcesQuarantineTtlHours: resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours),
77
+ providerRetryMode: resolveProviderRetryMode(config.providerRetryMode),
78
+ providerRetryAdapters: resolveProviderRetryAdapters(config.providerRetryAdapters),
79
+ providerRetryDelaysSeconds: resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds),
80
+ providerRetryMaxWindowSeconds: resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds),
81
+ runtimeHealth: {
82
+ quietAfterSeconds: health.quietAfterMs / 1_000,
83
+ diagnosticAfterSeconds: health.diagnosticAfterMs / 1_000,
84
+ stallAfterSeconds: health.stallWindowMs / 1_000
85
+ },
86
+ controllerTaskConcurrency: resolveControllerTaskConcurrency(config.controllerTaskConcurrency),
87
+ agentLaunchInactivityTimeoutSeconds: resolveAgentLaunchInactivityTimeoutSeconds(config.agentLaunchInactivityTimeoutSeconds),
88
+ deliveryTimeoutSeconds: resolveDeliveryTimeoutSeconds(config.deliveryTimeoutSeconds),
89
+ leaderSemanticBudgetTurns: resolveLeaderSemanticBudgetTurns(config.leaderSemanticBudgetTurns),
90
+ tmuxBin: resolveTmuxBin(config.tmuxBin),
91
+ tmuxHistoryLimit: resolveTmuxHistoryLimit(config.tmuxHistoryLimit),
92
+ telemetryEnabled: resolveTelemetryEnabled(config.telemetryEnabled),
93
+ telemetryTerminalKeep: resolveTelemetryTerminalKeep(config.telemetryTerminalKeep),
94
+ telemetryRunCap: resolveTelemetryRunCap(config.telemetryRunCap),
80
95
  review: config.review === undefined
81
96
  ? null
82
97
  : {
@@ -88,31 +103,48 @@ function effectiveConfigData(config) {
88
103
  ?? DEFAULT_DELTA_RECHECK_MAX_CHANGED_LINES,
89
104
  deltaRecheckMaxChangedFiles: config.review.deltaRecheckMaxChangedFiles
90
105
  ?? DEFAULT_DELTA_RECHECK_MAX_CHANGED_FILES
91
- }
106
+ },
107
+ completionInstallations: config.completionInstallations ?? {}
108
+ };
109
+ const definitions = configDefinitionsForDomain(domain);
110
+ return {
111
+ ...Object.fromEntries(definitions.map(({ property }) => [property, all[property]])),
112
+ valueSources: Object.fromEntries(definitions.map(({ key }) => {
113
+ const typedKey = key;
114
+ return [
115
+ key,
116
+ config[CONFIG_PROPERTIES[typedKey]] === undefined ? "default" : "stored"
117
+ ];
118
+ }))
92
119
  };
93
120
  }
94
- function runConfigSet(args, store) {
121
+ function runConfigSet(domain, keys, args, store) {
95
122
  const [key, ...values] = args;
123
+ const usage = `${capitalize(domain)} config set usage: yui config ${domain} set <key> <value...>; keys: ${keys.join(", ")}.`;
96
124
  if (key === undefined)
97
- throw usageError(CONFIG_SET_USAGE);
125
+ throw usageError(usage);
98
126
  const handler = CONFIG_KEY_HANDLERS.find((entry) => entry.key === key);
99
- if (handler === undefined) {
100
- throw usageError(`Unknown config key: ${key}`, CONFIG_SET_USAGE);
127
+ if (handler === undefined || !keys.includes(key)) {
128
+ throw usageError(`Unknown ${domain} config key: ${key}`, usage);
101
129
  }
102
130
  return handler.set(values, store);
103
131
  }
104
- function runConfigClear(args, store) {
132
+ function runConfigClear(domain, keys, args, store) {
105
133
  const [key, ...rest] = args;
134
+ const usage = `${capitalize(domain)} config clear usage: yui config ${domain} clear <key>; keys: ${keys.join(", ")}.`;
106
135
  if (key === undefined)
107
- throw usageError(CONFIG_CLEAR_USAGE);
136
+ throw usageError(usage);
108
137
  if (rest.length !== 0)
109
- throw usageError(`Config clear usage: yui config clear ${key}.`);
138
+ throw usageError(`${capitalize(domain)} config clear usage: yui config ${domain} clear ${key}.`);
110
139
  const handler = CONFIG_KEY_HANDLERS.find((entry) => entry.key === key);
111
- if (handler === undefined) {
112
- throw usageError(`Unknown config key: ${key}`, CONFIG_CLEAR_USAGE);
140
+ if (handler === undefined || !keys.includes(key)) {
141
+ throw usageError(`Unknown ${domain} config key: ${key}`, usage);
113
142
  }
114
143
  return handler.clear(store);
115
144
  }
145
+ function capitalize(value) {
146
+ return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
147
+ }
116
148
  function saveConfigKey(store, update) {
117
149
  store.transaction((tx) => {
118
150
  tx.saveConfig(update(tx.getConfig()));
@@ -152,6 +184,49 @@ function parsePositiveIntegerOption(value, usage) {
152
184
  return Number(value);
153
185
  }
154
186
  const CONFIG_KEY_HANDLERS = [
187
+ {
188
+ key: "default-agent",
189
+ showLabel: "Default Agent",
190
+ showValue: (config) => config.defaultAgent ?? "not configured",
191
+ set(args, store) {
192
+ if (args.length !== 1 || args[0].trim().length === 0) {
193
+ throw usageError("System config set usage: yui config system set default-agent <agent-id>.");
194
+ }
195
+ const defaultAgent = args[0].trim();
196
+ if (store.getConfiguredAgent?.(defaultAgent) === null) {
197
+ throw usageError(`Configured Agent not found: ${defaultAgent}.`);
198
+ }
199
+ saveConfigKey(store, (config) => ({ ...config, defaultAgent }));
200
+ return `Default Agent set to ${defaultAgent}\n`;
201
+ },
202
+ clear(store) {
203
+ saveConfigKey(store, (config) => {
204
+ const { defaultAgent: _removed, ...rest } = config;
205
+ return rest;
206
+ });
207
+ return "Default Agent cleared\n";
208
+ }
209
+ },
210
+ {
211
+ key: "default-workspace",
212
+ showLabel: "Default workspace",
213
+ showValue: (config) => config.defaultWorkspace ?? "not configured",
214
+ set(args, store) {
215
+ if (args.length !== 1 || !isAbsolute(args[0])) {
216
+ throw usageError("System config set usage: yui config system set default-workspace <absolute-path>.");
217
+ }
218
+ const defaultWorkspace = resolveDefaultWorkspace(args[0], store);
219
+ saveConfigKey(store, (config) => ({ ...config, defaultWorkspace }));
220
+ return `Default workspace set to ${defaultWorkspace}\n`;
221
+ },
222
+ clear(store) {
223
+ saveConfigKey(store, (config) => {
224
+ const { defaultWorkspace: _removed, ...rest } = config;
225
+ return rest;
226
+ });
227
+ return "Default workspace cleared\n";
228
+ }
229
+ },
155
230
  {
156
231
  key: "time-zone",
157
232
  showLabel: "Time zone",
@@ -216,6 +291,26 @@ const CONFIG_KEY_HANDLERS = [
216
291
  return `Leader next-action mode reset to ${DEFAULT_LEADER_NEXT_ACTION_MODE}\n`;
217
292
  }
218
293
  },
294
+ {
295
+ key: "leader-semantic-budget-turns",
296
+ showLabel: "Leader semantic budget",
297
+ showValue: (config) => `${resolveLeaderSemanticBudgetTurns(config.leaderSemanticBudgetTurns)} turns`,
298
+ set(args, store) {
299
+ const usage = "Workflow config set usage: yui config workflow set leader-semantic-budget-turns <1-20>.";
300
+ if (args.length !== 1)
301
+ throw usageError(usage);
302
+ const leaderSemanticBudgetTurns = validatedConfigValue(() => resolveLeaderSemanticBudgetTurns(Number(args[0])), usage);
303
+ saveConfigKey(store, (config) => ({ ...config, leaderSemanticBudgetTurns }));
304
+ return `Leader semantic budget set to ${leaderSemanticBudgetTurns} turns\n`;
305
+ },
306
+ clear(store) {
307
+ saveConfigKey(store, (config) => {
308
+ const { leaderSemanticBudgetTurns: _removed, ...rest } = config;
309
+ return rest;
310
+ });
311
+ return `Leader semantic budget reset to ${DEFAULT_LEADER_SEMANTIC_BUDGET_TURNS} turns\n`;
312
+ }
313
+ },
219
314
  {
220
315
  key: "context-budget",
221
316
  showLabel: "Context budget",
@@ -296,14 +391,135 @@ const CONFIG_KEY_HANDLERS = [
296
391
  return "Resources GC auto-quarantine reset to off\n";
297
392
  }
298
393
  },
394
+ {
395
+ key: "resources-quarantine-ttl-hours",
396
+ showLabel: "Resource quarantine TTL",
397
+ showValue: (config) => `${resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours)} hours`,
398
+ set(args, store) {
399
+ const usage = "Resources config set usage: yui config resources set resources-quarantine-ttl-hours <1-720>.";
400
+ if (args.length !== 1)
401
+ throw usageError(usage);
402
+ const resourcesQuarantineTtlHours = validatedConfigValue(() => resolveResourcesQuarantineTtlHours(Number(args[0])), usage);
403
+ saveConfigKey(store, (config) => ({ ...config, resourcesQuarantineTtlHours }));
404
+ return `Resource quarantine TTL set to ${resourcesQuarantineTtlHours} hours\n`;
405
+ },
406
+ clear(store) {
407
+ saveConfigKey(store, (config) => {
408
+ const { resourcesQuarantineTtlHours: _removed, ...rest } = config;
409
+ return rest;
410
+ });
411
+ return `Resource quarantine TTL reset to ${DEFAULT_RESOURCES_QUARANTINE_TTL_HOURS} hours\n`;
412
+ }
413
+ },
414
+ {
415
+ key: "controller-task-concurrency",
416
+ showLabel: "Controller Task concurrency",
417
+ showValue: (config) => String(resolveControllerTaskConcurrency(config.controllerTaskConcurrency)),
418
+ set(args, store) {
419
+ const usage = "Runtime config set usage: yui config runtime set controller-task-concurrency <1-32>.";
420
+ if (args.length !== 1)
421
+ throw usageError(usage);
422
+ const controllerTaskConcurrency = validatedConfigValue(() => resolveControllerTaskConcurrency(Number(args[0])), usage);
423
+ saveConfigKey(store, (config) => ({ ...config, controllerTaskConcurrency }));
424
+ return `Controller Task concurrency set to ${controllerTaskConcurrency}\n`;
425
+ },
426
+ clear(store) {
427
+ saveConfigKey(store, (config) => {
428
+ const { controllerTaskConcurrency: _removed, ...rest } = config;
429
+ return rest;
430
+ });
431
+ return `Controller Task concurrency reset to ${DEFAULT_CONTROLLER_TASK_CONCURRENCY}\n`;
432
+ }
433
+ },
434
+ {
435
+ key: "runtime-health",
436
+ showLabel: "Runtime health thresholds",
437
+ showValue: (config) => {
438
+ const health = resolveRuntimeHealth(config.runtimeHealth);
439
+ return `quiet ${health.quietAfterMs / 1_000}s / diagnostic ${health.diagnosticAfterMs / 1_000}s / stall ${health.stallWindowMs / 1_000}s`;
440
+ },
441
+ set(args, store) {
442
+ const usage = "Runtime config set usage: yui config runtime set runtime-health --quiet-after-seconds <n> --diagnostic-after-seconds <n> --stall-after-seconds <n>.";
443
+ if (args.length !== 6)
444
+ throw usageError(usage);
445
+ const options = new Map();
446
+ for (let index = 0; index < args.length; index += 2) {
447
+ const name = args[index];
448
+ const value = Number(args[index + 1]);
449
+ if (!["--quiet-after-seconds", "--diagnostic-after-seconds", "--stall-after-seconds"].includes(name)
450
+ || options.has(name))
451
+ throw usageError(usage);
452
+ options.set(name, value);
453
+ }
454
+ const resolved = validatedConfigValue(() => resolveRuntimeHealth({
455
+ quietAfterSeconds: options.get("--quiet-after-seconds"),
456
+ diagnosticAfterSeconds: options.get("--diagnostic-after-seconds"),
457
+ stallAfterSeconds: options.get("--stall-after-seconds")
458
+ }), usage);
459
+ const runtimeHealth = {
460
+ quietAfterSeconds: resolved.quietAfterMs / 1_000,
461
+ diagnosticAfterSeconds: resolved.diagnosticAfterMs / 1_000,
462
+ stallAfterSeconds: resolved.stallWindowMs / 1_000
463
+ };
464
+ saveConfigKey(store, (config) => ({ ...config, runtimeHealth }));
465
+ return `Runtime health thresholds set to quiet ${runtimeHealth.quietAfterSeconds}s / diagnostic ${runtimeHealth.diagnosticAfterSeconds}s / stall ${runtimeHealth.stallAfterSeconds}s\n`;
466
+ },
467
+ clear(store) {
468
+ saveConfigKey(store, (config) => {
469
+ const { runtimeHealth: _removed, ...rest } = config;
470
+ return rest;
471
+ });
472
+ return "Runtime health thresholds reset to quiet 300s / diagnostic 600s / stall 1800s\n";
473
+ }
474
+ },
475
+ {
476
+ key: "agent-launch-inactivity-timeout-seconds",
477
+ showLabel: "Agent launch inactivity timeout",
478
+ showValue: (config) => `${resolveAgentLaunchInactivityTimeoutSeconds(config.agentLaunchInactivityTimeoutSeconds)} seconds`,
479
+ set(args, store) {
480
+ const usage = "Runtime config set usage: yui config runtime set agent-launch-inactivity-timeout-seconds <15-3600>.";
481
+ if (args.length !== 1)
482
+ throw usageError(usage);
483
+ const agentLaunchInactivityTimeoutSeconds = validatedConfigValue(() => resolveAgentLaunchInactivityTimeoutSeconds(Number(args[0])), usage);
484
+ saveConfigKey(store, (config) => ({ ...config, agentLaunchInactivityTimeoutSeconds }));
485
+ return `Agent launch inactivity timeout set to ${agentLaunchInactivityTimeoutSeconds} seconds\n`;
486
+ },
487
+ clear(store) {
488
+ saveConfigKey(store, (config) => {
489
+ const { agentLaunchInactivityTimeoutSeconds: _removed, ...rest } = config;
490
+ return rest;
491
+ });
492
+ return `Agent launch inactivity timeout reset to ${DEFAULT_AGENT_LAUNCH_INACTIVITY_TIMEOUT_SECONDS} seconds\n`;
493
+ }
494
+ },
495
+ {
496
+ key: "delivery-timeout-seconds",
497
+ showLabel: "Delivery timeout",
498
+ showValue: (config) => `${resolveDeliveryTimeoutSeconds(config.deliveryTimeoutSeconds)} seconds`,
499
+ set(args, store) {
500
+ const usage = "Runtime config set usage: yui config runtime set delivery-timeout-seconds <5-600>.";
501
+ if (args.length !== 1)
502
+ throw usageError(usage);
503
+ const deliveryTimeoutSeconds = validatedConfigValue(() => resolveDeliveryTimeoutSeconds(Number(args[0])), usage);
504
+ saveConfigKey(store, (config) => ({ ...config, deliveryTimeoutSeconds }));
505
+ return `Delivery timeout set to ${deliveryTimeoutSeconds} seconds\n`;
506
+ },
507
+ clear(store) {
508
+ saveConfigKey(store, (config) => {
509
+ const { deliveryTimeoutSeconds: _removed, ...rest } = config;
510
+ return rest;
511
+ });
512
+ return `Delivery timeout reset to ${DEFAULT_DELIVERY_TIMEOUT_SECONDS} seconds\n`;
513
+ }
514
+ },
299
515
  {
300
516
  key: "provider-retry-mode",
301
517
  showLabel: "Provider retry mode",
302
518
  showValue: (config) => resolveProviderRetryMode(config.providerRetryMode),
303
519
  set(args, store) {
304
520
  if (args.length !== 1)
305
- throw usageError(`Config set usage: yui config set provider-retry-mode <${PROVIDER_RETRY_MODES.join("|")}>.`);
306
- const mode = validatedConfigValue(() => resolveProviderRetryMode(args[0]), `Config set usage: yui config set provider-retry-mode <${PROVIDER_RETRY_MODES.join("|")}>.`);
521
+ throw usageError(`Runtime config set usage: yui config runtime set provider-retry-mode <${PROVIDER_RETRY_MODES.join("|")}>.`);
522
+ const mode = validatedConfigValue(() => resolveProviderRetryMode(args[0]), `Runtime config set usage: yui config runtime set provider-retry-mode <${PROVIDER_RETRY_MODES.join("|")}>.`);
307
523
  saveConfigKey(store, (config) => ({ ...config, providerRetryMode: mode }));
308
524
  return `Provider retry mode set to ${mode}\n`;
309
525
  },
@@ -321,11 +537,11 @@ const CONFIG_KEY_HANDLERS = [
321
537
  showValue: (config) => resolveProviderRetryAdapters(config.providerRetryAdapters).join(", ") || "none",
322
538
  set(args, store) {
323
539
  if (args.length !== 1)
324
- throw usageError("Config set usage: yui config set provider-retry-adapters <all|claude,codex|off>.");
540
+ throw usageError("Runtime config set usage: yui config runtime set provider-retry-adapters <all|claude,codex|off>.");
325
541
  const raw = args[0].trim().toLowerCase();
326
542
  const adapters = raw === "off" || raw === "" || raw === "0"
327
543
  ? []
328
- : validatedConfigValue(() => resolveProviderRetryAdapters(raw.split(",")), "Config set usage: yui config set provider-retry-adapters <all|claude,codex|off>.");
544
+ : validatedConfigValue(() => resolveProviderRetryAdapters(raw.split(",")), "Runtime config set usage: yui config runtime set provider-retry-adapters <all|claude,codex|off>.");
329
545
  saveConfigKey(store, (config) => ({ ...config, providerRetryAdapters: adapters }));
330
546
  return `Provider retry adapters set to ${adapters.join(", ") || "none"}\n`;
331
547
  },
@@ -338,41 +554,43 @@ const CONFIG_KEY_HANDLERS = [
338
554
  }
339
555
  },
340
556
  {
341
- key: "provider-retry-max-window-ms",
342
- showLabel: "Provider retry max window",
343
- showValue: (config) => `${resolveProviderRetryMaxWindowMs(config.providerRetryMaxWindowMs)} ms`,
557
+ key: "provider-retry-delays-seconds",
558
+ showLabel: "Provider retry delays",
559
+ showValue: (config) => `${resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds).join(", ")} seconds`,
344
560
  set(args, store) {
561
+ const usage = "Runtime config set usage: yui config runtime set provider-retry-delays-seconds <comma-separated-seconds>.";
345
562
  if (args.length !== 1)
346
- throw usageError("Config set usage: yui config set provider-retry-max-window-ms <milliseconds>.");
347
- const maxWindowMs = validatedConfigValue(() => resolveProviderRetryMaxWindowMs(Number(args[0])), "Config set usage: yui config set provider-retry-max-window-ms <milliseconds>.");
348
- saveConfigKey(store, (config) => ({ ...config, providerRetryMaxWindowMs: maxWindowMs }));
349
- return `Provider retry max window set to ${maxWindowMs} ms\n`;
563
+ throw usageError(usage);
564
+ const providerRetryDelaysSeconds = validatedConfigValue(() => resolveProviderRetryDelaysSeconds(args[0].split(",").map(Number)), usage);
565
+ saveConfigKey(store, (config) => ({ ...config, providerRetryDelaysSeconds }));
566
+ return `Provider retry delays set to ${providerRetryDelaysSeconds.join(", ")} seconds\n`;
350
567
  },
351
568
  clear(store) {
352
569
  saveConfigKey(store, (config) => {
353
- const { providerRetryMaxWindowMs: _removed, ...rest } = config;
570
+ const { providerRetryDelaysSeconds: _removed, ...rest } = config;
354
571
  return rest;
355
572
  });
356
- return "Provider retry max window reset to default\n";
573
+ return `Provider retry delays reset to ${DEFAULT_PROVIDER_RETRY_DELAYS_SECONDS.join(", ")} seconds\n`;
357
574
  }
358
575
  },
359
576
  {
360
- key: "yield-receipt-replay",
361
- showLabel: "Yield receipt replay",
362
- showValue: (config) => (resolveYieldReceiptReplay(config.yieldReceiptReplay) ? "on" : "off"),
577
+ key: "provider-retry-max-window-seconds",
578
+ showLabel: "Provider retry max window",
579
+ showValue: (config) => `${resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds)} seconds`,
363
580
  set(args, store) {
581
+ const usage = "Runtime config set usage: yui config runtime set provider-retry-max-window-seconds <positive-seconds>.";
364
582
  if (args.length !== 1)
365
- throw usageError("Config set usage: yui config set yield-receipt-replay <true|false>.");
366
- const yieldReceiptReplay = validatedConfigValue(() => resolveYieldReceiptReplay(parseBooleanConfigValue(args[0])), "Config set usage: yui config set yield-receipt-replay <true|false>.");
367
- saveConfigKey(store, (config) => ({ ...config, yieldReceiptReplay }));
368
- return `Yield receipt replay set to ${yieldReceiptReplay ? "on" : "off"}\n`;
583
+ throw usageError(usage);
584
+ const providerRetryMaxWindowSeconds = validatedConfigValue(() => resolveProviderRetryMaxWindowSeconds(Number(args[0])), usage);
585
+ saveConfigKey(store, (config) => ({ ...config, providerRetryMaxWindowSeconds }));
586
+ return `Provider retry max window set to ${providerRetryMaxWindowSeconds} seconds\n`;
369
587
  },
370
588
  clear(store) {
371
589
  saveConfigKey(store, (config) => {
372
- const { yieldReceiptReplay: _removed, ...rest } = config;
590
+ const { providerRetryMaxWindowSeconds: _removed, ...rest } = config;
373
591
  return rest;
374
592
  });
375
- return "Yield receipt replay reset to on\n";
593
+ return `Provider retry max window reset to ${DEFAULT_PROVIDER_RETRY_MAX_WINDOW_SECONDS} seconds\n`;
376
594
  }
377
595
  },
378
596
  {
@@ -381,8 +599,8 @@ const CONFIG_KEY_HANDLERS = [
381
599
  showValue: (config) => resolveTmuxBin(config.tmuxBin),
382
600
  set(args, store) {
383
601
  if (args.length !== 1)
384
- throw usageError("Config set usage: yui config set tmux-bin <path>.");
385
- const tmuxBin = validatedConfigValue(() => resolveTmuxBin(args[0]), "Config set usage: yui config set tmux-bin <path>.");
602
+ throw usageError("Tools config set usage: yui config tools set tmux-bin <path>.");
603
+ const tmuxBin = validatedConfigValue(() => resolveTmuxBin(args[0]), "Tools config set usage: yui config tools set tmux-bin <path>.");
386
604
  saveConfigKey(store, (config) => ({ ...config, tmuxBin }));
387
605
  return `Tmux bin set to ${tmuxBin}\n`;
388
606
  },
@@ -395,41 +613,43 @@ const CONFIG_KEY_HANDLERS = [
395
613
  }
396
614
  },
397
615
  {
398
- key: "git-bin",
399
- showLabel: "Git bin",
400
- showValue: (config) => resolveGitBin(config.gitBin),
616
+ key: "tmux-history-limit",
617
+ showLabel: "Tmux history limit",
618
+ showValue: (config) => String(resolveTmuxHistoryLimit(config.tmuxHistoryLimit)),
401
619
  set(args, store) {
620
+ const usage = "Tools config set usage: yui config tools set tmux-history-limit <1000-1000000>.";
402
621
  if (args.length !== 1)
403
- throw usageError("Config set usage: yui config set git-bin <path>.");
404
- const gitBin = validatedConfigValue(() => resolveGitBin(args[0]), "Config set usage: yui config set git-bin <path>.");
405
- saveConfigKey(store, (config) => ({ ...config, gitBin }));
406
- return `Git bin set to ${gitBin}\n`;
622
+ throw usageError(usage);
623
+ const tmuxHistoryLimit = validatedConfigValue(() => resolveTmuxHistoryLimit(Number(args[0])), usage);
624
+ saveConfigKey(store, (config) => ({ ...config, tmuxHistoryLimit }));
625
+ return `Tmux history limit set to ${tmuxHistoryLimit}\n`;
407
626
  },
408
627
  clear(store) {
409
628
  saveConfigKey(store, (config) => {
410
- const { gitBin: _removed, ...rest } = config;
629
+ const { tmuxHistoryLimit: _removed, ...rest } = config;
411
630
  return rest;
412
631
  });
413
- return "Git bin reset to git\n";
632
+ return `Tmux history limit reset to ${DEFAULT_TMUX_HISTORY_LIMIT}\n`;
414
633
  }
415
634
  },
416
635
  {
417
- key: "telemetry-mode",
418
- showLabel: "Telemetry mode",
419
- showValue: (config) => resolveTelemetryMode(config.telemetryMode),
636
+ key: "telemetry-enabled",
637
+ showLabel: "Diagnostic telemetry",
638
+ showValue: (config) => (resolveTelemetryEnabled(config.telemetryEnabled) ? "on" : "off"),
420
639
  set(args, store) {
640
+ const usage = "Tools config set usage: yui config tools set telemetry-enabled <true|false>.";
421
641
  if (args.length !== 1)
422
- throw usageError("Config set usage: yui config set telemetry-mode <legacy|dual|bounded>.");
423
- const telemetryMode = validatedConfigValue(() => resolveTelemetryMode(args[0]), "Config set usage: yui config set telemetry-mode <legacy|dual|bounded>.");
424
- saveConfigKey(store, (config) => ({ ...config, telemetryMode }));
425
- return `Telemetry mode set to ${telemetryMode}\n`;
642
+ throw usageError(usage);
643
+ const telemetryEnabled = validatedConfigValue(() => resolveTelemetryEnabled(parseBooleanConfigValue(args[0])), usage);
644
+ saveConfigKey(store, (config) => ({ ...config, telemetryEnabled }));
645
+ return `Diagnostic telemetry set to ${telemetryEnabled ? "on" : "off"}\n`;
426
646
  },
427
647
  clear(store) {
428
648
  saveConfigKey(store, (config) => {
429
- const { telemetryMode: _removed, ...rest } = config;
649
+ const { telemetryEnabled: _removed, ...rest } = config;
430
650
  return rest;
431
651
  });
432
- return "Telemetry mode reset to legacy\n";
652
+ return "Diagnostic telemetry reset to off\n";
433
653
  }
434
654
  },
435
655
  {
@@ -438,8 +658,8 @@ const CONFIG_KEY_HANDLERS = [
438
658
  showValue: (config) => String(resolveTelemetryTerminalKeep(config.telemetryTerminalKeep)),
439
659
  set(args, store) {
440
660
  if (args.length !== 1)
441
- throw usageError("Config set usage: yui config set telemetry-terminal-keep <n>.");
442
- const telemetryTerminalKeep = validatedConfigValue(() => resolveTelemetryTerminalKeep(Number(args[0])), "Config set usage: yui config set telemetry-terminal-keep <n>.");
661
+ throw usageError("Tools config set usage: yui config tools set telemetry-terminal-keep <n>.");
662
+ const telemetryTerminalKeep = validatedConfigValue(() => resolveTelemetryTerminalKeep(Number(args[0])), "Tools config set usage: yui config tools set telemetry-terminal-keep <n>.");
443
663
  saveConfigKey(store, (config) => ({ ...config, telemetryTerminalKeep }));
444
664
  return `Telemetry terminal keep set to ${telemetryTerminalKeep}\n`;
445
665
  },
@@ -457,8 +677,8 @@ const CONFIG_KEY_HANDLERS = [
457
677
  showValue: (config) => String(resolveTelemetryRunCap(config.telemetryRunCap)),
458
678
  set(args, store) {
459
679
  if (args.length !== 1)
460
- throw usageError("Config set usage: yui config set telemetry-run-cap <n>.");
461
- const telemetryRunCap = validatedConfigValue(() => resolveTelemetryRunCap(Number(args[0])), "Config set usage: yui config set telemetry-run-cap <n>.");
680
+ throw usageError("Tools config set usage: yui config tools set telemetry-run-cap <n>.");
681
+ const telemetryRunCap = validatedConfigValue(() => resolveTelemetryRunCap(Number(args[0])), "Tools config set usage: yui config tools set telemetry-run-cap <n>.");
462
682
  saveConfigKey(store, (config) => ({ ...config, telemetryRunCap }));
463
683
  return `Telemetry run cap set to ${telemetryRunCap}\n`;
464
684
  },
@@ -548,3 +768,30 @@ const CONFIG_KEY_HANDLERS = [
548
768
  }
549
769
  }
550
770
  ];
771
+ const configuredHandlerKeys = new Set(CONFIG_KEY_HANDLERS.map(({ key }) => key));
772
+ const missingConfigHandlers = CONFIG_KEYS.filter((key) => !configuredHandlerKeys.has(key));
773
+ const duplicateConfigHandlers = CONFIG_KEY_HANDLERS
774
+ .map(({ key }) => key)
775
+ .filter((key, index, keys) => keys.indexOf(key) !== index);
776
+ if (missingConfigHandlers.length > 0 || duplicateConfigHandlers.length > 0) {
777
+ throw new Error(`Config handler/catalog drift: missing=${missingConfigHandlers.join(",") || "none"}; `
778
+ + `duplicate=${duplicateConfigHandlers.join(",") || "none"}.`);
779
+ }
780
+ function resolveDefaultWorkspace(value, store) {
781
+ const requested = resolve(value);
782
+ if (store.rootDirectory === undefined)
783
+ return requested;
784
+ const requestedHome = resolve(store.rootDirectory());
785
+ assertWorkspaceOutsideHome(requested, requestedHome);
786
+ mkdirSync(requested, { recursive: true, mode: 0o700 });
787
+ const workspace = realpathSync(requested);
788
+ const home = realpathSync(requestedHome);
789
+ assertWorkspaceOutsideHome(workspace, home);
790
+ return workspace;
791
+ }
792
+ function assertWorkspaceOutsideHome(workspace, home) {
793
+ const fromHome = relative(home, workspace);
794
+ if (fromHome === "" || (!fromHome.startsWith("..") && !isAbsolute(fromHome))) {
795
+ throw usageError("Default workspace must be outside YUI_HOME.");
796
+ }
797
+ }