@zq-silk/yui 0.6.13 → 0.6.15

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 (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +172 -62
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +102 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +518 -55
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +71 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +191 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +16 -2
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +253 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +198 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +7 -1
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +289 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +17 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -1,200 +1,550 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
- import { DEFAULT_RECONCILIATION_INTERVAL_SECONDS, reconciliationIntervalMilliseconds, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.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";
3
3
  import { resolveTimeZone } from "../output/timePresentation.js";
4
- import { REVIEW_FINDING_LEDGER_MODES, REVIEW_TRIGGERS } from "../review/reviewConfig.js";
5
- import { DEFAULT_LEADER_NEXT_ACTION_MODE, LEADER_NEXT_ACTION_MODES, resolveLeaderNextActionMode } from "../config/yuiConfig.js";
6
- const CONFIG_SET_USAGE = "Config set usage: yui config set "
7
- + "<--time-zone <IANA timezone> | "
8
- + "--reconciliation-interval-seconds <seconds> | "
9
- + "--resources-gc-mode <report|quarantine> | "
10
- + "--resources-gc-auto-quarantine <true|false>>.";
4
+ import { defaultTableWidth, renderTable } from "../output/table.js";
5
+ 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
+ /**
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.
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> "
42
+ + "[--finding-ledger <shadow|enforce>] [--delta-recheck <enabled|disabled>] "
43
+ + "[--delta-recheck-max-lines <n>] [--delta-recheck-max-files <n>].";
11
44
  export function runConfigCommand(args, store) {
12
45
  const [command, ...rest] = args;
13
- if (command === "review")
14
- return runReviewConfigCommand(rest, store);
15
- if (command === "leader-next-action")
16
- return runLeaderNextActionConfigCommand(rest, store);
17
46
  if (command === "show") {
18
47
  if (rest.length !== 0)
19
48
  throw usageError("Config show usage: yui config show.");
20
49
  const config = store.getConfig();
21
- const reconciliationIntervalSeconds = config.reconciliationIntervalSeconds
22
- ?? DEFAULT_RECONCILIATION_INTERVAL_SECONDS;
23
- reconciliationIntervalMilliseconds(reconciliationIntervalSeconds);
24
- return [
25
- `Time zone: ${resolveTimeZone(config.timeZone)}`,
26
- `Reconciliation interval: ${reconciliationIntervalSeconds} seconds`,
27
- `Leader next-action mode: ${resolveLeaderNextActionMode(config.leaderNextActionMode)}`,
28
- `Resources GC mode: ${resolveResourcesGcMode(config.resourcesGcMode)}`,
29
- `Resources GC auto-quarantine: ${resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine) ? "on" : "off"}`,
30
- ""
31
- ].join("\n");
50
+ reconciliationIntervalMilliseconds(config.reconciliationIntervalSeconds);
51
+ return { output: renderConfigShow(config), data: effectiveConfigData(config) };
32
52
  }
33
- if (command === "set") {
34
- if (rest.length !== 2) {
35
- throw configSetUsageError();
53
+ if (command === "set")
54
+ return { output: runConfigSet(rest, store) };
55
+ if (command === "clear")
56
+ return { output: runConfigClear(rest, store) };
57
+ throw usageError(command === undefined ? "Config command is required." : `Unknown command: config ${command}`, CONFIG_USAGE);
58
+ }
59
+ /**
60
+ * `config show` is row/column data, so it renders through the shared
61
+ * `renderTable` like every other list command. The column contract follows
62
+ * the shared rules: left-aligned cells, widths fitted between min/max,
63
+ * terminal width from `defaultTableWidth()`, and empty values as empty cells.
64
+ */
65
+ function renderConfigShow(config) {
66
+ return renderTable("Yui configuration", [
67
+ { header: "Setting", minWidth: 20, maxWidth: 32 },
68
+ { header: "Value", minWidth: 10, maxWidth: 60 }
69
+ ], CONFIG_KEY_HANDLERS.map((handler) => [handler.showLabel, handler.showValue(config)]), defaultTableWidth());
70
+ }
71
+ /** Effective values under the same field names as the stored YuiConfig. */
72
+ function effectiveConfigData(config) {
73
+ return {
74
+ timeZone: resolveTimeZone(config.timeZone),
75
+ reconciliationIntervalSeconds: config.reconciliationIntervalSeconds
76
+ ?? DEFAULT_RECONCILIATION_INTERVAL_SECONDS,
77
+ leaderNextActionMode: resolveLeaderNextActionMode(config.leaderNextActionMode),
78
+ resourcesGcMode: resolveResourcesGcMode(config.resourcesGcMode),
79
+ resourcesGcAutoQuarantine: resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine),
80
+ review: config.review === undefined
81
+ ? null
82
+ : {
83
+ roleName: config.review.roleName,
84
+ trigger: config.review.trigger,
85
+ findingLedger: config.review.findingLedger ?? "shadow",
86
+ deltaRecheck: config.review.deltaRecheck ?? "disabled",
87
+ deltaRecheckMaxChangedLines: config.review.deltaRecheckMaxChangedLines
88
+ ?? DEFAULT_DELTA_RECHECK_MAX_CHANGED_LINES,
89
+ deltaRecheckMaxChangedFiles: config.review.deltaRecheckMaxChangedFiles
90
+ ?? DEFAULT_DELTA_RECHECK_MAX_CHANGED_FILES
91
+ }
92
+ };
93
+ }
94
+ function runConfigSet(args, store) {
95
+ const [key, ...values] = args;
96
+ if (key === undefined)
97
+ throw usageError(CONFIG_SET_USAGE);
98
+ const handler = CONFIG_KEY_HANDLERS.find((entry) => entry.key === key);
99
+ if (handler === undefined) {
100
+ throw usageError(`Unknown config key: ${key}`, CONFIG_SET_USAGE);
101
+ }
102
+ return handler.set(values, store);
103
+ }
104
+ function runConfigClear(args, store) {
105
+ const [key, ...rest] = args;
106
+ if (key === undefined)
107
+ throw usageError(CONFIG_CLEAR_USAGE);
108
+ if (rest.length !== 0)
109
+ throw usageError(`Config clear usage: yui config clear ${key}.`);
110
+ const handler = CONFIG_KEY_HANDLERS.find((entry) => entry.key === key);
111
+ if (handler === undefined) {
112
+ throw usageError(`Unknown config key: ${key}`, CONFIG_CLEAR_USAGE);
113
+ }
114
+ return handler.clear(store);
115
+ }
116
+ function saveConfigKey(store, update) {
117
+ store.transaction((tx) => {
118
+ tx.saveConfig(update(tx.getConfig()));
119
+ });
120
+ }
121
+ function validatedConfigValue(validate, usage) {
122
+ try {
123
+ return validate();
124
+ }
125
+ catch (error) {
126
+ if (error instanceof TypeError) {
127
+ throw usageError(error.message, usage);
36
128
  }
37
- if (rest[0] === "--time-zone") {
38
- const timeZone = validatedConfigValue(() => resolveTimeZone(rest[1]));
39
- store.transaction((tx) => {
40
- tx.saveConfig({ ...tx.getConfig(), timeZone });
41
- });
129
+ throw error;
130
+ }
131
+ }
132
+ function parseBudgetToken(value, label) {
133
+ if (!/^\d+$/.test(value)) {
134
+ throw usageError(`${label} must be a positive integer.`);
135
+ }
136
+ return Number(value);
137
+ }
138
+ function parseReconciliationIntervalSeconds(value) {
139
+ if (!/^\d+$/.test(value))
140
+ return Number.NaN;
141
+ return Number(value);
142
+ }
143
+ function parseBooleanConfigValue(value) {
144
+ return value === "true" ? true : value === "false" ? false : value;
145
+ }
146
+ function parsePositiveIntegerOption(value, usage) {
147
+ if (value === undefined)
148
+ return undefined;
149
+ if (!/^\d+$/.test(value) || Number(value) < 1) {
150
+ throw usageError(usage);
151
+ }
152
+ return Number(value);
153
+ }
154
+ const CONFIG_KEY_HANDLERS = [
155
+ {
156
+ key: "time-zone",
157
+ showLabel: "Time zone",
158
+ showValue: (config) => resolveTimeZone(config.timeZone),
159
+ set(args, store) {
160
+ if (args.length !== 1)
161
+ throw usageError(TIME_ZONE_SET_USAGE);
162
+ const timeZone = validatedConfigValue(() => resolveTimeZone(args[0]), TIME_ZONE_SET_USAGE);
163
+ saveConfigKey(store, (config) => ({ ...config, timeZone }));
42
164
  return `Time zone set to ${timeZone}\n`;
43
- }
44
- if (rest[0] === "--reconciliation-interval-seconds") {
45
- const reconciliationIntervalSeconds = parseReconciliationIntervalSeconds(rest[1]);
46
- validatedConfigValue(() => reconciliationIntervalMilliseconds(reconciliationIntervalSeconds));
47
- store.transaction((tx) => {
48
- tx.saveConfig({ ...tx.getConfig(), reconciliationIntervalSeconds });
165
+ },
166
+ clear(store) {
167
+ saveConfigKey(store, (config) => {
168
+ const { timeZone: _removed, ...rest } = config;
169
+ return rest;
49
170
  });
171
+ return `Time zone reset to ${resolveTimeZone(undefined)}\n`;
172
+ }
173
+ },
174
+ {
175
+ key: "reconciliation-interval-seconds",
176
+ showLabel: "Reconciliation interval",
177
+ showValue: (config) => `${config.reconciliationIntervalSeconds ?? DEFAULT_RECONCILIATION_INTERVAL_SECONDS} seconds`,
178
+ set(args, store) {
179
+ if (args.length !== 1)
180
+ throw usageError(RECONCILIATION_SET_USAGE);
181
+ const reconciliationIntervalSeconds = parseReconciliationIntervalSeconds(args[0]);
182
+ validatedConfigValue(() => reconciliationIntervalMilliseconds(reconciliationIntervalSeconds), RECONCILIATION_SET_USAGE);
183
+ saveConfigKey(store, (config) => ({ ...config, reconciliationIntervalSeconds }));
50
184
  return `Reconciliation interval set to ${reconciliationIntervalSeconds} seconds\n`;
185
+ },
186
+ clear(store) {
187
+ saveConfigKey(store, (config) => {
188
+ const { reconciliationIntervalSeconds: _removed, ...rest } = config;
189
+ return rest;
190
+ });
191
+ return `Reconciliation interval reset to ${DEFAULT_RECONCILIATION_INTERVAL_SECONDS} seconds\n`;
51
192
  }
52
- if (rest[0] === "--resources-gc-mode") {
53
- const resourcesGcMode = validatedConfigValue(() => resolveResourcesGcMode(rest[1]));
54
- store.transaction((tx) => {
55
- tx.saveConfig({ ...tx.getConfig(), resourcesGcMode });
193
+ },
194
+ {
195
+ key: "leader-next-action",
196
+ showLabel: "Leader next-action mode",
197
+ showValue: (config) => resolveLeaderNextActionMode(config.leaderNextActionMode),
198
+ set(args, store) {
199
+ if (args.length !== 1)
200
+ throw usageError(LEADER_NEXT_ACTION_SET_USAGE);
201
+ let mode;
202
+ try {
203
+ mode = resolveLeaderNextActionMode(args[0]);
204
+ }
205
+ catch (error) {
206
+ throw usageError(error instanceof Error ? error.message : String(error), LEADER_NEXT_ACTION_SET_USAGE);
207
+ }
208
+ saveConfigKey(store, (config) => ({ ...config, leaderNextActionMode: mode }));
209
+ return `Leader next-action mode set to ${mode}\n`;
210
+ },
211
+ clear(store) {
212
+ saveConfigKey(store, (config) => {
213
+ const { leaderNextActionMode: _removed, ...rest } = config;
214
+ return rest;
56
215
  });
57
- return `Resources GC mode set to ${resourcesGcMode}\n`;
216
+ return `Leader next-action mode reset to ${DEFAULT_LEADER_NEXT_ACTION_MODE}\n`;
58
217
  }
59
- if (rest[0] === "--resources-gc-auto-quarantine") {
60
- const resourcesGcAutoQuarantine = validatedConfigValue(() => resolveResourcesGcAutoQuarantine(rest[1] === "true" ? true : rest[1] === "false" ? false : rest[1]));
61
- store.transaction((tx) => {
62
- tx.saveConfig({ ...tx.getConfig(), resourcesGcAutoQuarantine });
218
+ },
219
+ {
220
+ key: "context-budget",
221
+ showLabel: "Context budget",
222
+ showValue: (config) => {
223
+ const budget = resolveContextBudget(config.contextBudget);
224
+ return `soft ${budget.softTokens} / hard ${budget.hardTokens} tokens`;
225
+ },
226
+ set(args, store) {
227
+ if (args.length !== 2 && args.length !== 4)
228
+ throw usageError(CONTEXT_BUDGET_SET_USAGE);
229
+ const options = new Map();
230
+ for (let index = 0; index < args.length; index += 2) {
231
+ const name = args[index];
232
+ const value = args[index + 1];
233
+ if (!["--soft-tokens", "--hard-tokens"].includes(name)
234
+ || value === undefined
235
+ || options.has(name)) {
236
+ throw usageError(CONTEXT_BUDGET_SET_USAGE);
237
+ }
238
+ options.set(name, value);
239
+ }
240
+ const current = resolveContextBudget(store.getConfig().contextBudget);
241
+ const next = {
242
+ ...(options.get("--soft-tokens") === undefined
243
+ ? {}
244
+ : { softTokens: parseBudgetToken(options.get("--soft-tokens"), "--soft-tokens") }),
245
+ ...(options.get("--hard-tokens") === undefined
246
+ ? {}
247
+ : { hardTokens: parseBudgetToken(options.get("--hard-tokens"), "--hard-tokens") })
248
+ };
249
+ const resolved = resolveContextBudget({ ...current, ...next });
250
+ saveConfigKey(store, (config) => ({ ...config, contextBudget: resolved }));
251
+ return `Context budget set to soft ${resolved.softTokens} / hard ${resolved.hardTokens} tokens\n`;
252
+ },
253
+ clear(store) {
254
+ saveConfigKey(store, (config) => {
255
+ const { contextBudget: _removed, ...rest } = config;
256
+ return rest;
257
+ });
258
+ return `Context budget reset to soft ${DEFAULT_CONTEXT_SOFT_TOKENS} / hard ${DEFAULT_CONTEXT_HARD_TOKENS} tokens\n`;
259
+ }
260
+ },
261
+ {
262
+ key: "resources-gc-mode",
263
+ showLabel: "Resources GC mode",
264
+ showValue: (config) => resolveResourcesGcMode(config.resourcesGcMode),
265
+ set(args, store) {
266
+ if (args.length !== 1)
267
+ throw usageError(RESOURCES_GC_MODE_SET_USAGE);
268
+ const resourcesGcMode = validatedConfigValue(() => resolveResourcesGcMode(args[0]), RESOURCES_GC_MODE_SET_USAGE);
269
+ saveConfigKey(store, (config) => ({ ...config, resourcesGcMode }));
270
+ return `Resources GC mode set to ${resourcesGcMode}\n`;
271
+ },
272
+ clear(store) {
273
+ saveConfigKey(store, (config) => {
274
+ const { resourcesGcMode: _removed, ...rest } = config;
275
+ return rest;
63
276
  });
277
+ return `Resources GC mode reset to ${DEFAULT_RESOURCES_GC_MODE}\n`;
278
+ }
279
+ },
280
+ {
281
+ key: "resources-gc-auto-quarantine",
282
+ showLabel: "Resources GC auto-quarantine",
283
+ showValue: (config) => (resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine) ? "on" : "off"),
284
+ set(args, store) {
285
+ if (args.length !== 1)
286
+ throw usageError(RESOURCES_GC_AUTO_QUARANTINE_SET_USAGE);
287
+ const resourcesGcAutoQuarantine = validatedConfigValue(() => resolveResourcesGcAutoQuarantine(parseBooleanConfigValue(args[0])), RESOURCES_GC_AUTO_QUARANTINE_SET_USAGE);
288
+ saveConfigKey(store, (config) => ({ ...config, resourcesGcAutoQuarantine }));
64
289
  return `Resources GC auto-quarantine set to ${resourcesGcAutoQuarantine ? "on" : "off"}\n`;
290
+ },
291
+ clear(store) {
292
+ saveConfigKey(store, (config) => {
293
+ const { resourcesGcAutoQuarantine: _removed, ...rest } = config;
294
+ return rest;
295
+ });
296
+ return "Resources GC auto-quarantine reset to off\n";
65
297
  }
66
- throw configSetUsageError();
67
- }
68
- throw usageError(command === undefined
69
- ? "Config command is required."
70
- : `Unknown command: config ${command}`);
71
- }
72
- function runLeaderNextActionConfigCommand(args, store) {
73
- const [command, ...rest] = args;
74
- if (command === "show") {
75
- if (rest.length !== 0) {
76
- throw usageError("Config leader-next-action show usage: yui config leader-next-action show.");
298
+ },
299
+ {
300
+ key: "provider-retry-mode",
301
+ showLabel: "Provider retry mode",
302
+ showValue: (config) => resolveProviderRetryMode(config.providerRetryMode),
303
+ set(args, store) {
304
+ 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("|")}>.`);
307
+ saveConfigKey(store, (config) => ({ ...config, providerRetryMode: mode }));
308
+ return `Provider retry mode set to ${mode}\n`;
309
+ },
310
+ clear(store) {
311
+ saveConfigKey(store, (config) => {
312
+ const { providerRetryMode: _removed, ...rest } = config;
313
+ return rest;
314
+ });
315
+ return `Provider retry mode reset to ${DEFAULT_PROVIDER_RETRY_MODE}\n`;
77
316
  }
78
- return `Leader next-action mode: ${resolveLeaderNextActionMode(store.getConfig().leaderNextActionMode)}\n`;
79
- }
80
- if (command === "set") {
81
- const usage = "Config leader-next-action set usage: "
82
- + `yui config leader-next-action set <${LEADER_NEXT_ACTION_MODES.join("|")}>.`;
83
- if (rest.length !== 1)
84
- throw usageError(usage);
85
- let mode;
86
- try {
87
- mode = resolveLeaderNextActionMode(rest[0]);
88
- }
89
- catch (error) {
90
- throw usageError(error instanceof Error ? error.message : String(error), usage);
91
- }
92
- store.transaction((tx) => {
93
- tx.saveConfig({ ...tx.getConfig(), leaderNextActionMode: mode });
94
- });
95
- return `Leader next-action mode set to ${mode}\n`;
96
- }
97
- if (command === "clear") {
98
- if (rest.length !== 0) {
99
- throw usageError("Config leader-next-action clear usage: yui config leader-next-action clear.");
100
- }
101
- store.transaction((tx) => {
102
- const { leaderNextActionMode: _mode, ...config } = tx.getConfig();
103
- tx.saveConfig(config);
104
- });
105
- return `Leader next-action mode reset to ${DEFAULT_LEADER_NEXT_ACTION_MODE}\n`;
106
- }
107
- throw usageError(command === undefined
108
- ? "Config leader-next-action command is required."
109
- : `Unknown command: config leader-next-action ${command}`);
110
- }
111
- function runReviewConfigCommand(args, store) {
112
- const [command, ...rest] = args;
113
- if (command === "show") {
114
- if (rest.length !== 0)
115
- throw usageError("Config review show usage: yui config review show.");
116
- const review = store.getConfig().review;
117
- return review === undefined
118
- ? "Review: disabled\n"
119
- : `Review: ${review.roleName} (${review.trigger}; finding ledger: ${review.findingLedger ?? "shadow"})\n`;
120
- }
121
- if (command === "clear") {
122
- if (rest.length !== 0)
123
- throw usageError("Config review clear usage: yui config review clear.");
124
- store.transaction((tx) => {
125
- const { review: _review, ...config } = tx.getConfig();
126
- tx.saveConfig(config);
127
- });
128
- return "Review disabled\n";
129
- }
130
- if (command === "set") {
131
- const usage = "Config review set usage: "
132
- + "yui config review set --role <global-role> --trigger <always|leader|final> "
133
- + "[--finding-ledger <shadow|enforce>].";
134
- if (rest.length !== 4 && rest.length !== 6)
135
- throw usageError(usage);
136
- const options = new Map();
137
- for (let index = 0; index < rest.length; index += 2) {
138
- const name = rest[index];
139
- const value = rest[index + 1];
140
- if (!["--role", "--trigger", "--finding-ledger"].includes(name)
141
- || value === undefined
142
- || options.has(name)) {
143
- throw usageError(usage);
317
+ },
318
+ {
319
+ key: "provider-retry-adapters",
320
+ showLabel: "Provider retry adapters",
321
+ showValue: (config) => resolveProviderRetryAdapters(config.providerRetryAdapters).join(", ") || "none",
322
+ set(args, store) {
323
+ if (args.length !== 1)
324
+ throw usageError("Config set usage: yui config set provider-retry-adapters <all|claude,codex|off>.");
325
+ const raw = args[0].trim().toLowerCase();
326
+ const adapters = raw === "off" || raw === "" || raw === "0"
327
+ ? []
328
+ : validatedConfigValue(() => resolveProviderRetryAdapters(raw.split(",")), "Config set usage: yui config set provider-retry-adapters <all|claude,codex|off>.");
329
+ saveConfigKey(store, (config) => ({ ...config, providerRetryAdapters: adapters }));
330
+ return `Provider retry adapters set to ${adapters.join(", ") || "none"}\n`;
331
+ },
332
+ clear(store) {
333
+ saveConfigKey(store, (config) => {
334
+ const { providerRetryAdapters: _removed, ...rest } = config;
335
+ return rest;
336
+ });
337
+ return "Provider retry adapters reset to all supported\n";
338
+ }
339
+ },
340
+ {
341
+ key: "provider-retry-max-window-ms",
342
+ showLabel: "Provider retry max window",
343
+ showValue: (config) => `${resolveProviderRetryMaxWindowMs(config.providerRetryMaxWindowMs)} ms`,
344
+ set(args, store) {
345
+ 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`;
350
+ },
351
+ clear(store) {
352
+ saveConfigKey(store, (config) => {
353
+ const { providerRetryMaxWindowMs: _removed, ...rest } = config;
354
+ return rest;
355
+ });
356
+ return "Provider retry max window reset to default\n";
357
+ }
358
+ },
359
+ {
360
+ key: "yield-receipt-replay",
361
+ showLabel: "Yield receipt replay",
362
+ showValue: (config) => (resolveYieldReceiptReplay(config.yieldReceiptReplay) ? "on" : "off"),
363
+ set(args, store) {
364
+ 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`;
369
+ },
370
+ clear(store) {
371
+ saveConfigKey(store, (config) => {
372
+ const { yieldReceiptReplay: _removed, ...rest } = config;
373
+ return rest;
374
+ });
375
+ return "Yield receipt replay reset to on\n";
376
+ }
377
+ },
378
+ {
379
+ key: "tmux-bin",
380
+ showLabel: "Tmux bin",
381
+ showValue: (config) => resolveTmuxBin(config.tmuxBin),
382
+ set(args, store) {
383
+ 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>.");
386
+ saveConfigKey(store, (config) => ({ ...config, tmuxBin }));
387
+ return `Tmux bin set to ${tmuxBin}\n`;
388
+ },
389
+ clear(store) {
390
+ saveConfigKey(store, (config) => {
391
+ const { tmuxBin: _removed, ...rest } = config;
392
+ return rest;
393
+ });
394
+ return "Tmux bin reset to tmux\n";
395
+ }
396
+ },
397
+ {
398
+ key: "git-bin",
399
+ showLabel: "Git bin",
400
+ showValue: (config) => resolveGitBin(config.gitBin),
401
+ set(args, store) {
402
+ 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`;
407
+ },
408
+ clear(store) {
409
+ saveConfigKey(store, (config) => {
410
+ const { gitBin: _removed, ...rest } = config;
411
+ return rest;
412
+ });
413
+ return "Git bin reset to git\n";
414
+ }
415
+ },
416
+ {
417
+ key: "telemetry-mode",
418
+ showLabel: "Telemetry mode",
419
+ showValue: (config) => resolveTelemetryMode(config.telemetryMode),
420
+ set(args, store) {
421
+ 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`;
426
+ },
427
+ clear(store) {
428
+ saveConfigKey(store, (config) => {
429
+ const { telemetryMode: _removed, ...rest } = config;
430
+ return rest;
431
+ });
432
+ return "Telemetry mode reset to legacy\n";
433
+ }
434
+ },
435
+ {
436
+ key: "telemetry-terminal-keep",
437
+ showLabel: "Telemetry terminal keep",
438
+ showValue: (config) => String(resolveTelemetryTerminalKeep(config.telemetryTerminalKeep)),
439
+ set(args, store) {
440
+ 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>.");
443
+ saveConfigKey(store, (config) => ({ ...config, telemetryTerminalKeep }));
444
+ return `Telemetry terminal keep set to ${telemetryTerminalKeep}\n`;
445
+ },
446
+ clear(store) {
447
+ saveConfigKey(store, (config) => {
448
+ const { telemetryTerminalKeep: _removed, ...rest } = config;
449
+ return rest;
450
+ });
451
+ return "Telemetry terminal keep reset to default\n";
452
+ }
453
+ },
454
+ {
455
+ key: "telemetry-run-cap",
456
+ showLabel: "Telemetry run cap",
457
+ showValue: (config) => String(resolveTelemetryRunCap(config.telemetryRunCap)),
458
+ set(args, store) {
459
+ 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>.");
462
+ saveConfigKey(store, (config) => ({ ...config, telemetryRunCap }));
463
+ return `Telemetry run cap set to ${telemetryRunCap}\n`;
464
+ },
465
+ clear(store) {
466
+ saveConfigKey(store, (config) => {
467
+ const { telemetryRunCap: _removed, ...rest } = config;
468
+ return rest;
469
+ });
470
+ return "Telemetry run cap reset to default\n";
471
+ }
472
+ },
473
+ {
474
+ key: "review",
475
+ showLabel: "Review",
476
+ showValue: (config) => (config.review === undefined
477
+ ? "disabled"
478
+ : `${config.review.roleName} (${config.review.trigger}; finding ledger: ${config.review.findingLedger ?? "shadow"}`
479
+ + `${config.review.deltaRecheck === "enabled" ? "; delta recheck: enabled" : ""})`),
480
+ set(args, store) {
481
+ if (args.length < 4 || args.length % 2 !== 0)
482
+ throw usageError(REVIEW_SET_USAGE);
483
+ const options = new Map();
484
+ for (let index = 0; index < args.length; index += 2) {
485
+ const name = args[index];
486
+ const value = args[index + 1];
487
+ if (!["--role", "--trigger", "--finding-ledger", "--delta-recheck",
488
+ "--delta-recheck-max-lines", "--delta-recheck-max-files"].includes(name)
489
+ || value === undefined
490
+ || options.has(name)) {
491
+ throw usageError(REVIEW_SET_USAGE);
492
+ }
493
+ options.set(name, value);
144
494
  }
145
- options.set(name, value);
146
- }
147
- const roleName = options.get("--role")?.trim();
148
- const rawTrigger = options.get("--trigger")?.trim();
149
- if (roleName === undefined || roleName.length === 0
150
- || rawTrigger === undefined
151
- || !REVIEW_TRIGGERS.includes(rawTrigger)) {
152
- throw usageError(usage);
153
- }
154
- if (store.getGlobalRole?.(roleName) === null) {
155
- throw usageError(`Global Role not found: ${roleName}.`);
156
- }
157
- const trigger = rawTrigger;
158
- const rawLedgerMode = options.get("--finding-ledger")?.trim();
159
- let findingLedger;
160
- if (rawLedgerMode !== undefined) {
161
- if (!REVIEW_FINDING_LEDGER_MODES.includes(rawLedgerMode)) {
162
- throw usageError(usage);
495
+ const roleName = options.get("--role")?.trim();
496
+ const rawTrigger = options.get("--trigger")?.trim();
497
+ if (roleName === undefined || roleName.length === 0
498
+ || rawTrigger === undefined
499
+ || !REVIEW_TRIGGERS.includes(rawTrigger)) {
500
+ throw usageError(REVIEW_SET_USAGE);
163
501
  }
164
- findingLedger = rawLedgerMode;
165
- }
166
- store.transaction((tx) => {
167
- tx.saveConfig({
168
- ...tx.getConfig(),
502
+ if (store.getGlobalRole?.(roleName) === null) {
503
+ throw usageError(`Global Role not found: ${roleName}.`);
504
+ }
505
+ const trigger = rawTrigger;
506
+ const rawLedgerMode = options.get("--finding-ledger")?.trim();
507
+ let findingLedger;
508
+ if (rawLedgerMode !== undefined) {
509
+ if (!REVIEW_FINDING_LEDGER_MODES.includes(rawLedgerMode)) {
510
+ throw usageError(REVIEW_SET_USAGE);
511
+ }
512
+ findingLedger = rawLedgerMode;
513
+ }
514
+ const rawDeltaRecheck = options.get("--delta-recheck")?.trim();
515
+ let deltaRecheck;
516
+ if (rawDeltaRecheck !== undefined) {
517
+ if (!REVIEW_DELTA_RECHECK_MODES.includes(rawDeltaRecheck)) {
518
+ throw usageError(REVIEW_SET_USAGE);
519
+ }
520
+ deltaRecheck = rawDeltaRecheck;
521
+ }
522
+ const deltaRecheckMaxChangedLines = parsePositiveIntegerOption(options.get("--delta-recheck-max-lines"), REVIEW_SET_USAGE);
523
+ const deltaRecheckMaxChangedFiles = parsePositiveIntegerOption(options.get("--delta-recheck-max-files"), REVIEW_SET_USAGE);
524
+ saveConfigKey(store, (config) => ({
525
+ ...config,
169
526
  review: {
170
527
  roleName,
171
528
  trigger,
172
- ...(findingLedger === undefined ? {} : { findingLedger })
529
+ ...(findingLedger === undefined ? {} : { findingLedger }),
530
+ ...(deltaRecheck === undefined ? {} : { deltaRecheck }),
531
+ ...(deltaRecheckMaxChangedLines === undefined
532
+ ? {}
533
+ : { deltaRecheckMaxChangedLines }),
534
+ ...(deltaRecheckMaxChangedFiles === undefined
535
+ ? {}
536
+ : { deltaRecheckMaxChangedFiles })
173
537
  }
538
+ }));
539
+ return `Review set to ${roleName} (${trigger}; finding ledger: ${findingLedger ?? "shadow"}; `
540
+ + `delta recheck: ${deltaRecheck ?? "disabled"})\n`;
541
+ },
542
+ clear(store) {
543
+ saveConfigKey(store, (config) => {
544
+ const { review: _removed, ...rest } = config;
545
+ return rest;
174
546
  });
175
- });
176
- return `Review set to ${roleName} (${trigger}; finding ledger: ${findingLedger ?? "shadow"})\n`;
177
- }
178
- throw usageError(command === undefined
179
- ? "Config review command is required."
180
- : `Unknown command: config review ${command}`);
181
- }
182
- function parseReconciliationIntervalSeconds(value) {
183
- if (!/^\d+$/.test(value))
184
- return Number.NaN;
185
- return Number(value);
186
- }
187
- function validatedConfigValue(validate) {
188
- try {
189
- return validate();
190
- }
191
- catch (error) {
192
- if (error instanceof TypeError) {
193
- throw usageError(error.message, CONFIG_SET_USAGE);
547
+ return "Review disabled\n";
194
548
  }
195
- throw error;
196
549
  }
197
- }
198
- function configSetUsageError() {
199
- return usageError(CONFIG_SET_USAGE);
200
- }
550
+ ];