@zq-silk/yui 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. package/skills/yui-worker/SKILL.md +31 -0
@@ -0,0 +1,367 @@
1
+ import { roleNotFound, usageError } from "../errors/cliError.js";
2
+ import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
3
+ import { resolveAgentAdapter } from "../executor/agentAdapter.js";
4
+ import { resolveAgentEnvironment } from "../agent/agent.js";
5
+ import { defaultTableWidth, renderTable } from "../output/table.js";
6
+ import { activeRoleSummary, renderRoleDetails } from "../output/rolePresentation.js";
7
+ import { activeRoleAgentBinding, createGlobalRole, createRoleAgentBinding, switchActiveRoleAgent, updateGlobalRole } from "../role/role.js";
8
+ import { isSystemRoleName, SYSTEM_ROLE_NAMES, systemRoleDescription } from "../role/systemRoles.js";
9
+ import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
10
+ export function runGlobalRoleCommand(args, store, options = {}) {
11
+ const [command, ...rest] = args;
12
+ switch (command) {
13
+ case "add": return addRole(rest, store);
14
+ case "list": return listRoles(rest, store);
15
+ case "show": return showRole(rest, store);
16
+ case "update": return updateRole(rest, store);
17
+ case "remove": return removeRole(rest, store);
18
+ case "bind": return bindRole(rest, store);
19
+ case "enter": return enterRole(rest, store, options);
20
+ case "session": return roleSession(rest, store, options);
21
+ default:
22
+ throw usageError(command === undefined
23
+ ? "Role command is required."
24
+ : `Unknown command: role ${command}`);
25
+ }
26
+ }
27
+ function addRole(args, store) {
28
+ const [rawName, ...tail] = args;
29
+ const name = roleName(rawName);
30
+ const parsed = parseRoleOptions(tail, roleOptionSpecs({
31
+ update: false, includeAgent: true, includeWorkspace: true
32
+ }));
33
+ const agentId = required(parsed.one("--agent"), "--agent");
34
+ const agent = requireAgent(agentId, store);
35
+ if (parsed.has("--workspace") && trimmed(parsed.one("--workspace")) === undefined) {
36
+ throw usageError("--workspace is required.");
37
+ }
38
+ const workspace = trimmed(parsed.one("--workspace"))
39
+ ?? store.getConfig().defaultWorkspace
40
+ ?? process.cwd();
41
+ const binding = patchRoleAgentBinding(createRoleAgentBinding(definition(agent)), parsed);
42
+ const role = createGlobalRole(name, [binding], agent.id, workspace, new Date(), roleProfileFrom(parsed));
43
+ const created = store.createGlobalRoleIfAbsent(role);
44
+ if (created === null)
45
+ throw usageError(`Role already exists: ${name}.`);
46
+ return presentRole(`Added role ${name}`, created, store);
47
+ }
48
+ function listRoles(args, store) {
49
+ assertNoArguments(args, "Role list usage: yui role list");
50
+ const rows = new Map();
51
+ for (const name of SYSTEM_ROLE_NAMES) {
52
+ const role = store.getGlobalRole(name);
53
+ rows.set(name, role === null
54
+ ? [name, "system", "?", "?", "?", "?"]
55
+ : roleListRow(role, "system"));
56
+ }
57
+ for (const role of store.listGlobalRoles()) {
58
+ if (!rows.has(role.name)) {
59
+ rows.set(role.name, roleListRow(role, "global"));
60
+ }
61
+ }
62
+ if (rows.size === 0)
63
+ return "No roles configured.\n";
64
+ return `${renderTable("Roles", [
65
+ { header: "Role", minWidth: 4, maxWidth: 24 },
66
+ { header: "Kind", minWidth: 6, maxWidth: 8 },
67
+ { header: "Active Agent", minWidth: 8, maxWidth: 20 },
68
+ { header: "Model", minWidth: 8, maxWidth: 22 },
69
+ { header: "Effort", minWidth: 8, maxWidth: 14 },
70
+ { header: "Workspace", minWidth: 9, maxWidth: 54 }
71
+ ], [...rows.values()].sort((left, right) => left[0].localeCompare(right[0])), defaultTableWidth())}\n`;
72
+ }
73
+ function showRole(args, store) {
74
+ const [rawName, ...rest] = args;
75
+ const name = roleName(rawName);
76
+ assertNoArguments(rest, "Role show usage: yui role show <role>");
77
+ const role = store.getGlobalRole(name);
78
+ if (role === null) {
79
+ if (isSystemRoleName(name))
80
+ return renderMissingSystemRole(name);
81
+ throw roleNotFound(name);
82
+ }
83
+ return renderRoleDetails(`Role: ${name}`, role, {
84
+ kind: isSystemRoleName(name) ? "system" : "global",
85
+ sessions: store.getGlobalRoleSessionSet(name)
86
+ });
87
+ }
88
+ function updateRole(args, store) {
89
+ const [rawName, ...tail] = args;
90
+ const name = roleName(rawName);
91
+ const role = requireRole(name, store);
92
+ const parsed = parseRoleOptions(tail, roleOptionSpecs({
93
+ update: true, includeAgent: true, includeWorkspace: true
94
+ }));
95
+ if (parsed.has("--agent") && trimmed(parsed.one("--agent")) === undefined) {
96
+ throw usageError("--agent is required.");
97
+ }
98
+ if (parsed.has("--workspace") && trimmed(parsed.one("--workspace")) === undefined) {
99
+ throw usageError("--workspace is required.");
100
+ }
101
+ if ([...parsed.seen].every((option) => option === "--agent")) {
102
+ throw usageError("At least one role update option is required.");
103
+ }
104
+ const workspace = trimmed(parsed.one("--workspace"));
105
+ let bindings = role.agentBindings;
106
+ if (hasAgentConfigOptions(parsed)) {
107
+ const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
108
+ const agent = requireAgent(agentId, store);
109
+ const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
110
+ const activeSession = store.getGlobalRoleSessionSet(name)?.sessions[agentId];
111
+ if (agentId === role.activeAgentId && activeSession?.status === "running") {
112
+ throw usageError("Active Agent settings cannot be changed while its native process is running.");
113
+ }
114
+ bindings = { ...role.agentBindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
115
+ }
116
+ const next = {
117
+ ...updateGlobalRole(role, {
118
+ ...(workspace === undefined ? {} : { workspace }),
119
+ ...(bindings === role.agentBindings ? {} : { agentBindings: bindings }),
120
+ ...roleProfilePatch(parsed)
121
+ }, new Date())
122
+ };
123
+ store.saveGlobalRole(next);
124
+ return renderRoleDetails(`Updated role ${name}`, next, {
125
+ kind: isSystemRoleName(name) ? "system" : "global",
126
+ sessions: store.getGlobalRoleSessionSet(name)
127
+ });
128
+ }
129
+ function bindRole(args, store) {
130
+ const [rawName, rawAgentId, ...rest] = args;
131
+ const name = roleName(rawName);
132
+ const agentId = required(rawAgentId, "Agent id");
133
+ assertNoArguments(rest, "Role bind usage: yui role bind <role> <agent-id>");
134
+ const role = requireRole(name, store);
135
+ const agent = requireAgent(agentId, store);
136
+ const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
137
+ const withBinding = updateGlobalRole(role, {
138
+ agentBindings: { ...role.agentBindings, [agentId]: binding }
139
+ }, new Date());
140
+ if (agentId === role.activeAgentId) {
141
+ store.saveGlobalRole(withBinding);
142
+ return presentRole(`Role ${name} already bound to ${agentId}`, withBinding, store);
143
+ }
144
+ const existingSet = store.getGlobalRoleSessionSet(name);
145
+ const activeSession = existingSet?.sessions[role.activeAgentId];
146
+ try {
147
+ const switched = switchActiveRoleAgent(withBinding, existingSet ?? createRoleSessionSet({ scope: "global", roleName: name }, role.activeAgentId, new Date()), agentId, { activeRun: false, nativeProcessRunning: activeSession?.status === "running" }, new Date());
148
+ store.saveGlobalRoleWithSessionSet(switched.role, switched.sessions);
149
+ return presentRole(`Bound role ${name} to ${agentId}`, switched.role, store);
150
+ }
151
+ catch (error) {
152
+ throw usageError(error instanceof Error ? error.message : String(error));
153
+ }
154
+ }
155
+ function removeRole(args, store) {
156
+ const [rawName, ...rest] = args;
157
+ const name = roleName(rawName);
158
+ assertNoArguments(rest, "Role remove usage: yui role remove <role>");
159
+ if (isSystemRoleName(name))
160
+ throw usageError(`System role cannot be removed: ${name}`);
161
+ const role = requireRole(name, store);
162
+ const active = store.getGlobalRoleSessionSet(name)?.sessions[role.activeAgentId];
163
+ if (active?.status === "running") {
164
+ throw usageError(`GlobalRole is active and cannot be removed: ${name}.`);
165
+ }
166
+ if (!store.removeGlobalRole(name))
167
+ throw roleNotFound(name);
168
+ return `Removed role ${name}\n`;
169
+ }
170
+ function enterRole(args, store, options) {
171
+ const [rawName, ...rest] = args;
172
+ const name = roleName(rawName);
173
+ assertNoArguments(rest, "Role enter usage: yui role enter <role>");
174
+ const role = requireRole(name, store);
175
+ const agent = requireAgent(role.activeAgentId, store);
176
+ const set = store.getGlobalRoleSessionSet(name);
177
+ const session = set?.sessions[role.activeAgentId] ?? null;
178
+ const launch = compileGlobalRoleLaunch(role, agent, session?.nativeSessionId, options);
179
+ return { kind: "enter", role, launch };
180
+ }
181
+ function roleSession(args, store, options) {
182
+ const [command, rawName, ...tail] = args;
183
+ if (command !== "record" && command !== "replace") {
184
+ throw usageError("Role session usage: yui role session record|replace <role> --native-id <id> [--reason <reason>].");
185
+ }
186
+ const name = roleName(rawName);
187
+ const parsed = parseOptions(tail, new Map([
188
+ ["--native-id", false],
189
+ ...(command === "replace" ? [["--reason", false]] : [])
190
+ ]));
191
+ const nativeSessionId = required(parsed.one("--native-id"), "--native-id");
192
+ if (nativeSessionId.trim() !== nativeSessionId || nativeSessionId.length === 0) {
193
+ throw usageError("Native session id must not contain surrounding whitespace.");
194
+ }
195
+ const role = requireRole(name, store);
196
+ const binding = activeRoleAgentBinding(role);
197
+ const agent = requireAgent(binding.agentId, store);
198
+ const environment = options.env ?? process.env;
199
+ assertSessionProvenance(command, role, nativeSessionId, environment);
200
+ const set = store.getGlobalRoleSessionSet(name)
201
+ ?? createRoleSessionSet({ scope: "global", roleName: name }, role.activeAgentId, new Date());
202
+ const existing = set.sessions[role.activeAgentId] ?? null;
203
+ const input = () => ({
204
+ agentId: binding.agentId,
205
+ adapterId: binding.adapterId,
206
+ nativeSessionId,
207
+ policy: "fixed",
208
+ status: environment.YUI_ROLE === name ? "running" : "ready"
209
+ });
210
+ if (command === "record") {
211
+ if (existing !== null && existing.nativeSessionId !== nativeSessionId) {
212
+ throw usageError("GlobalRole session replacement must be explicit.");
213
+ }
214
+ store.saveGlobalRoleSessionSet(recordRoleAgentSession(set, input(), new Date()));
215
+ return `Recorded native session for role ${name}\n`;
216
+ }
217
+ if (existing === null) {
218
+ throw usageError("Native session replacement requires an existing native session.");
219
+ }
220
+ if (existing.status === "running") {
221
+ throw usageError("Native session replacement is blocked while the native Agent process is running.");
222
+ }
223
+ if (existing.nativeSessionId === nativeSessionId) {
224
+ throw usageError("Native session replacement requires a different native session identity.");
225
+ }
226
+ const reason = required(parsed.one("--reason"), "--reason").trim();
227
+ store.saveGlobalRoleSessionSet(recordRoleAgentSession(set, input(), new Date()));
228
+ return `Replaced native session for role ${name}\n`;
229
+ }
230
+ function assertSessionProvenance(command, role, nativeSessionId, environment) {
231
+ const values = [
232
+ environment.YUI_ROLE,
233
+ environment.YUI_AGENT_ID,
234
+ environment.YUI_ADAPTER_ID
235
+ ];
236
+ if (values.every((value) => value === undefined))
237
+ return;
238
+ if (values.some((value) => value === undefined || value.trim().length === 0)) {
239
+ throw usageError("Native session registration provenance is incomplete.");
240
+ }
241
+ if (command !== "record") {
242
+ throw usageError("A running Agent may record only its current native session.");
243
+ }
244
+ const binding = activeRoleAgentBinding(role);
245
+ if (environment.YUI_ROLE !== role.name
246
+ || environment.YUI_AGENT_ID !== binding.agentId
247
+ || environment.YUI_ADAPTER_ID !== binding.adapterId) {
248
+ throw usageError("Native session registration does not match the active GlobalRole binding.");
249
+ }
250
+ if (binding.adapterId === "codex" && environment.CODEX_THREAD_ID?.trim() !== nativeSessionId) {
251
+ throw usageError("Native session id does not match CODEX_THREAD_ID.");
252
+ }
253
+ }
254
+ function parseOptions(args, specs) {
255
+ const values = new Map();
256
+ const seen = new Set();
257
+ for (let index = 0; index < args.length; index += 1) {
258
+ const option = args[index];
259
+ const kind = specs.get(option);
260
+ if (kind === undefined) {
261
+ throw usageError(option.startsWith("--")
262
+ ? `Unsupported option: ${option}`
263
+ : `Unexpected argument: ${option}`);
264
+ }
265
+ const repeatable = kind === true;
266
+ if (!repeatable && seen.has(option)) {
267
+ throw usageError(`Option may only be specified once: ${option}`);
268
+ }
269
+ seen.add(option);
270
+ if (kind === "flag")
271
+ continue;
272
+ const value = args[index + 1];
273
+ if (value === undefined || value.startsWith("--"))
274
+ throw usageError(`${option} is required.`);
275
+ values.set(option, [...(values.get(option) ?? []), value]);
276
+ index += 1;
277
+ }
278
+ return {
279
+ seen,
280
+ has: (option) => seen.has(option),
281
+ one: (option) => values.get(option)?.[0],
282
+ many: (option) => [...(values.get(option) ?? [])]
283
+ };
284
+ }
285
+ function compileGlobalRoleLaunch(role, agent, nativeSessionId, options) {
286
+ const adapter = resolveAgentAdapter(agent.adapterId);
287
+ const input = {
288
+ agent: definition(agent),
289
+ config: activeRoleAgentBinding(role).config,
290
+ workspace: role.workspace,
291
+ systemPrompt: role.systemPrompt
292
+ };
293
+ const compiled = nativeSessionId === undefined
294
+ ? adapter.compileNew(input)
295
+ : adapter.compileResume({ ...input, nativeSessionId });
296
+ const baseEnvironment = options.env ?? process.env;
297
+ return {
298
+ command: agent.command,
299
+ args: compiled.argv,
300
+ env: stringEnvironment({
301
+ ...baseEnvironment,
302
+ ...resolveAgentEnvironment(definition(agent), baseEnvironment),
303
+ ...(options.yuiHome === undefined ? {} : { YUI_HOME: options.yuiHome }),
304
+ YUI_ROLE: role.name,
305
+ YUI_AGENT_ID: agent.id,
306
+ YUI_ADAPTER_ID: agent.adapterId,
307
+ YUI_WORKSPACE: role.workspace
308
+ })
309
+ };
310
+ }
311
+ function stringEnvironment(environment) {
312
+ return Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined));
313
+ }
314
+ function definition(agent) {
315
+ return { ...agent, baseArgs: [...agent.baseArgs], environment: [...agent.environment], source: "custom" };
316
+ }
317
+ function requireAgent(id, store) {
318
+ const agent = store.getConfiguredAgent(id);
319
+ if (agent === null)
320
+ throw usageError(`Unsupported agent: ${id}`);
321
+ return agent;
322
+ }
323
+ function requireRole(name, store) {
324
+ const role = store.getGlobalRole(name);
325
+ if (role === null)
326
+ throw roleNotFound(name);
327
+ return role;
328
+ }
329
+ function roleName(value) {
330
+ if (value === undefined || value.trim().length === 0)
331
+ throw usageError("Role name is required.");
332
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) {
333
+ throw usageError("Role name may only contain letters, numbers, hyphens, and underscores.");
334
+ }
335
+ return value.trim();
336
+ }
337
+ function required(value, label) {
338
+ if (value === undefined || value.trim().length === 0)
339
+ throw usageError(`${label} is required.`);
340
+ return value.trim();
341
+ }
342
+ function trimmed(value) {
343
+ const result = value?.trim();
344
+ return result === undefined || result.length === 0 ? undefined : result;
345
+ }
346
+ function assertNoArguments(args, message) {
347
+ if (args.length > 0)
348
+ throw usageError(`${message}. Unexpected argument: ${args[0]}`);
349
+ }
350
+ function presentRole(title, role, store) {
351
+ return renderRoleDetails(title, role, {
352
+ kind: isSystemRoleName(role.name) ? "system" : "global",
353
+ sessions: store.getGlobalRoleSessionSet(role.name)
354
+ });
355
+ }
356
+ function roleListRow(role, kind) {
357
+ const summary = activeRoleSummary(role);
358
+ return [role.name, kind, summary.agent, summary.model, summary.effort, role.workspace];
359
+ }
360
+ function renderMissingSystemRole(name) {
361
+ return [
362
+ `Role: ${name}`,
363
+ `System: ${systemRoleDescription(name)}`,
364
+ "Active agent: ?",
365
+ "Workspace: ?"
366
+ ].join("\n").concat("\n");
367
+ }
@@ -0,0 +1,100 @@
1
+ import { runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
2
+ import { defaultTableWidth, renderTable } from "../output/table.js";
3
+ import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
4
+ const WAKEUP_PREFIX = "leader-wakeup:";
5
+ const RECOVERY_PREFIX = "leader-recovery:";
6
+ /**
7
+ * A compatibility view over the two durable scheduler records. This does not
8
+ * reintroduce the deleted generic Job queue.
9
+ */
10
+ export function runJobCommand(args, store, options = {}) {
11
+ const [command, ...rest] = args;
12
+ if (command === "list")
13
+ return listJobs(rest, store);
14
+ if (command === "retry")
15
+ return retryJob(rest, store, options);
16
+ throw usageError(command === undefined
17
+ ? "Jobs command is required."
18
+ : `Unknown command: jobs ${command}`);
19
+ }
20
+ function listJobs(args, store) {
21
+ if (args.length !== 0)
22
+ throw usageError("Jobs list usage: yui jobs list.");
23
+ const jobs = collectJobs(store);
24
+ if (jobs.length === 0)
25
+ return "No scheduler jobs found.\n";
26
+ return `${renderTable("Scheduler jobs", [
27
+ { header: "Job", minWidth: 12, maxWidth: 44 },
28
+ { header: "Task", minWidth: 6, maxWidth: 20 },
29
+ { header: "Kind", minWidth: 12, maxWidth: 18 },
30
+ { header: "Status", minWidth: 6, maxWidth: 10 },
31
+ { header: "Updated", minWidth: 10, maxWidth: 28 },
32
+ { header: "Detail", minWidth: 8, maxWidth: 64 }
33
+ ], jobs.map((job) => [
34
+ job.id,
35
+ job.taskId,
36
+ job.kind,
37
+ job.status,
38
+ job.updatedAt,
39
+ job.detail
40
+ ]), defaultTableWidth())}\n`;
41
+ }
42
+ function collectJobs(store) {
43
+ const jobs = [];
44
+ for (const task of store.listTasks()) {
45
+ const wakeup = store.getPendingWakeup(task.id);
46
+ if (wakeup !== null) {
47
+ jobs.push({
48
+ id: `${WAKEUP_PREFIX}${task.id}`,
49
+ taskId: task.id,
50
+ kind: "leader-wakeup",
51
+ status: "pending",
52
+ detail: `${wakeup.reasons.join(", ")} (${wakeup.requestCount})`,
53
+ updatedAt: wakeup.lastRequestedAt
54
+ });
55
+ }
56
+ const failure = store.getLeaderFailure(task.id);
57
+ const notification = store.getOperatorNotification(task.id);
58
+ if (failure !== null || notification !== null) {
59
+ jobs.push({
60
+ id: `${RECOVERY_PREFIX}${task.id}`,
61
+ taskId: task.id,
62
+ kind: "leader-recovery",
63
+ status: "failed",
64
+ detail: failure?.message ?? notification?.message ?? "Leader recovery failed.",
65
+ updatedAt: failure?.lastFailedAt ?? notification?.updatedAt ?? task.updatedAt
66
+ });
67
+ }
68
+ }
69
+ return jobs.sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }));
70
+ }
71
+ function retryJob(args, store, options) {
72
+ if (args.length !== 1 || args[0].trim().length === 0) {
73
+ throw usageError("Jobs retry usage: yui jobs retry <id>.");
74
+ }
75
+ const id = args[0].trim();
76
+ if (!id.startsWith(RECOVERY_PREFIX) || id.length === RECOVERY_PREFIX.length) {
77
+ throw usageError(`Job is not retryable: ${id}. Only leader-recovery jobs can be retried.`);
78
+ }
79
+ const taskId = id.slice(RECOVERY_PREFIX.length);
80
+ const now = options.now?.() ?? new Date();
81
+ const runtime = options.runtime;
82
+ if (runtime === undefined)
83
+ throw runtimeError("Task workflow runtime is not configured.");
84
+ store.transaction((tx) => {
85
+ const task = tx.getTask(taskId);
86
+ if (task === null)
87
+ throw taskNotFound(taskId);
88
+ if (task.status !== "active") {
89
+ throw usageError(`Leader recovery can only be retried for an active Task: ${task.id}.`);
90
+ }
91
+ if (tx.getLeaderFailure(task.id) === null && tx.getOperatorNotification(task.id) === null) {
92
+ throw usageError(`Job not found: ${id}.`);
93
+ }
94
+ tx.clearLeaderFailure(task.id);
95
+ tx.clearOperatorNotification(task.id);
96
+ queueLeaderWakeup(tx, task.id, "recovery-retry", now);
97
+ });
98
+ runtime.notifyStateChanged(taskId);
99
+ return `Retry requested for ${id}\n`;
100
+ }
@@ -0,0 +1,38 @@
1
+ import { usageError } from "../errors/cliError.js";
2
+ import { submitOperatorMessage } from "./taskCommands.js";
3
+ /** Operator is intentionally small: interactive entry is owned by the tmux CLI path. */
4
+ export function runOperatorCommand(args, store, options = {}) {
5
+ const [command, ...rest] = args;
6
+ if (command !== "submit") {
7
+ throw usageError(command === undefined
8
+ ? "Operator command is required."
9
+ : `Unknown command: operator ${command}`);
10
+ }
11
+ const usage = "Operator submit usage: yui operator submit <body> [--task <id>].";
12
+ const positionals = [];
13
+ let taskId;
14
+ for (let index = 0; index < rest.length; index += 1) {
15
+ const value = rest[index];
16
+ if (value !== "--task") {
17
+ if (value.startsWith("--"))
18
+ throw usageError(`Unsupported option: ${value}.`, usage);
19
+ positionals.push(value);
20
+ continue;
21
+ }
22
+ if (taskId !== undefined)
23
+ throw usageError("Option may only be specified once: --task.", usage);
24
+ const candidate = rest[index + 1];
25
+ if (candidate === undefined || candidate.startsWith("--")) {
26
+ throw usageError("--task is required.", usage);
27
+ }
28
+ taskId = candidate.trim();
29
+ index += 1;
30
+ }
31
+ if (positionals.length !== 1 || positionals[0].trim().length === 0) {
32
+ throw usageError(usage);
33
+ }
34
+ return {
35
+ kind: "output",
36
+ output: submitOperatorMessage(positionals[0], taskId, store, options)
37
+ };
38
+ }
@@ -0,0 +1,86 @@
1
+ import { usageError } from "../errors/cliError.js";
2
+ import { defaultTableWidth, renderTable } from "../output/table.js";
3
+ import { NodeGitWorkspace } from "../repository/gitWorkspace.js";
4
+ import { createRepository } from "../repository/repository.js";
5
+ export async function runRepositoryCommand(args, store, options = {}) {
6
+ const [command, ...rest] = args;
7
+ if (command === "add")
8
+ return addRepository(rest, store, options);
9
+ if (command === "list")
10
+ return listRepositories(rest, store);
11
+ throw usageError(command === undefined
12
+ ? "Repository command is required."
13
+ : `Unknown command: repository ${command}`);
14
+ }
15
+ async function addRepository(args, store, options) {
16
+ const usage = "Repository add usage: yui repository add <name> <path> [--base <ref>].";
17
+ const parsed = parseAddArguments(args, usage);
18
+ const git = options.git ?? new NodeGitWorkspace();
19
+ const inspected = await git.inspect(parsed.path, parsed.baseRef ?? "HEAD");
20
+ const repository = createRepository(store.nextRepositoryId(), parsed.name, inspected.root, parsed.baseRef ?? inspected.baseRef, (options.now ?? (() => new Date()))());
21
+ if (store.listRepositories().some((entry) => entry.path === repository.path)) {
22
+ throw usageError(`Repository path is already registered: ${repository.path}.`);
23
+ }
24
+ const created = store.createRepositoryIfAbsent(repository);
25
+ if (created === null)
26
+ throw usageError(`Repository already exists: ${repository.name}.`);
27
+ return [
28
+ `Added repository ${created.id}`,
29
+ `Name: ${created.name}`,
30
+ `Path: ${created.path}`,
31
+ `Base: ${created.defaultBranch}`
32
+ ].join("\n").concat("\n");
33
+ }
34
+ function listRepositories(args, store) {
35
+ if (args.length !== 0) {
36
+ throw usageError("Repository list usage: yui repository list.");
37
+ }
38
+ const repositories = store.listRepositories();
39
+ if (repositories.length === 0)
40
+ return "No repositories found.\n";
41
+ return `${renderTable("Repositories", [
42
+ { header: "Repository", minWidth: 10, maxWidth: 24 },
43
+ { header: "Name", minWidth: 4, maxWidth: 28 },
44
+ { header: "Path", minWidth: 8, maxWidth: 64 },
45
+ { header: "Base", minWidth: 4, maxWidth: 24 }
46
+ ], repositories.map((repository) => [
47
+ repository.id,
48
+ repository.name,
49
+ repository.path,
50
+ repository.defaultBranch
51
+ ]), defaultTableWidth())}\n`;
52
+ }
53
+ function parseAddArguments(args, usage) {
54
+ const positionals = [];
55
+ let baseRef;
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const value = args[index];
58
+ if (value === "--base") {
59
+ if (baseRef !== undefined)
60
+ throw usageError(`--base may only be provided once. ${usage}`);
61
+ const next = args[index + 1];
62
+ if (next === undefined || next.startsWith("--"))
63
+ throw usageError(`--base is required. ${usage}`);
64
+ baseRef = requireText(next, "--base");
65
+ index += 1;
66
+ continue;
67
+ }
68
+ if (value.startsWith("--"))
69
+ throw usageError(`Unknown option: ${value}. ${usage}`);
70
+ positionals.push(value);
71
+ }
72
+ if (positionals.length !== 2)
73
+ throw usageError(usage);
74
+ return {
75
+ name: requireText(positionals[0], "Repository name"),
76
+ path: requireText(positionals[1], "Repository path"),
77
+ ...(baseRef === undefined ? {} : { baseRef })
78
+ };
79
+ }
80
+ function requireText(value, label) {
81
+ const normalized = value.trim();
82
+ if (normalized.length === 0 || normalized.includes("\0")) {
83
+ throw usageError(`${label} is required.`);
84
+ }
85
+ return normalized;
86
+ }