@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,326 @@
1
+ export class InputRequestStateError extends Error {
2
+ requestId;
3
+ status;
4
+ constructor(requestId, status) {
5
+ super(`Input request ${requestId} is already ${status}.`);
6
+ this.requestId = requestId;
7
+ this.status = status;
8
+ this.name = "InputRequestStateError";
9
+ }
10
+ }
11
+ const CHOICE_KEY = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
12
+ export function createInputRequest(id, taskId, requester, input, now) {
13
+ const timestamp = requireDate(now, "Input request creation time");
14
+ const choices = normalizeChoices(input.choices);
15
+ return validateInputRequest({
16
+ schemaVersion: 1,
17
+ id: requireIdentity(id, "Input request id"),
18
+ taskId: requireIdentity(taskId, "Input request Task id"),
19
+ requester: normalizeRequester(requester),
20
+ question: requireText(input.question, "Input request question"),
21
+ choices,
22
+ blockedRefs: normalizeBlockedRefs(input.blockedRefs),
23
+ policy: normalizePolicy(input.policy, choices, timestamp),
24
+ status: "open",
25
+ createdAt: timestamp,
26
+ updatedAt: timestamp
27
+ });
28
+ }
29
+ export function answerInputRequest(request, answer, answeredBy, now) {
30
+ validateInputRequest(request);
31
+ assertOpen(request);
32
+ if (answeredBy !== "user" && answeredBy !== "operator" && answeredBy !== "agent-timeout") {
33
+ throw new Error("Input answerer must be user, operator, or agent-timeout.");
34
+ }
35
+ const answeredAt = transitionTime(request, now);
36
+ const normalizedAnswer = normalizeAnswer(request, answer);
37
+ if (answeredBy === "agent-timeout") {
38
+ assertAgentTimeoutAnswer(request, normalizedAnswer, answeredAt);
39
+ }
40
+ return validateInputRequest({
41
+ ...request,
42
+ status: "answered",
43
+ resolution: {
44
+ answer: normalizedAnswer,
45
+ answeredBy,
46
+ answeredAt
47
+ },
48
+ updatedAt: answeredAt
49
+ });
50
+ }
51
+ export function cancelInputRequest(request, reason, now) {
52
+ validateInputRequest(request);
53
+ assertOpen(request);
54
+ const cancelledAt = transitionTime(request, now);
55
+ return validateInputRequest({
56
+ ...request,
57
+ status: "cancelled",
58
+ cancellation: {
59
+ reason: requireText(reason, "Input cancellation reason"),
60
+ cancelledAt
61
+ },
62
+ updatedAt: cancelledAt
63
+ });
64
+ }
65
+ export function validateInputRequest(value) {
66
+ const request = record(value, "Input request");
67
+ const terminalField = request.status === "answered"
68
+ ? ["resolution"]
69
+ : request.status === "cancelled" ? ["cancellation"] : [];
70
+ exact(request, [
71
+ "schemaVersion", "id", "taskId", "requester", "question", "choices",
72
+ "blockedRefs", "policy", "status", "createdAt", "updatedAt", ...terminalField
73
+ ], "Input request");
74
+ if (request.schemaVersion !== 1)
75
+ throw new Error("Input request must use schemaVersion 1.");
76
+ const choices = validateChoices(request.choices);
77
+ const createdAt = requireTimestamp(request.createdAt, "Input request createdAt");
78
+ const base = {
79
+ schemaVersion: 1,
80
+ id: requireIdentity(request.id, "Input request id"),
81
+ taskId: requireIdentity(request.taskId, "Input request Task id"),
82
+ requester: normalizeRequester(request.requester),
83
+ question: requireNormalizedText(request.question, "Input request question"),
84
+ choices,
85
+ blockedRefs: validateBlockedRefs(request.blockedRefs),
86
+ policy: normalizePolicy(request.policy, choices, createdAt),
87
+ createdAt,
88
+ updatedAt: requireTimestamp(request.updatedAt, "Input request updatedAt")
89
+ };
90
+ if (Date.parse(base.updatedAt) < Date.parse(base.createdAt)) {
91
+ throw new Error("Input request updatedAt cannot precede createdAt.");
92
+ }
93
+ if (request.status === "open")
94
+ return { ...base, status: "open" };
95
+ if (request.status === "answered") {
96
+ const resolution = record(request.resolution, "Input resolution");
97
+ exact(resolution, ["answer", "answeredBy", "answeredAt"], "Input resolution");
98
+ const answer = validatePersistedAnswer(base.choices, resolution.answer);
99
+ if (resolution.answeredBy !== "user"
100
+ && resolution.answeredBy !== "operator"
101
+ && resolution.answeredBy !== "agent-timeout") {
102
+ throw new Error("Input resolution answerer must be user, operator, or agent-timeout.");
103
+ }
104
+ const answeredAt = requireTimestamp(resolution.answeredAt, "Input resolution answeredAt");
105
+ if (answeredAt !== base.updatedAt)
106
+ throw new Error("Input resolution answeredAt must match updatedAt.");
107
+ if (resolution.answeredBy === "agent-timeout") {
108
+ assertAgentTimeoutAnswer(base, answer, answeredAt);
109
+ }
110
+ return {
111
+ ...base,
112
+ status: "answered",
113
+ resolution: {
114
+ answer,
115
+ answeredBy: resolution.answeredBy,
116
+ answeredAt
117
+ }
118
+ };
119
+ }
120
+ if (request.status === "cancelled") {
121
+ const cancellation = record(request.cancellation, "Input cancellation");
122
+ exact(cancellation, ["reason", "cancelledAt"], "Input cancellation");
123
+ const cancelledAt = requireTimestamp(cancellation.cancelledAt, "Input cancellation cancelledAt");
124
+ if (cancelledAt !== base.updatedAt)
125
+ throw new Error("Input cancellation cancelledAt must match updatedAt.");
126
+ return {
127
+ ...base,
128
+ status: "cancelled",
129
+ cancellation: {
130
+ reason: requireNormalizedText(cancellation.reason, "Input cancellation reason"),
131
+ cancelledAt
132
+ }
133
+ };
134
+ }
135
+ throw new Error(`Input request status is invalid: ${String(request.status)}.`);
136
+ }
137
+ function normalizeChoices(value) {
138
+ if (!Array.isArray(value))
139
+ throw new Error("Input choices must be an array.");
140
+ const choices = value.map((choice) => {
141
+ const item = record(choice, "Input choice");
142
+ exact(item, ["key", "label"], "Input choice");
143
+ return {
144
+ key: requireChoiceKey(item.key),
145
+ label: requireText(item.label, "Input choice label")
146
+ };
147
+ });
148
+ if (new Set(choices.map(({ key }) => key)).size !== choices.length) {
149
+ throw new Error("Input choice keys must be unique.");
150
+ }
151
+ return choices;
152
+ }
153
+ function validateChoices(value) {
154
+ const choices = normalizeChoices(value);
155
+ if (JSON.stringify(choices) !== JSON.stringify(value))
156
+ throw new Error("Input choices must be normalized.");
157
+ return choices;
158
+ }
159
+ function normalizeBlockedRefs(value) {
160
+ if (!Array.isArray(value))
161
+ throw new Error("Input blocked references must be an array.");
162
+ const references = value.map((reference) => {
163
+ const item = record(reference, "Input blocked reference");
164
+ exact(item, ["type", "id"], "Input blocked reference");
165
+ if (item.type !== "work-item" && item.type !== "run") {
166
+ throw new Error("Input blocked reference type must be work-item or run.");
167
+ }
168
+ return { type: item.type, id: requireIdentity(item.id, "Input blocked reference id") };
169
+ });
170
+ const keys = references.map(({ type, id }) => `${type}:${id}`);
171
+ if (new Set(keys).size !== keys.length)
172
+ throw new Error("Input blocked references must be unique.");
173
+ return references;
174
+ }
175
+ function normalizePolicy(value, choices, createdAt) {
176
+ if (value === undefined)
177
+ return { kind: "required" };
178
+ const policy = record(value, "Input request policy");
179
+ if (policy.kind === "required") {
180
+ exact(policy, ["kind"], "Input request policy");
181
+ return { kind: "required" };
182
+ }
183
+ if (policy.kind !== "recommended") {
184
+ throw new Error("Input request policy kind is invalid.");
185
+ }
186
+ exact(policy, ["kind", "recommendedChoiceKey", "timeoutAt"], "Input request policy");
187
+ if (choices.length === 0) {
188
+ throw new Error("Recommended input request requires choices.");
189
+ }
190
+ const recommendedChoiceKey = requireChoiceKey(policy.recommendedChoiceKey);
191
+ if (!choices.some((choice) => choice.key === recommendedChoiceKey)) {
192
+ throw new Error(`Recommended input choice does not exist: ${recommendedChoiceKey}.`);
193
+ }
194
+ const timeoutAt = requireTimestamp(policy.timeoutAt, "Input request timeoutAt");
195
+ if (Date.parse(timeoutAt) <= Date.parse(createdAt)) {
196
+ throw new Error("Input request timeoutAt must be after creation.");
197
+ }
198
+ return { kind: "recommended", recommendedChoiceKey, timeoutAt };
199
+ }
200
+ function validateBlockedRefs(value) {
201
+ const references = normalizeBlockedRefs(value);
202
+ if (JSON.stringify(references) !== JSON.stringify(value)) {
203
+ throw new Error("Input blocked references must be normalized.");
204
+ }
205
+ return references;
206
+ }
207
+ function normalizeRequester(value) {
208
+ const requester = record(value, "Input requester");
209
+ exact(requester, requester.nativeSessionId === undefined
210
+ ? ["roleName", "agentId", "runId"]
211
+ : ["roleName", "agentId", "runId", "nativeSessionId"], "Input requester");
212
+ if (requester.roleName !== "leader")
213
+ throw new Error("Input requester must be the Task Leader.");
214
+ return {
215
+ roleName: "leader",
216
+ agentId: requireIdentity(requester.agentId, "Input requester Agent id"),
217
+ runId: requireIdentity(requester.runId, "Input requester Run id"),
218
+ ...(requester.nativeSessionId === undefined
219
+ ? {}
220
+ : { nativeSessionId: requireIdentity(requester.nativeSessionId, "Input requester native session id") })
221
+ };
222
+ }
223
+ function normalizeAnswer(request, answer) {
224
+ const value = record(answer, "Input answer");
225
+ if (request.choices.length === 0) {
226
+ if (value.choiceKey !== undefined)
227
+ throw new Error("Free-text input request does not accept a choice.");
228
+ exact(value, ["text"], "Input answer");
229
+ return { text: requireText(value.text, "Input answer") };
230
+ }
231
+ exact(value, ["choiceKey"], "Input answer");
232
+ const key = requireChoiceKey(value.choiceKey);
233
+ const choice = request.choices.find((candidate) => candidate.key === key);
234
+ if (choice === undefined)
235
+ throw new Error(`Input answer choice does not exist: ${key}.`);
236
+ return { choiceKey: choice.key, text: choice.label };
237
+ }
238
+ function assertAgentTimeoutAnswer(request, answer, answeredAt) {
239
+ if (request.policy.kind !== "recommended") {
240
+ throw new Error("Required input request cannot be answered by Agent timeout.");
241
+ }
242
+ if (Date.parse(answeredAt) < Date.parse(request.policy.timeoutAt)) {
243
+ throw new Error("Input request has not reached its timeout.");
244
+ }
245
+ if (answer.choiceKey !== request.policy.recommendedChoiceKey) {
246
+ throw new Error("Agent timeout must use the recommended choice.");
247
+ }
248
+ }
249
+ function assertOpen(request) {
250
+ if (request.status !== "open")
251
+ throw new InputRequestStateError(request.id, request.status);
252
+ }
253
+ function transitionTime(request, now) {
254
+ const timestamp = requireDate(now, "Input request transition time");
255
+ if (Date.parse(timestamp) < Date.parse(request.createdAt)) {
256
+ throw new Error("Input request transition cannot predate creation.");
257
+ }
258
+ return timestamp;
259
+ }
260
+ function requireChoiceKey(value) {
261
+ if (typeof value !== "string" || !CHOICE_KEY.test(value))
262
+ throw new Error("Input choice key is invalid.");
263
+ return value;
264
+ }
265
+ function validatePersistedAnswer(choices, value) {
266
+ const answer = record(value, "Input resolution answer");
267
+ if (choices.length === 0) {
268
+ exact(answer, ["text"], "Input resolution answer");
269
+ return { text: requireNormalizedText(answer.text, "Input answer") };
270
+ }
271
+ exact(answer, ["choiceKey", "text"], "Input resolution answer");
272
+ const choiceKey = requireChoiceKey(answer.choiceKey);
273
+ const choice = choices.find((candidate) => candidate.key === choiceKey);
274
+ if (choice === undefined)
275
+ throw new Error(`Input answer choice does not exist: ${choiceKey}.`);
276
+ const text = requireNormalizedText(answer.text, "Input answer");
277
+ if (text !== choice.label)
278
+ throw new Error("Input answer text does not match the selected choice label.");
279
+ return { choiceKey, text };
280
+ }
281
+ function requireIdentity(value, label) {
282
+ const text = requireText(value, label);
283
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(text) || /[\/\\\0]/.test(text)) {
284
+ throw new Error(`${label} is invalid.`);
285
+ }
286
+ return text;
287
+ }
288
+ function requireText(value, label) {
289
+ if (typeof value !== "string" || value.includes("\0"))
290
+ throw new Error(`${label} is invalid.`);
291
+ const text = value.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trim();
292
+ if (text.length === 0)
293
+ throw new Error(`${label} is required.`);
294
+ return text;
295
+ }
296
+ function requireNormalizedText(value, label) {
297
+ const text = requireText(value, label);
298
+ if (text !== value)
299
+ throw new Error(`${label} must be normalized.`);
300
+ return text;
301
+ }
302
+ function requireDate(value, label) {
303
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime()))
304
+ throw new Error(`${label} is invalid.`);
305
+ return value.toISOString();
306
+ }
307
+ function requireTimestamp(value, label) {
308
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value)))
309
+ throw new Error(`${label} is invalid.`);
310
+ return value;
311
+ }
312
+ function record(value, label) {
313
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
314
+ throw new Error(`${label} must be an object.`);
315
+ }
316
+ return value;
317
+ }
318
+ function exact(value, fields, label) {
319
+ const allowed = new Set(fields);
320
+ const unknown = Object.keys(value).find((key) => !allowed.has(key));
321
+ if (unknown !== undefined)
322
+ throw new Error(`${label} has unknown field: ${unknown}.`);
323
+ const missing = fields.find((key) => !Object.hasOwn(value, key));
324
+ if (missing !== undefined)
325
+ throw new Error(`${label} is missing field: ${missing}.`);
326
+ }
@@ -0,0 +1,69 @@
1
+ export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
2
+ export function createTaskMessage(id, body, kind, author, now, context = {}) {
3
+ validateKindAndAuthor(kind, author);
4
+ const message = {
5
+ schemaVersion: 1,
6
+ id: requireSafeIdentity(id, "Message id"),
7
+ kind,
8
+ author: normalizeAuthor(author),
9
+ body: requireText(body, "Message body"),
10
+ ...(context.runId === undefined
11
+ ? {}
12
+ : { runId: requireSafeIdentity(context.runId, "Message Run id") }),
13
+ ...(context.workItemId === undefined
14
+ ? {}
15
+ : { workItemId: requireSafeIdentity(context.workItemId, "Message Work item id") }),
16
+ createdAt: now.toISOString()
17
+ };
18
+ validateTaskMessage(message);
19
+ return message;
20
+ }
21
+ export function taskMessageAuthorLabel(author) {
22
+ return author.type === "role" ? author.roleName : author.type;
23
+ }
24
+ export function validateTaskMessage(message) {
25
+ if (message.schemaVersion !== 1)
26
+ throw new Error("Task Message must use schemaVersion 1.");
27
+ requireSafeIdentity(message.id, "Message id");
28
+ requireText(message.body, "Message body");
29
+ validateKindAndAuthor(message.kind, message.author);
30
+ normalizeAuthor(message.author);
31
+ if (message.runId !== undefined)
32
+ requireSafeIdentity(message.runId, "Message Run id");
33
+ if (message.workItemId !== undefined) {
34
+ requireSafeIdentity(message.workItemId, "Message Work item id");
35
+ }
36
+ if (typeof message.createdAt !== "string" || Number.isNaN(Date.parse(message.createdAt))) {
37
+ throw new Error("Message createdAt is invalid.");
38
+ }
39
+ }
40
+ function validateKindAndAuthor(kind, author) {
41
+ if (!TASK_MESSAGE_KINDS.includes(kind))
42
+ throw new Error(`Message kind is invalid: ${String(kind)}.`);
43
+ const expectedType = kind === "role-result" ? "role" : kind;
44
+ if (author?.type !== expectedType) {
45
+ const label = kind === "role-result" ? "Role result" : `Message kind ${kind}`;
46
+ throw new Error(`${label} requires a ${expectedType} author.`);
47
+ }
48
+ }
49
+ function normalizeAuthor(author) {
50
+ return author.type === "role"
51
+ ? { type: "role", roleName: requireSafeIdentity(author.roleName, "Message Role name") }
52
+ : { type: author.type };
53
+ }
54
+ function requireSafeIdentity(value, label) {
55
+ const normalized = requireText(value, label);
56
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
57
+ || /[\/\\\0]/.test(normalized)) {
58
+ throw new Error(`${label} is invalid.`);
59
+ }
60
+ return normalized;
61
+ }
62
+ function requireText(value, label) {
63
+ if (typeof value !== "string" || value.includes("\0"))
64
+ throw new Error(`${label} is invalid.`);
65
+ const normalized = value.trim();
66
+ if (normalized.length === 0)
67
+ throw new Error(`${label} is required.`);
68
+ return normalized;
69
+ }
@@ -0,0 +1,27 @@
1
+ export function createMilestone(id, taskId, title, summary, now) {
2
+ return {
3
+ schemaVersion: 1,
4
+ id: requireSafeIdentity(id, "Milestone id"),
5
+ taskId: requireSafeIdentity(taskId, "Task id"),
6
+ title: requireText(title, "Milestone title"),
7
+ summary: requireText(summary, "Milestone summary"),
8
+ createdBy: "leader",
9
+ createdAt: now.toISOString()
10
+ };
11
+ }
12
+ function requireSafeIdentity(value, label) {
13
+ const normalized = requireText(value, label);
14
+ if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
15
+ || /[\/\\\0]/.test(normalized)) {
16
+ throw new Error(`${label} is invalid.`);
17
+ }
18
+ return normalized;
19
+ }
20
+ function requireText(value, label) {
21
+ if (typeof value !== "string" || value.includes("\0"))
22
+ throw new Error(`${label} is invalid.`);
23
+ const normalized = value.trim();
24
+ if (normalized.length === 0)
25
+ throw new Error(`${label} is required.`);
26
+ return normalized;
27
+ }
@@ -0,0 +1,66 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
4
+ /**
5
+ * Prepares durable Operator instructions without turning them into Codex's
6
+ * positional first prompt. Interactive Operator entry must open at the native
7
+ * Agent composer; launch code can pass systemPrompt through an adapter-native
8
+ * instruction channel when one is available.
9
+ */
10
+ export function prepareGlobalRoleLaunch(role, options = {}) {
11
+ const args = [...(role.args ?? [])];
12
+ const base = mergeEnv(options.baseEnv, role.env ?? {});
13
+ if (role.name !== SYSTEM_OPERATOR_ROLE || options.yuiHome === undefined) {
14
+ return { args, env: base };
15
+ }
16
+ const contextPath = writeOperatorContext(options.yuiHome, role.workspace);
17
+ return {
18
+ args,
19
+ env: mergeEnv(base, {
20
+ YUI_HOME: options.yuiHome,
21
+ YUI_ROLE: role.name,
22
+ YUI_WORKSPACE: role.workspace,
23
+ YUI_OPERATOR_CONTEXT: contextPath
24
+ }),
25
+ systemPrompt: renderOperatorLaunchInstruction(contextPath),
26
+ contextPath
27
+ };
28
+ }
29
+ export function writeOperatorContext(yuiHome, workspace) {
30
+ const operatorDir = join(yuiHome, "operator");
31
+ const contextPath = join(operatorDir, "YUI_OPERATOR.md");
32
+ mkdirSync(operatorDir, { recursive: true });
33
+ writeFileSync(contextPath, `${renderOperatorContext(yuiHome, workspace)}\n`, { mode: 0o600 });
34
+ return contextPath;
35
+ }
36
+ export function renderOperatorLaunchInstruction(contextPath) {
37
+ return [
38
+ `Read and follow the Yui Operator instructions in ${contextPath}.`,
39
+ "Manage Yui through its CLI; do not perform Task work."
40
+ ].join(" ");
41
+ }
42
+ function renderOperatorContext(yuiHome, workspace) {
43
+ return `${readOperatorSkill()}
44
+
45
+ # Yui Operator runtime
46
+
47
+ You are the Yui Operator. Act as the user's CLI proxy and manage Yui without performing Task work.
48
+
49
+ Rules:
50
+
51
+ - Use Yui commands to create and inspect Tasks, manage global and Task Roles, choose Agents, and submit user input.
52
+ - Do not edit files under YUI_HOME directly.
53
+ - Every Task has a protected leader Role; Operator is global and never acts as a Task Leader or Worker.
54
+ - Keep native Agent session interaction intact. Do not emulate Agent slash commands or terminal input.
55
+
56
+ Environment:
57
+
58
+ - YUI_HOME=${yuiHome}
59
+ - YUI_WORKSPACE=${workspace}`;
60
+ }
61
+ function readOperatorSkill() {
62
+ return readFileSync(new URL("../../skills/yui-operator/SKILL.md", import.meta.url), "utf8").trim();
63
+ }
64
+ function mergeEnv(baseEnv = process.env, roleEnv) {
65
+ return { ...baseEnv, ...roleEnv };
66
+ }
@@ -0,0 +1,82 @@
1
+ import { defaultTableWidth, renderTable } from "./table.js";
2
+ export function activeRoleSummary(role) {
3
+ const binding = role.agentBindings[role.activeAgentId];
4
+ return {
5
+ agent: role.activeAgentId,
6
+ model: binding?.config.model ?? "CLI default",
7
+ effort: binding?.config.effort ?? "CLI default"
8
+ };
9
+ }
10
+ export function renderRoleDetails(title, role, input) {
11
+ const bindings = Object.values(role.agentBindings)
12
+ .sort((left, right) => left.agentId.localeCompare(right.agentId));
13
+ const profile = [
14
+ ` Description ${present(role.description)}`,
15
+ ` Responsibilities ${presentList(role.responsibilities)}`,
16
+ ` Constraints ${presentList(role.constraints)}`,
17
+ ` Expected output ${present(role.expectedOutput)}`,
18
+ ` System prompt ${present(role.systemPrompt)}`,
19
+ ` Skills ${presentList(role.skills)}`
20
+ ];
21
+ const overview = [
22
+ ` Kind ${input.kind}`,
23
+ ` Active Agent ${role.activeAgentId}`,
24
+ ...("status" in role ? [` Status ${role.status}`] : []),
25
+ ` Workspace ${role.workspace}`
26
+ ];
27
+ return [
28
+ title,
29
+ "",
30
+ "Role settings",
31
+ ...overview,
32
+ "",
33
+ "Profile",
34
+ ...profile,
35
+ "",
36
+ renderTable("Agent settings", [
37
+ { header: "Agent", minWidth: 5, maxWidth: 20 },
38
+ { header: "Active", minWidth: 6, maxWidth: 6 },
39
+ { header: "Adapter", minWidth: 7, maxWidth: 10 },
40
+ { header: "Model", minWidth: 8, maxWidth: 24 },
41
+ { header: "Effort", minWidth: 8, maxWidth: 16 },
42
+ { header: "Permission", minWidth: 10, maxWidth: 34 },
43
+ { header: "Search", minWidth: 6, maxWidth: 11 },
44
+ { header: "Session", minWidth: 7, maxWidth: 12 }
45
+ ], bindings.map((binding) => bindingRow(binding, role, input.sessions)), defaultTableWidth())
46
+ ].join("\n").concat("\n");
47
+ }
48
+ function bindingRow(binding, role, sessions) {
49
+ return [
50
+ binding.agentId,
51
+ binding.agentId === role.activeAgentId ? "yes" : "",
52
+ binding.adapterId,
53
+ binding.config.model ?? "CLI default",
54
+ binding.config.effort ?? "CLI default",
55
+ permission(binding),
56
+ binding.config.adapterId === "codex"
57
+ ? binding.config.search === true ? "enabled" : "CLI default"
58
+ : "-",
59
+ sessions?.sessions[binding.agentId]?.status ?? "not started"
60
+ ];
61
+ }
62
+ function permission(binding) {
63
+ if (binding.config.adapterId === "codex") {
64
+ const sandbox = binding.config.permission?.sandbox;
65
+ const approval = binding.config.permission?.approval;
66
+ if (sandbox === undefined && approval === undefined)
67
+ return "CLI default";
68
+ return [
69
+ sandbox === undefined ? undefined : `sandbox=${sandbox}`,
70
+ approval === undefined ? undefined : `approval=${approval}`
71
+ ].filter((value) => value !== undefined).join(", ");
72
+ }
73
+ return binding.config.permission?.mode === undefined
74
+ ? "CLI default"
75
+ : `mode=${binding.config.permission.mode}`;
76
+ }
77
+ function present(value) {
78
+ return value === undefined || value.length === 0 ? "-" : value;
79
+ }
80
+ function presentList(values) {
81
+ return values === undefined || values.length === 0 ? "-" : values.join("; ");
82
+ }
@@ -0,0 +1,77 @@
1
+ import { padVisibleEnd, visibleWidth, wrapVisibleText } from "./terminal.js";
2
+ export function renderTable(title, columns, rows, maxWidth) {
3
+ if (columns.length === 0)
4
+ return title;
5
+ const availableWidth = Math.max(20, Math.floor(maxWidth));
6
+ const minimumWidth = tableWidth(columns.map((column) => column.minWidth));
7
+ if (minimumWidth > availableWidth)
8
+ return renderRecords(title, columns, rows, availableWidth);
9
+ const widths = fitColumnWidths(columns, rows, availableWidth);
10
+ return [
11
+ ...wrapVisibleText(title, availableWidth),
12
+ "",
13
+ renderTableRow(columns.map((column) => column.header), widths),
14
+ ` ${widths.map((width) => "─".repeat(width)).join(" ")}`,
15
+ ...rows.flatMap((row) => renderWrappedTableRow(row, widths))
16
+ ].join("\n");
17
+ }
18
+ export function defaultTableWidth() {
19
+ return Math.max(46, Math.min(process.stdout.columns ?? 100, 140));
20
+ }
21
+ function fitColumnWidths(columns, rows, maxWidth) {
22
+ const widths = columns.map((column, columnIndex) => {
23
+ const contentWidths = rows.flatMap((row) => (row[columnIndex] ?? "").split("\n").map(visibleWidth));
24
+ const maxContentWidth = Math.max(visibleWidth(column.header), ...contentWidths);
25
+ return Math.min(column.maxWidth, Math.max(column.minWidth, maxContentWidth));
26
+ });
27
+ while (tableWidth(widths) > maxWidth) {
28
+ const shrink = widths.map((width, index) => ({
29
+ index,
30
+ spare: width - (columns[index]?.minWidth ?? width)
31
+ })).sort((left, right) => right.spare - left.spare)[0];
32
+ if (shrink === undefined || shrink.spare <= 0)
33
+ break;
34
+ widths[shrink.index] = (widths[shrink.index] ?? 1) - 1;
35
+ }
36
+ return widths;
37
+ }
38
+ function renderWrappedTableRow(row, widths) {
39
+ const cells = widths.map((width, index) => wrapVisibleText(row[index] ?? "", width));
40
+ const height = Math.max(...cells.map((cell) => cell.length));
41
+ return Array.from({ length: height }, (_, lineIndex) => renderTableRow(cells.map((cell) => cell[lineIndex] ?? ""), widths));
42
+ }
43
+ function renderTableRow(cells, widths) {
44
+ return ` ${cells.map((cell, index) => padVisibleEnd(cell, widths[index] ?? 1)).join(" ").trimEnd()}`;
45
+ }
46
+ function tableWidth(widths) {
47
+ return 2 + widths.reduce((sum, width) => sum + width, 0) + Math.max(0, widths.length - 1) * 2;
48
+ }
49
+ function renderRecords(title, columns, rows, maxWidth) {
50
+ const indexColumn = columns[0]?.header === "#" ? 0 : undefined;
51
+ const primaryColumn = indexColumn === 0 && columns.length > 1 ? 1 : 0;
52
+ const detailColumns = columns.map((_, index) => index)
53
+ .filter((index) => index !== indexColumn && index !== primaryColumn);
54
+ const labelWidth = Math.max(0, ...detailColumns.map((index) => visibleWidth(columns[index]?.header ?? "")));
55
+ const lines = [...wrapVisibleText(title, maxWidth), ""];
56
+ rows.forEach((row, rowIndex) => {
57
+ if (rowIndex > 0)
58
+ lines.push("");
59
+ const indexValue = indexColumn === undefined ? "" : row[indexColumn] ?? "";
60
+ const primaryValue = row[primaryColumn] ?? "";
61
+ const primaryPrefix = ` ${indexValue.length === 0 ? "" : `${indexValue} `}`;
62
+ const primaryLines = wrapVisibleText(primaryValue, Math.max(1, maxWidth - visibleWidth(primaryPrefix)));
63
+ lines.push(`${primaryPrefix}${primaryLines[0] ?? ""}`.trimEnd());
64
+ lines.push(...primaryLines.slice(1).map((line) => `${" ".repeat(visibleWidth(primaryPrefix))}${line}`));
65
+ for (const columnIndex of detailColumns) {
66
+ const value = row[columnIndex] ?? "";
67
+ if (value.length === 0)
68
+ continue;
69
+ const header = columns[columnIndex]?.header ?? "";
70
+ const prefix = ` ${padVisibleEnd(header, labelWidth)} `;
71
+ const wrapped = wrapVisibleText(value, Math.max(1, maxWidth - visibleWidth(prefix)));
72
+ lines.push(`${prefix}${wrapped[0] ?? ""}`.trimEnd());
73
+ lines.push(...wrapped.slice(1).map((line) => `${" ".repeat(visibleWidth(prefix))}${line}`));
74
+ }
75
+ });
76
+ return lines.join("\n");
77
+ }