@zq-silk/yui 0.7.1 → 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 (47) 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/doctor/doctor.js +2 -2
  28. package/dist/resources/autoResourceGc.js +3 -1
  29. package/dist/review/reviewConfig.js +0 -2
  30. package/dist/run/providerRetry.js +29 -16
  31. package/dist/run/providerRetryConfig.js +5 -3
  32. package/dist/runtime/launchDiagnostics.js +1 -1
  33. package/dist/scheduler/roleRunStall.js +12 -9
  34. package/dist/setup/setupCommand.js +153 -492
  35. package/dist/storage/compatibleTaskStore.js +9 -5
  36. package/dist/storage/migration/productionRegistry.js +58 -0
  37. package/dist/storage/taskStore.js +21 -2
  38. package/dist/telemetry/sqliteTelemetryStore.js +9 -1
  39. package/dist/telemetry/telemetryConfig.js +1 -18
  40. package/dist/telemetry/telemetryStore.js +2 -2
  41. package/dist/telemetry/telemetryWiring.js +6 -5
  42. package/dist/web/webSnapshot.js +5 -3
  43. package/i18n/README.zh-CN.md +37 -32
  44. package/package.json +1 -1
  45. package/skills/yui-leader/SKILL.md +12 -5
  46. package/skills/yui-operator/SKILL.md +44 -6
  47. package/skills/yui-runtime/SKILL.md +1 -1
@@ -1,22 +1,14 @@
1
1
  import { existsSync, mkdirSync, realpathSync } from "node:fs";
2
2
  import { delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { createInterface } from "node:readline/promises";
4
- import { isDeepStrictEqual } from "node:util";
5
4
  import { configuredAgentToDefinition, createConfiguredAgent } from "../agent/agent.js";
6
- import { selectAgentPermission, selectAgentModelAndEffort } from "../cli/agentConfigurationPicker.js";
7
- import { runCompletionWizard } from "../completion/completionWizard.js";
5
+ import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
6
+ import { resolveTmuxBin } from "../config/yuiConfig.js";
8
7
  import { usageError } from "../errors/cliError.js";
9
- import { defaultRoleAgentConfig, resolveAgentAdapter } from "../executor/agentAdapter.js";
10
- import { AgentConfigurationCatalogService } from "../executor/agentConfigurationCatalog.js";
11
- import { defaultTableWidth, renderTable } from "../output/table.js";
12
- import { resolveTimeZone } from "../output/timePresentation.js";
13
8
  import { createGlobalRole, createRoleAgentBinding, updateGlobalRole } from "../role/role.js";
14
- import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE, SYSTEM_WORKER_ROLE } from "../role/systemRoles.js";
15
- import { DEFAULT_REVIEWER_ROLE } from "../review/reviewConfig.js";
16
- import { builtinAgentProfileInputs, createAgentProfile } from "../profile/agentProfile.js";
17
- import { assertRoleRuntimeMutationAllowed } from "../commands/roleRuntimeGuard.js";
9
+ import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
10
+ import { initializeCompatibleTaskStore } from "../storage/compatibleTaskStore.js";
18
11
  import { ensureYuiHome, resolveYuiHome } from "../storage/taskStore.js";
19
- import { initializeCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
20
12
  const BUILTIN_AGENTS = Object.freeze([
21
13
  Object.freeze({
22
14
  id: "codex",
@@ -32,7 +24,7 @@ const BUILTIN_AGENTS = Object.freeze([
32
24
  })
33
25
  ]);
34
26
  export async function runSetupCommand(args, env, executor, io = {}) {
35
- const dependency = parseSetupOptions(args);
27
+ parseSetupOptions(args);
36
28
  if (!shouldPrompt(io))
37
29
  throw setupRequiresInteractiveError();
38
30
  const readline = createInterface({
@@ -44,48 +36,32 @@ export async function runSetupCommand(args, env, executor, io = {}) {
44
36
  const question = createSetupQuestion(readline, io);
45
37
  const home = resolveYuiHome(env);
46
38
  ensureYuiHome(home);
47
- const store = initializeCompatibleFileTaskStore(home);
48
- const catalogs = new AgentConfigurationCatalogService(home, { environment: env });
49
- const result = await configureYui(store, home, env, question, setupSelectionIo(question, io), catalogs, io);
50
- const lines = [
51
- "Yui home initialized.",
52
- `Agents configured: ${result.agentIds.join(", ")}.`,
53
- `Default Agent: ${result.defaultAgentId}.`,
54
- `Operator Agent: ${result.operatorAgentId}.`,
55
- `Leader model: ${result.leaderConfig.model ?? "CLI default"}.`,
56
- `Leader reasoning effort: ${result.leaderConfig.effort ?? "CLI default"}.`,
57
- `Leader permission: ${result.leaderConfig.permission.strategy}.`,
58
- `Operator model: ${result.operatorConfig.model ?? "CLI default"}.`,
59
- `Operator reasoning effort: ${result.operatorConfig.effort ?? "CLI default"}.`,
60
- `Operator permission: ${result.operatorConfig.permission.strategy}.`,
61
- `Worker Agent: ${result.workerAgentId}.`,
62
- `Worker configuration: ${result.workerReusesLeader
63
- ? "Reused Leader configuration"
64
- : "Configured separately"}.`,
65
- `Worker model: ${result.workerConfig.model ?? "CLI default"}.`,
66
- `Worker reasoning effort: ${result.workerConfig.effort ?? "CLI default"}.`,
67
- `Worker permission: ${result.workerConfig.permission.strategy}.`,
68
- ...(result.reviewerInitialized
69
- ? [
70
- `Reviewer Agent: ${result.reviewerAgentId}.`,
71
- `Reviewer model: ${result.reviewerConfig?.model ?? "CLI default"}.`,
72
- `Reviewer reasoning effort: ${result.reviewerConfig?.effort ?? "CLI default"}.`,
73
- `Reviewer permission: ${result.reviewerConfig?.permission.strategy}.`,
74
- ...(result.reviewPolicy === undefined
75
- ? ["Review policy: disabled."]
76
- : [`Review policy: ${result.reviewPolicy.roleName} (${result.reviewPolicy.trigger}).`])
77
- ]
78
- : []),
79
- `Project workspace: ${result.workspace}.`,
80
- `Time zone: ${resolveTimeZone(store.getConfig().timeZone)}.`
81
- ];
82
- if (dependency === undefined || dependency === "tmux") {
83
- lines.push(...await setupTmux(env, executor, question));
39
+ const store = initializeCompatibleTaskStore(home);
40
+ const tmuxBin = resolveTmuxBin(store.getConfig().tmuxBin);
41
+ assertTmuxAvailable(executor, tmuxBin);
42
+ const choices = availableAgentChoices(store, env);
43
+ if (choices.length === 0) {
44
+ throw usageError("No usable Agent configuration was found. Setup never overwrites an existing Agent id; "
45
+ + "repair it with yui config agent update, add another usable Agent with yui config agent add, "
46
+ + "or install Codex or Claude, then run yui setup again.");
84
47
  }
85
- const completion = await runCompletionWizard("install", store, env, cliIdentity(env), question, { width: tableWidth(io), defaultSelection: "skip" });
86
- lines.push(completion.trimEnd());
87
- lines.push("Yui setup complete.");
88
- return `${lines.join("\n")}\n`;
48
+ const currentOperator = store.getGlobalRole(SYSTEM_OPERATOR_ROLE);
49
+ const selected = await selectOperatorAgent(choices, currentOperator, store.getConfig().defaultAgent, question, io);
50
+ const agent = selected.existing ?? createConfiguredAgent(selected.id, selected.adapterId, selected.command, [], [], new Date());
51
+ const workspace = resolveWorkspace(store.getConfig().defaultWorkspace ?? join(dirname(resolve(home)), "workspace"), home);
52
+ const roleStatus = saveMinimumConfiguration(store, agent, workspace, new Set(choices.map(({ id }) => id)));
53
+ return `${[
54
+ "Yui setup complete.",
55
+ `Yui home: ${home}.`,
56
+ `Operator Agent: ${agent.id}.`,
57
+ `Leader Agent: ${roleStatus.leaderAgentId}.`,
58
+ `Default workspace: ${workspace}.`,
59
+ `Operator configuration: ${roleStatus.operator}.`,
60
+ `Leader configuration: ${roleStatus.leader}.`,
61
+ `Tmux: ${tmuxBin}.`,
62
+ "Setup did not configure Review, Worker, Profiles, or shell completion.",
63
+ "Run `yui operator enter` to continue, or `yui config show` to inspect configuration."
64
+ ].join("\n")}\n`;
89
65
  }
90
66
  finally {
91
67
  readline.close();
@@ -96,371 +72,129 @@ export function validateSetupInvocation(args, io = {}) {
96
72
  if (!shouldPrompt(io))
97
73
  throw setupRequiresInteractiveError();
98
74
  }
99
- async function configureYui(store, home, env, question, selectionIo, catalogs, io) {
100
- const initialConfig = store.getConfig();
101
- const freshHome = store.listGlobalRoles().length === 0
102
- && store.listConfiguredAgents().length === 0
103
- && initialConfig.defaultAgent === undefined
104
- && initialConfig.defaultWorkspace === undefined
105
- && initialConfig.review === undefined;
106
- const candidates = availableAgentChoices(store, env);
107
- if (candidates.length === 0) {
108
- throw usageError("No supported Agent CLI was found. Install Codex or Claude, then run setup again.");
109
- }
110
- io.output?.write(`${renderTable("Available Agents", [
111
- { header: "#", minWidth: 1, maxWidth: 3 },
112
- { header: "Agent", minWidth: 6, maxWidth: 16 },
113
- { header: "Adapter", minWidth: 7, maxWidth: 10 },
114
- { header: "Command", minWidth: 7, maxWidth: 24 },
115
- { header: "Description", minWidth: 12, maxWidth: 42 }
116
- ], candidates.map((candidate, index) => [
117
- String(index + 1),
118
- candidate.id,
119
- candidate.adapterId,
120
- candidate.command,
121
- candidate.description
122
- ]), tableWidth(io))}\n`);
123
- const selected = parseAgentSetSelection(await question(`Choose Agents by number or name, comma-separated [all: ${candidates.map(({ id }) => id).join(", ")}]: `), candidates);
124
- const now = new Date();
125
- const prepared = selected.map((choice) => prepareAgent(store, choice, now));
126
- const configuredIds = new Set(prepared.map(({ id }) => id));
127
- const config = store.getConfig();
128
- const defaultFallback = prepared.some(({ id }) => id === config.defaultAgent)
129
- ? config.defaultAgent
130
- : prepared[0].id;
131
- const defaultAgentId = parseSingleAgentSelection(await question(`Choose default Agent [${defaultFallback}]: `), prepared, defaultFallback);
132
- const currentOperatorAgent = store.getGlobalRole(SYSTEM_OPERATOR_ROLE)?.activeAgentId;
133
- const operatorFallback = configuredIds.has(currentOperatorAgent ?? "")
134
- ? currentOperatorAgent
135
- : defaultAgentId;
136
- const operatorAgentId = parseSingleAgentSelection(await question(`Choose Operator Agent [${operatorFallback}]: `), prepared, operatorFallback);
137
- const existingReviewer = store.getGlobalRole(DEFAULT_REVIEWER_ROLE);
138
- const reviewerInitialized = freshHome || existingReviewer !== null;
139
- let reviewerAgentId;
140
- let reviewerConfig;
141
- if (reviewerInitialized) {
142
- const reviewerFallback = configuredIds.has(existingReviewer?.activeAgentId ?? "")
143
- ? existingReviewer.activeAgentId
144
- : defaultAgentId;
145
- reviewerAgentId = parseSingleAgentSelection(await question(`Choose Reviewer Agent [${reviewerFallback}]: `), prepared, reviewerFallback);
146
- const reviewerAgent = prepared.find(({ id }) => id === reviewerAgentId);
147
- if (reviewerAgent === undefined) {
148
- throw usageError("Selected Reviewer Agent is no longer available.");
149
- }
150
- reviewerConfig = await promptRoleAgentConfig("Reviewer", reviewerAgent, existingReviewer, home, selectionIo, catalogs, true);
151
- }
152
- const defaultAgent = prepared.find(({ id }) => id === defaultAgentId);
153
- const operatorAgent = prepared.find(({ id }) => id === operatorAgentId);
154
- if (defaultAgent === undefined || operatorAgent === undefined) {
155
- throw usageError("Selected setup Agent is no longer available.");
156
- }
157
- const leaderConfig = await promptRoleAgentConfig("Leader", defaultAgent, store.getGlobalRole(SYSTEM_LEADER_ROLE), home, selectionIo, catalogs);
158
- const operatorConfig = await promptRoleAgentConfig("Operator", operatorAgent, store.getGlobalRole(SYSTEM_OPERATOR_ROLE), home, selectionIo, catalogs);
159
- const existingWorker = store.getGlobalRole(SYSTEM_WORKER_ROLE);
160
- const workerModeFallback = workerConfigurationModeFallback(existingWorker, defaultAgentId, leaderConfig);
161
- io.output?.write("\n\nWorker is the default Agent configuration copied into Task Roles such as "
162
- + "investigator and implementer. Each Task Role gets its own Session.\n");
163
- const workerReusesLeader = await selectWorkerConfigurationMode(question, io, workerModeFallback) === "reuse-leader";
164
- let workerAgentId = defaultAgentId;
165
- let workerConfig = structuredClone(leaderConfig);
166
- if (!workerReusesLeader) {
167
- io.output?.write("\n\nConfigure Worker separately:\n");
168
- const existingWorkerAgent = existingWorker?.activeAgentId;
169
- const workerFallback = configuredIds.has(existingWorkerAgent ?? "")
170
- ? existingWorkerAgent
171
- : defaultAgentId;
172
- workerAgentId = parseSingleAgentSelection(await question(`Choose Worker Agent [${workerFallback}]: `), prepared, workerFallback);
173
- const workerAgent = prepared.find(({ id }) => id === workerAgentId);
174
- if (workerAgent === undefined) {
175
- throw usageError("Selected Worker Agent is no longer available.");
176
- }
177
- workerConfig = await promptRoleAgentConfig("Worker", workerAgent, existingWorker, home, selectionIo, catalogs, true);
178
- }
179
- const suggestedWorkspace = config.defaultWorkspace?.trim()
180
- || join(dirname(resolve(home)), "workspace");
181
- io.output?.write("\n\n");
182
- const workspaceAnswer = (await question(`Project workspace for stable checkouts and managed worktrees [${suggestedWorkspace}]: `)).trim();
183
- const workspace = resolveWorkspace(workspaceAnswer || suggestedWorkspace, home);
184
- if (config.defaultWorkspace !== undefined
185
- && resolve(config.defaultWorkspace) !== workspace) {
186
- throw usageError(`Project workspace is fixed after setup: ${resolve(config.defaultWorkspace)}.`);
187
- }
188
- store.transaction((tx) => {
189
- for (const agent of prepared)
190
- tx.saveConfiguredAgent(agent);
191
- const latestDefaultAgent = requireSetupAgent(tx, defaultAgentId);
192
- const latestOperatorAgent = requireSetupAgent(tx, operatorAgentId);
193
- const latestWorkerAgent = requireSetupAgent(tx, workerAgentId);
194
- const operatorRole = prepareSystemRole(tx, SYSTEM_OPERATOR_ROLE, latestOperatorAgent, workspace, now, operatorConfig);
195
- const leaderRole = prepareSystemRole(tx, SYSTEM_LEADER_ROLE, latestDefaultAgent, workspace, now, leaderConfig);
196
- const workerRole = prepareSystemRole(tx, SYSTEM_WORKER_ROLE, latestWorkerAgent, workspace, now, workerConfig);
197
- const reviewerRole = reviewerInitialized
198
- && reviewerAgentId !== undefined
199
- && reviewerConfig !== undefined
200
- ? prepareSystemRole(tx, DEFAULT_REVIEWER_ROLE, requireSetupAgent(tx, reviewerAgentId), workspace, now, reviewerConfig, freshHome ? reviewerRoleProfile() : undefined)
201
- : null;
202
- const latest = tx.getConfig();
203
- tx.saveConfig({
204
- ...latest,
205
- defaultAgent: defaultAgentId,
206
- defaultWorkspace: workspace,
207
- timeZone: resolveTimeZone(latest.timeZone),
208
- ...(freshHome
209
- ? { review: { roleName: DEFAULT_REVIEWER_ROLE, trigger: "final" } }
210
- : {})
75
+ function availableAgentChoices(store, env) {
76
+ const choices = new Map();
77
+ const configured = store.listConfiguredAgents();
78
+ const configuredIds = new Set(configured.map(({ id }) => id));
79
+ for (const agent of configured) {
80
+ if (!commandOnPath(agent.command, env))
81
+ continue;
82
+ choices.set(agent.id, {
83
+ id: agent.id,
84
+ adapterId: agent.adapterId,
85
+ command: agent.command,
86
+ description: "Existing Yui Agent",
87
+ existing: agent
211
88
  });
212
- if (operatorRole !== null)
213
- savePreparedSystemRole(tx, operatorRole, now);
214
- if (leaderRole !== null)
215
- savePreparedSystemRole(tx, leaderRole, now);
216
- if (workerRole !== null)
217
- savePreparedSystemRole(tx, workerRole, now);
218
- if (reviewerRole !== null)
219
- savePreparedSystemRole(tx, reviewerRole, now);
220
- seedBuiltinProfiles(tx, now);
221
- });
222
- return {
223
- agentIds: prepared.map(({ id }) => id),
224
- defaultAgentId,
225
- operatorAgentId,
226
- leaderConfig,
227
- operatorConfig,
228
- workerAgentId,
229
- workerConfig,
230
- workerReusesLeader,
231
- reviewerInitialized,
232
- ...(reviewerAgentId === undefined ? {} : { reviewerAgentId }),
233
- ...(reviewerConfig === undefined ? {} : { reviewerConfig }),
234
- ...(freshHome
235
- ? { reviewPolicy: { roleName: DEFAULT_REVIEWER_ROLE, trigger: "final" } }
236
- : initialConfig.review === undefined ? {} : { reviewPolicy: initialConfig.review }),
237
- workspace
238
- };
239
- }
240
- function reviewerRoleProfile() {
241
- const reviewer = builtinAgentProfileInputs().find(({ id }) => id === "reviewer");
242
- if (reviewer === undefined)
243
- return {};
244
- return {
245
- ...(reviewer.description === undefined ? {} : { description: reviewer.description }),
246
- ...(reviewer.instructions === undefined ? {} : { systemPrompt: reviewer.instructions }),
247
- ...(reviewer.skills === undefined ? {} : { skills: [...reviewer.skills] })
248
- };
249
- }
250
- async function selectWorkerConfigurationMode(question, io, fallback) {
251
- const choices = [
252
- [
253
- "1",
254
- `Reuse Leader${fallback === "reuse-leader" ? " (default)" : ""}`,
255
- "Copy the complete Leader Agent launch configuration; skip Worker-specific prompts"
256
- ],
257
- [
258
- "2",
259
- `Configure separately${fallback === "configure-separately" ? " (default)" : ""}`,
260
- "Choose the Worker Agent, model, effort, and permission independently"
261
- ]
262
- ];
263
- const fallbackNumber = fallback === "reuse-leader" ? "1" : "2";
264
- io.output?.write(`\n${renderTable("Choose Worker configuration", [
265
- { header: "#", minWidth: 1, maxWidth: 4 },
266
- { header: "Choice", minWidth: 16, maxWidth: 28 },
267
- { header: "Consequence", minWidth: 24, maxWidth: 64 }
268
- ], choices, tableWidth(io))}\n\n`);
269
- return parseWorkerConfigurationMode(await question(`Choose Worker configuration [1-2; default ${fallbackNumber}]: `), fallback);
270
- }
271
- function workerConfigurationModeFallback(existing, leaderAgentId, leaderConfig) {
272
- if (existing === null)
273
- return "reuse-leader";
274
- return existing.activeAgentId === leaderAgentId
275
- && isDeepStrictEqual(existing.agentBindings[leaderAgentId]?.config, leaderConfig)
276
- ? "reuse-leader"
277
- : "configure-separately";
278
- }
279
- function parseWorkerConfigurationMode(answer, fallback) {
280
- const value = answer.trim().toLowerCase();
281
- if (value.length === 0)
282
- return fallback;
283
- if (["1", "reuse", "reuse leader", "leader"].includes(value))
284
- return "reuse-leader";
285
- if (["2", "configure", "configure separately", "separate"].includes(value)) {
286
- return "configure-separately";
287
- }
288
- throw usageError("Choose reuse Leader or configure separately for Worker.");
289
- }
290
- function savePreparedSystemRole(store, role, now) {
291
- const sessions = store.getGlobalRoleSessionSet(role.name);
292
- const activeSession = sessions?.sessions[sessions.activeAgentId];
293
- if (sessions === null
294
- || sessions.activeAgentId === role.activeAgentId
295
- || (activeSession !== undefined && activeSession.status !== "stopped")) {
296
- store.saveGlobalRole(role);
297
- return;
298
89
  }
299
- store.saveGlobalRoleWithSessionSet(role, {
300
- ...sessions,
301
- activeAgentId: role.activeAgentId,
302
- updatedAt: now.toISOString()
303
- });
304
- }
305
- function seedBuiltinProfiles(store, now) {
306
- for (const desired of builtinAgentProfileInputs()) {
307
- const existing = store.getAgentProfile(desired.id);
308
- if (existing === null)
309
- store.saveAgentProfile(createAgentProfile(desired, now));
90
+ for (const builtin of BUILTIN_AGENTS) {
91
+ if (configuredIds.has(builtin.id) || !commandOnPath(builtin.command, env))
92
+ continue;
93
+ choices.set(builtin.id, builtin);
94
+ }
95
+ return [...choices.values()].sort((left, right) => left.id.localeCompare(right.id));
96
+ }
97
+ async function selectOperatorAgent(choices, currentOperator, defaultAgent, question, io) {
98
+ const current = choices.find(({ id }) => id === currentOperator?.activeAgentId);
99
+ if (current !== undefined)
100
+ return current;
101
+ const configuredDefault = choices.find(({ id }) => id === defaultAgent);
102
+ if (configuredDefault !== undefined)
103
+ return configuredDefault;
104
+ if (choices.length === 1)
105
+ return choices[0];
106
+ io.output?.write([
107
+ "Available Operator Agents:",
108
+ ...choices.map((choice, index) => ` ${index + 1}. ${choice.id} (${choice.adapterId}) - ${choice.description}`),
109
+ ""
110
+ ].join("\n"));
111
+ const answer = (await question(`Choose Operator Agent [1: ${choices[0].id}]: `)).trim();
112
+ if (answer.length === 0)
113
+ return choices[0];
114
+ const numeric = Number(answer);
115
+ const selected = Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= choices.length
116
+ ? choices[numeric - 1]
117
+ : choices.find(({ id }) => id.toLowerCase() === answer.toLowerCase());
118
+ if (selected === undefined) {
119
+ throw usageError("Choose one available Agent by number or name.");
310
120
  }
121
+ return selected;
311
122
  }
312
- async function promptRoleAgentConfig(label, agent, existingRole, cwd, io, catalogs, selectPermission = false) {
313
- const existing = existingRole?.activeAgentId === agent.id
314
- ? existingRole.agentBindings[agent.id]?.config
315
- : undefined;
316
- io.write(`\n\n${label} Agent configuration: ${agent.id}\n`);
317
- const resolved = await catalogs.resolve({
318
- agent,
319
- cwd,
320
- ...(existing === undefined ? {} : { config: existing })
321
- });
322
- const selection = await selectAgentModelAndEffort(resolved, io, {
323
- currentModel: existing?.model,
324
- currentEffort: existing?.effort
325
- });
326
- if (selection.kind === "cancelled") {
327
- throw usageError(`${label} Agent configuration was cancelled.`);
328
- }
329
- const candidate = structuredClone(existing ?? defaultRoleAgentConfig(agent.adapterId));
330
- if (selection.model === undefined)
331
- delete candidate.model;
332
- else
333
- candidate.model = selection.model;
334
- if (selection.effort === undefined)
335
- delete candidate.effort;
336
- else
337
- candidate.effort = selection.effort;
338
- if (selectPermission) {
339
- const permission = await selectAgentPermission(resolved, io, candidate.permission);
340
- if (permission.kind === "cancelled") {
341
- throw usageError(`${label} permission configuration was cancelled.`);
123
+ function saveMinimumConfiguration(store, agent, workspace, usableAgentIds) {
124
+ return store.transaction((tx) => {
125
+ tx.saveConfiguredAgent(agent);
126
+ const config = tx.getConfig();
127
+ tx.saveConfig({
128
+ ...config,
129
+ defaultAgent: config.defaultAgent ?? agent.id,
130
+ defaultWorkspace: config.defaultWorkspace ?? workspace
131
+ });
132
+ const now = new Date();
133
+ const leader = tx.getGlobalRole(SYSTEM_LEADER_ROLE);
134
+ let leaderStatus = "preserved";
135
+ let leaderAgentId = leader?.activeAgentId;
136
+ if (leader === null) {
137
+ assertRoleRuntimeMutationAllowed(tx, {
138
+ scope: "global",
139
+ roleName: SYSTEM_LEADER_ROLE
140
+ }, "creation");
141
+ const definition = configuredAgentToDefinition(agent);
142
+ tx.saveGlobalRole(createGlobalRole(SYSTEM_LEADER_ROLE, [createRoleAgentBinding(definition)], agent.id, workspace, now));
143
+ leaderStatus = "created";
144
+ leaderAgentId = agent.id;
342
145
  }
343
- candidate.permission = permission.permission;
344
- }
345
- try {
346
- return resolveAgentAdapter(agent.adapterId).canonicalizeConfig(candidate);
347
- }
348
- catch (error) {
349
- throw usageError(error instanceof Error ? error.message : String(error));
350
- }
351
- }
352
- function availableAgentChoices(store, env) {
353
- const existing = new Map(store.listConfiguredAgents().map((agent) => [agent.id, agent]));
354
- return BUILTIN_AGENTS.flatMap((builtin) => {
355
- const configured = existing.get(builtin.id);
356
- const command = configured?.command ?? builtin.command;
357
- if (!commandOnPath(command, env))
358
- return [];
359
- return [{
360
- ...builtin,
361
- adapterId: configured?.adapterId ?? builtin.adapterId,
362
- command
363
- }];
364
- });
365
- }
366
- function setupSelectionIo(question, io) {
367
- return {
368
- interactive: true,
369
- json: false,
370
- width: tableWidth(io),
371
- write: (value) => { io.output.write(value); },
372
- question
373
- };
374
- }
375
- function prepareAgent(store, choice, now) {
376
- const existing = store.getConfiguredAgent(choice.id);
377
- if (existing !== null
378
- && existing.adapterId === choice.adapterId
379
- && existing.command === choice.command) {
380
- return existing;
381
- }
382
- const agent = createConfiguredAgent(choice.id, choice.adapterId, choice.command, existing?.baseArgs ?? [], existing?.environment ?? [], now);
383
- return agent;
384
- }
385
- function requireSetupAgent(store, agentId) {
386
- const agent = store.getConfiguredAgent(agentId);
387
- if (agent === null)
388
- throw usageError(`Configured Agent not found: ${agentId}.`);
389
- return agent;
390
- }
391
- function prepareSystemRole(store, name, agent, workspace, now, config, profile) {
392
- const existing = store.getGlobalRole(name);
393
- if (existing !== null) {
394
- const definition = configuredAgentToDefinition(agent);
395
- const binding = createRoleAgentBinding(definition, config);
396
- if (name === "operator" && !Object.hasOwn(existing.agentBindings, agent.id)) {
397
- const sameAdapter = Object.values(existing.agentBindings).find((candidate) => candidate.adapterId === agent.adapterId);
398
- if (sameAdapter !== undefined) {
399
- throw usageError(`${name} already has a ${agent.adapterId} Agent: ${sameAdapter.agentId}. `
400
- + "Update that Agent's configuration, or activate another adapter and "
401
- + "unbind it before selecting this Agent.");
402
- }
146
+ if (leaderAgentId === undefined) {
147
+ throw usageError("Leader has no active Agent. Repair it with yui config role update leader, then run yui setup again.");
403
148
  }
404
- if (existing.activeAgentId === agent.id
405
- && existing.workspace === workspace
406
- && isDeepStrictEqual(existing.agentBindings[agent.id], binding)) {
407
- return null;
149
+ if (!usableAgentIds.has(leaderAgentId)) {
150
+ throw usageError(`Leader Agent ${leaderAgentId} is unavailable. Repair it with yui config agent update `
151
+ + "or yui config role update leader, then run yui setup again.");
152
+ }
153
+ const operator = tx.getGlobalRole(SYSTEM_OPERATOR_ROLE);
154
+ if (operator !== null && operator.activeAgentId === agent.id) {
155
+ return {
156
+ operator: "preserved",
157
+ leader: leaderStatus,
158
+ leaderAgentId
159
+ };
160
+ }
161
+ const definition = configuredAgentToDefinition(agent);
162
+ const binding = createRoleAgentBinding(definition);
163
+ if (operator === null) {
164
+ assertRoleRuntimeMutationAllowed(tx, {
165
+ scope: "global",
166
+ roleName: SYSTEM_OPERATOR_ROLE
167
+ }, "creation");
168
+ tx.saveGlobalRole(createGlobalRole(SYSTEM_OPERATOR_ROLE, [binding], agent.id, workspace, now));
169
+ return {
170
+ operator: "created",
171
+ leader: leaderStatus,
172
+ leaderAgentId
173
+ };
408
174
  }
409
- assertRoleRuntimeMutationAllowed(store, {
175
+ assertRoleRuntimeMutationAllowed(tx, {
410
176
  scope: "global",
411
- roleName: name
412
- }, "desired launch configuration update");
413
- return updateGlobalRole(existing, {
414
- activeAgentId: agent.id,
415
- workspace,
416
- agentBindings: { ...existing.agentBindings, [agent.id]: binding },
417
- ...(profile === undefined ? {} : profile)
418
- }, now);
419
- }
420
- assertRoleRuntimeMutationAllowed(store, {
421
- scope: "global",
422
- roleName: name
423
- }, "creation");
424
- const definition = configuredAgentToDefinition(agent);
425
- return createGlobalRole(name, [createRoleAgentBinding(definition, config)], definition.id, workspace, now, profile);
426
- }
427
- function parseAgentSetSelection(answer, candidates) {
428
- const value = answer.trim().toLowerCase();
429
- if (value.length === 0 || value === "all")
430
- return [...candidates];
431
- const tokens = value.split(",").map((token) => token.trim()).filter(Boolean);
432
- if (tokens.length === 0)
433
- throw usageError("Select at least one Agent.");
434
- const selected = [];
435
- for (const token of tokens) {
436
- const numeric = Number(token);
437
- const choice = Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= candidates.length
438
- ? candidates[numeric - 1]
439
- : candidates.find(({ id }) => id.toLowerCase() === token);
440
- if (choice === undefined) {
441
- throw usageError("Choose available Agents by number or name, separated by commas.");
177
+ roleName: SYSTEM_OPERATOR_ROLE
178
+ }, "desired Agent binding update");
179
+ const duplicateAdapter = Object.values(operator.agentBindings).find((candidate) => candidate.adapterId === agent.adapterId && candidate.agentId !== agent.id);
180
+ if (duplicateAdapter !== undefined) {
181
+ throw usageError(`Operator already has a ${agent.adapterId} Agent: ${duplicateAdapter.agentId}. `
182
+ + "Use yui config agent update or yui config role bind to repair the Operator configuration.");
442
183
  }
443
- if (!selected.some(({ id }) => id === choice.id))
444
- selected.push(choice);
445
- }
446
- return selected;
447
- }
448
- function parseSingleAgentSelection(answer, agents, fallback) {
449
- const value = answer.trim().toLowerCase();
450
- if (value.length === 0)
451
- return fallback;
452
- const numeric = Number(value);
453
- const selected = Number.isSafeInteger(numeric) && numeric >= 1 && numeric <= agents.length
454
- ? agents[numeric - 1]
455
- : agents.find(({ id }) => id.toLowerCase() === value);
456
- if (selected === undefined) {
457
- throw usageError("Choose one of the configured Agents by number or name.");
458
- }
459
- return selected.id;
184
+ tx.saveGlobalRole(updateGlobalRole(operator, {
185
+ activeAgentId: agent.id,
186
+ agentBindings: { ...operator.agentBindings, [agent.id]: binding }
187
+ }, now));
188
+ return {
189
+ operator: "updated",
190
+ leader: leaderStatus,
191
+ leaderAgentId
192
+ };
193
+ });
460
194
  }
461
195
  function resolveWorkspace(value, home) {
462
196
  if (!isAbsolute(value))
463
- throw usageError("Project workspace must be an absolute path.");
197
+ throw usageError("Default workspace must be an absolute path.");
464
198
  const requested = resolve(value);
465
199
  assertWorkspaceOutsideHome(requested, resolve(home));
466
200
  mkdirSync(requested, { recursive: true, mode: 0o700 });
@@ -472,7 +206,7 @@ function resolveWorkspace(value, home) {
472
206
  function assertWorkspaceOutsideHome(workspace, homeRoot) {
473
207
  const fromHome = relative(homeRoot, workspace);
474
208
  if (fromHome === "" || (!fromHome.startsWith("..") && !isAbsolute(fromHome))) {
475
- throw usageError("Project workspace must be outside YUI_HOME.");
209
+ throw usageError("Default workspace must be outside YUI_HOME.");
476
210
  }
477
211
  }
478
212
  function commandOnPath(command, env) {
@@ -484,6 +218,15 @@ function commandOnPath(command, env) {
484
218
  : [""];
485
219
  return entries.some((entry) => extensions.some((extension) => existsSync(join(entry, `${command}${extension}`))));
486
220
  }
221
+ function assertTmuxAvailable(executor, tmuxBin) {
222
+ try {
223
+ executor.run(tmuxBin, ["-V"], { timeoutMs: 3_000 });
224
+ }
225
+ catch {
226
+ throw usageError(`Tmux is required to run the Operator, but ${tmuxBin} is unavailable. `
227
+ + "Install tmux or set yui config tools set tmux-bin, then run yui setup again.");
228
+ }
229
+ }
487
230
  function createSetupQuestion(readline, io) {
488
231
  if (io.input.isTTY === true)
489
232
  return (prompt) => readline.question(prompt);
@@ -502,89 +245,7 @@ function shouldPrompt(io) {
502
245
  function setupRequiresInteractiveError() {
503
246
  return usageError("Setup requires an interactive terminal.");
504
247
  }
505
- function tableWidth(io) {
506
- return io.output?.columns === undefined
507
- ? defaultTableWidth()
508
- : Math.max(46, Math.min(io.output.columns, 140));
509
- }
510
- function cliIdentity(env) {
511
- return env.YUI_CLI_NAME === "yui-dev" ? "yui-dev" : "yui";
512
- }
513
248
  function parseSetupOptions(args) {
514
- if (args.length === 0)
515
- return undefined;
516
- if (args.length === 1 && args[0] === "tmux")
517
- return "tmux";
518
- throw usageError("Setup usage: yui setup [tmux]");
519
- }
520
- async function setupTmux(env, executor, question) {
521
- const command = "tmux";
522
- if (hasExecutable(command, ["-V"], executor))
523
- return ["Tmux already installed."];
524
- const plan = detectTmuxInstallPlan(env, executor);
525
- if (plan === null) {
526
- return ["Tmux is not installed.", "Install tmux manually, then run yui doctor."];
527
- }
528
- const lines = [
529
- "Tmux is not installed.",
530
- `Install with ${plan.manager}:`,
531
- ...plan.steps.map((step) => ` ${step.command} ${step.args.join(" ")}`)
532
- ];
533
- const answer = (await question("Install tmux now? [y/N]: ")).trim().toLowerCase();
534
- if (answer !== "y" && answer !== "yes") {
535
- return [...lines, "Skipped tmux installation.", "After installing tmux, run yui doctor."];
536
- }
537
- for (const step of plan.steps)
538
- executor.run(step.command, [...step.args], { inheritStdio: true });
539
- return hasExecutable(command, ["-V"], executor)
540
- ? [...lines, "Tmux installed."]
541
- : [...lines, `Tmux install command completed, but ${command} is still unavailable.`, plan.manualHint];
542
- }
543
- function detectTmuxInstallPlan(env, executor) {
544
- if (process.platform === "darwin" && commandExists("brew", env, executor)) {
545
- return {
546
- manager: "Homebrew",
547
- steps: [{ command: "brew", args: ["install", "tmux"] }],
548
- manualHint: "brew install tmux"
549
- };
550
- }
551
- if (process.platform !== "linux")
552
- return null;
553
- for (const candidate of [
554
- { command: "apt-get", args: ["install", "-y", "tmux"], hint: "sudo apt-get install -y tmux" },
555
- { command: "dnf", args: ["install", "-y", "tmux"], hint: "sudo dnf install -y tmux" },
556
- { command: "pacman", args: ["-S", "--noconfirm", "tmux"], hint: "sudo pacman -S --noconfirm tmux" },
557
- { command: "apk", args: ["add", "tmux"], hint: "sudo apk add tmux" }
558
- ]) {
559
- if (!commandExists(candidate.command, env, executor))
560
- continue;
561
- const step = withLinuxPrivilege(candidate.command, candidate.args, env, executor);
562
- if (step !== null) {
563
- return { manager: candidate.command, steps: [step], manualHint: candidate.hint };
564
- }
565
- }
566
- return null;
567
- }
568
- function hasExecutable(command, args, executor) {
569
- try {
570
- executor.run(command, args);
571
- return true;
572
- }
573
- catch {
574
- return false;
575
- }
576
- }
577
- function commandExists(command, env, executor) {
578
- if (command.includes("/"))
579
- return existsSync(command);
580
- return (env.PATH ?? "").split(delimiter).filter(Boolean)
581
- .some((entry) => existsSync(join(entry, command)))
582
- || hasExecutable(command, ["--version"], executor);
583
- }
584
- function withLinuxPrivilege(command, args, env, executor) {
585
- if (process.getuid?.() === 0)
586
- return { command, args };
587
- if (!commandExists("sudo", env, executor))
588
- return null;
589
- return { command: "sudo", args: [command, ...args] };
249
+ if (args.length !== 0)
250
+ throw usageError("Setup usage: yui setup");
590
251
  }