@zq-silk/yui 0.2.0 → 0.4.2

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -1,22 +1,24 @@
1
1
  import { roleNotFound, usageError } from "../errors/cliError.js";
2
2
  import { createRoleSessionSet, recordRoleAgentSession } from "../executor/agentExecutor.js";
3
- import { resolveAgentAdapter } from "../executor/agentAdapter.js";
4
- import { resolveAgentEnvironment } from "../agent/agent.js";
3
+ import { resolveEffectiveLaunch } from "../executor/effectiveLaunch.js";
5
4
  import { defaultTableWidth, renderTable } from "../output/table.js";
6
5
  import { activeRoleSummary, renderRoleDetails } from "../output/rolePresentation.js";
7
- import { activeRoleAgentBinding, createGlobalRole, createRoleAgentBinding, switchActiveRoleAgent, updateGlobalRole } from "../role/role.js";
6
+ import { activeRoleAgentBinding, createGlobalRole, createRoleAgentBinding, switchActiveRoleAgent, unbindRoleAgent, updateGlobalRole } from "../role/role.js";
8
7
  import { isSystemRoleName, SYSTEM_ROLE_NAMES, systemRoleDescription } from "../role/systemRoles.js";
9
8
  import { hasAgentConfigOptions, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
9
+ import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
10
+ import { assertRoleRuntimeMutationAllowed } from "./roleRuntimeGuard.js";
10
11
  export function runGlobalRoleCommand(args, store, options = {}) {
11
12
  const [command, ...rest] = args;
12
13
  switch (command) {
13
- case "add": return addRole(rest, store);
14
+ case "add": return addRole(rest, store, options);
14
15
  case "list": return listRoles(rest, store);
15
16
  case "show": return showRole(rest, store);
16
- case "update": return updateRole(rest, store);
17
+ case "update": return updateRole(rest, store, options);
17
18
  case "remove": return removeRole(rest, store);
18
19
  case "bind": return bindRole(rest, store);
19
- case "enter": return enterRole(rest, store, options);
20
+ case "unbind": return unbindRole(rest, store);
21
+ case "enter": return enterRole(rest, store);
20
22
  case "session": return roleSession(rest, store, options);
21
23
  default:
22
24
  throw usageError(command === undefined
@@ -24,25 +26,32 @@ export function runGlobalRoleCommand(args, store, options = {}) {
24
26
  : `Unknown command: role ${command}`);
25
27
  }
26
28
  }
27
- function addRole(args, store) {
29
+ function addRole(args, store, options) {
28
30
  const [rawName, ...tail] = args;
29
31
  const name = roleName(rawName);
30
32
  const parsed = parseRoleOptions(tail, roleOptionSpecs({
31
33
  update: false, includeAgent: true, includeWorkspace: true
32
34
  }));
33
35
  const agentId = required(parsed.one("--agent"), "--agent");
34
- const agent = requireAgent(agentId, store);
35
36
  if (parsed.has("--workspace") && trimmed(parsed.one("--workspace")) === undefined) {
36
37
  throw usageError("--workspace is required.");
37
38
  }
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}.`);
39
+ const now = new Date();
40
+ const created = store.transaction((tx) => {
41
+ assertRoleRuntimeMutationAllowed(tx, { scope: "global", roleName: name }, "creation");
42
+ const agent = requireAgent(agentId, tx);
43
+ const workspace = trimmed(parsed.one("--workspace"))
44
+ ?? tx.getConfig().defaultWorkspace
45
+ ?? process.cwd();
46
+ const binding = patchRoleAgentBinding(createRoleAgentBinding(definition(agent)), parsed);
47
+ const profile = roleProfileFrom(parsed);
48
+ validateConfiguredRoleSkills(options.yuiHome, profile.skills ?? []);
49
+ const role = createGlobalRole(name, [binding], agent.id, workspace, now, profile);
50
+ const result = tx.createGlobalRoleIfAbsent(role);
51
+ if (result === null)
52
+ throw usageError(`Role already exists: ${name}.`);
53
+ return result;
54
+ });
46
55
  return presentRole(`Added role ${name}`, created, store);
47
56
  }
48
57
  function listRoles(args, store) {
@@ -85,10 +94,9 @@ function showRole(args, store) {
85
94
  sessions: store.getGlobalRoleSessionSet(name)
86
95
  });
87
96
  }
88
- function updateRole(args, store) {
97
+ function updateRole(args, store, options) {
89
98
  const [rawName, ...tail] = args;
90
99
  const name = roleName(rawName);
91
- const role = requireRole(name, store);
92
100
  const parsed = parseRoleOptions(tail, roleOptionSpecs({
93
101
  update: true, includeAgent: true, includeWorkspace: true
94
102
  }));
@@ -102,25 +110,35 @@ function updateRole(args, store) {
102
110
  throw usageError("At least one role update option is required.");
103
111
  }
104
112
  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
+ const next = store.transaction((tx) => {
114
+ const role = requireRole(name, tx);
115
+ const changesLaunchContext = hasRoleLaunchContextOptions(parsed);
116
+ const changesAgentConfig = hasAgentConfigOptions(parsed);
117
+ if (changesLaunchContext || changesAgentConfig) {
118
+ assertRoleRuntimeMutationAllowed(tx, {
119
+ scope: "global",
120
+ roleName: role.name
121
+ }, "desired launch configuration update");
113
122
  }
114
- bindings = { ...role.agentBindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
115
- }
116
- const next = {
117
- ...updateGlobalRole(role, {
123
+ let bindings = role.agentBindings;
124
+ if (changesAgentConfig) {
125
+ const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
126
+ const agent = requireAgent(agentId, tx);
127
+ assertOperatorAdapterAvailable(role, agent);
128
+ const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
129
+ bindings = { ...role.agentBindings, [agentId]: patchRoleAgentBinding(binding, parsed) };
130
+ }
131
+ const updated = updateGlobalRole(role, {
118
132
  ...(workspace === undefined ? {} : { workspace }),
119
133
  ...(bindings === role.agentBindings ? {} : { agentBindings: bindings }),
120
134
  ...roleProfilePatch(parsed)
121
- }, new Date())
122
- };
123
- store.saveGlobalRole(next);
135
+ }, new Date());
136
+ if (changesLaunchContext) {
137
+ validateConfiguredRoleSkills(options.yuiHome, updated.skills ?? []);
138
+ }
139
+ tx.saveGlobalRole(updated);
140
+ return updated;
141
+ });
124
142
  return renderRoleDetails(`Updated role ${name}`, next, {
125
143
  kind: isSystemRoleName(name) ? "system" : "global",
126
144
  sessions: store.getGlobalRoleSessionSet(name)
@@ -131,25 +149,51 @@ function bindRole(args, store) {
131
149
  const name = roleName(rawName);
132
150
  const agentId = required(rawAgentId, "Agent id");
133
151
  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));
152
+ const now = new Date();
153
+ const result = store.transaction((tx) => {
154
+ const role = requireRole(name, tx);
155
+ assertRoleRuntimeMutationAllowed(tx, {
156
+ scope: "global",
157
+ roleName: role.name
158
+ }, "desired Agent binding update");
159
+ const agent = requireAgent(agentId, tx);
160
+ assertOperatorAdapterAvailable(role, agent);
161
+ const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
162
+ const withBinding = updateGlobalRole(role, {
163
+ agentBindings: { ...role.agentBindings, [agentId]: binding }
164
+ }, now);
165
+ if (agentId === role.activeAgentId) {
166
+ tx.saveGlobalRole(withBinding);
167
+ return {
168
+ message: `Role ${name} already bound to ${agentId}`,
169
+ role: withBinding
170
+ };
171
+ }
172
+ const existingSet = tx.getGlobalRoleSessionSet(name);
173
+ const activeSession = existingSet?.sessions[existingSet.activeAgentId];
174
+ try {
175
+ const switched = switchActiveRoleAgent(withBinding, existingSet ?? createRoleSessionSet({ scope: "global", roleName: name }, role.activeAgentId, now), agentId, {
176
+ activeRun: false,
177
+ nativeProcessRunning: activeSession !== undefined
178
+ && activeSession.status !== "stopped"
179
+ }, now);
180
+ tx.saveGlobalRoleWithSessionSet(switched.role, switched.sessions);
181
+ return { message: `Bound role ${name} to ${agentId}`, role: switched.role };
182
+ }
183
+ catch (error) {
184
+ throw usageError(error instanceof Error ? error.message : String(error));
185
+ }
186
+ });
187
+ return presentRole(result.message, result.role, store);
188
+ }
189
+ function assertOperatorAdapterAvailable(role, agent) {
190
+ if (role.name !== "operator" || Object.hasOwn(role.agentBindings, agent.id))
191
+ return;
192
+ const existing = Object.values(role.agentBindings).find((binding) => binding.adapterId === agent.adapterId);
193
+ if (existing !== undefined) {
194
+ throw usageError(`Operator already has a ${agent.adapterId} Agent: ${existing.agentId}. `
195
+ + "Update that Agent's configuration, or activate another adapter and "
196
+ + "unbind it before binding this Agent.");
153
197
  }
154
198
  }
155
199
  function removeRole(args, store) {
@@ -158,25 +202,44 @@ function removeRole(args, store) {
158
202
  assertNoArguments(rest, "Role remove usage: yui role remove <role>");
159
203
  if (isSystemRoleName(name))
160
204
  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);
205
+ store.transaction((tx) => {
206
+ const role = requireRole(name, tx);
207
+ assertRoleRuntimeMutationAllowed(tx, {
208
+ scope: "global",
209
+ roleName: role.name
210
+ }, "removal");
211
+ const sessions = tx.getGlobalRoleSessionSet(name);
212
+ if (Object.values(sessions?.sessions ?? {}).some(({ status }) => status !== "stopped")) {
213
+ throw usageError(`GlobalRole is active and cannot be removed: ${name}.`);
214
+ }
215
+ if (!tx.removeGlobalRole(name))
216
+ throw roleNotFound(name);
217
+ });
168
218
  return `Removed role ${name}\n`;
169
219
  }
170
- function enterRole(args, store, options) {
220
+ function unbindRole(args, store) {
221
+ const [rawName, rawAgentId, ...rest] = args;
222
+ const name = roleName(rawName);
223
+ const agentId = required(rawAgentId, "Agent id");
224
+ assertNoArguments(rest, "Role unbind usage: yui role unbind <role> <agent-id>");
225
+ store.transaction((tx) => {
226
+ const role = requireRole(name, tx);
227
+ try {
228
+ const result = unbindRoleAgent(role, tx.getGlobalRoleSessionSet(name), agentId, new Date());
229
+ tx.saveGlobalRoleWithSessionSet(result.role, result.sessions);
230
+ }
231
+ catch (error) {
232
+ throw usageError(error instanceof Error ? error.message : String(error));
233
+ }
234
+ });
235
+ return `Unbound Agent ${agentId} from role ${name}\n`;
236
+ }
237
+ function enterRole(args, store) {
171
238
  const [rawName, ...rest] = args;
172
239
  const name = roleName(rawName);
173
240
  assertNoArguments(rest, "Role enter usage: yui role enter <role>");
174
241
  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 };
242
+ return { kind: "enter", role };
180
243
  }
181
244
  function roleSession(args, store, options) {
182
245
  const [command, rawName, ...tail] = args;
@@ -192,40 +255,48 @@ function roleSession(args, store, options) {
192
255
  if (nativeSessionId.trim() !== nativeSessionId || nativeSessionId.length === 0) {
193
256
  throw usageError("Native session id must not contain surrounding whitespace.");
194
257
  }
195
- const role = requireRole(name, store);
196
- const binding = activeRoleAgentBinding(role);
197
- const agent = requireAgent(binding.agentId, store);
198
258
  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.");
259
+ return store.transaction((tx) => {
260
+ const role = requireRole(name, tx);
261
+ assertRoleRuntimeMutationAllowed(tx, {
262
+ scope: "global",
263
+ roleName: role.name
264
+ }, "manual native session update");
265
+ const binding = activeRoleAgentBinding(role);
266
+ requireAgent(binding.agentId, tx);
267
+ assertSessionProvenance(command, role, nativeSessionId, environment);
268
+ const now = new Date();
269
+ const set = tx.getGlobalRoleSessionSet(name)
270
+ ?? createRoleSessionSet({ scope: "global", roleName: name }, role.activeAgentId, now);
271
+ const existing = set.sessions[role.activeAgentId] ?? null;
272
+ const input = {
273
+ agentId: binding.agentId,
274
+ adapterId: binding.adapterId,
275
+ nativeSessionId,
276
+ policy: "fixed",
277
+ status: environment.YUI_ROLE === name ? "running" : "ready",
278
+ effective: resolveEffectiveLaunch({ role, purpose: "execution" })
279
+ };
280
+ if (command === "record") {
281
+ if (existing !== null && existing.nativeSessionId !== nativeSessionId) {
282
+ throw usageError("GlobalRole session replacement must be explicit.");
283
+ }
284
+ tx.saveGlobalRoleSessionSet(recordRoleAgentSession(set, input, now));
285
+ return `Recorded native session for role ${name}\n`;
213
286
  }
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`;
287
+ if (existing === null) {
288
+ throw usageError("Native session replacement requires an existing native session.");
289
+ }
290
+ if (existing.status !== "stopped") {
291
+ throw usageError("Native session replacement is blocked while the native Agent process is running.");
292
+ }
293
+ if (existing.nativeSessionId === nativeSessionId) {
294
+ throw usageError("Native session replacement requires a different native session identity.");
295
+ }
296
+ required(parsed.one("--reason"), "--reason");
297
+ tx.saveGlobalRoleSessionSet(recordRoleAgentSession(set, input, now));
298
+ return `Replaced native session for role ${name}\n`;
299
+ });
229
300
  }
230
301
  function assertSessionProvenance(command, role, nativeSessionId, environment) {
231
302
  const values = [
@@ -282,35 +353,6 @@ function parseOptions(args, specs) {
282
353
  many: (option) => [...(values.get(option) ?? [])]
283
354
  };
284
355
  }
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
356
  function definition(agent) {
315
357
  return { ...agent, baseArgs: [...agent.baseArgs], environment: [...agent.environment], source: "custom" };
316
358
  }
@@ -1,6 +1,7 @@
1
1
  import { runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
2
2
  import { defaultTableWidth, renderTable } from "../output/table.js";
3
- import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
3
+ import { formatTimestamp } from "../output/timePresentation.js";
4
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
4
5
  const WAKEUP_PREFIX = "leader-wakeup:";
5
6
  const RECOVERY_PREFIX = "leader-recovery:";
6
7
  /**
@@ -23,6 +24,7 @@ function listJobs(args, store) {
23
24
  const jobs = collectJobs(store);
24
25
  if (jobs.length === 0)
25
26
  return "No scheduler jobs found.\n";
27
+ const timeZone = store.getConfig().timeZone;
26
28
  return `${renderTable("Scheduler jobs", [
27
29
  { header: "Job", minWidth: 12, maxWidth: 44 },
28
30
  { header: "Task", minWidth: 6, maxWidth: 20 },
@@ -35,7 +37,7 @@ function listJobs(args, store) {
35
37
  job.taskId,
36
38
  job.kind,
37
39
  job.status,
38
- job.updatedAt,
40
+ formatTimestamp(job.updatedAt, timeZone),
39
41
  job.detail
40
42
  ]), defaultTableWidth())}\n`;
41
43
  }
@@ -55,14 +57,17 @@ function collectJobs(store) {
55
57
  }
56
58
  const failure = store.getLeaderFailure(task.id);
57
59
  const notification = store.getOperatorNotification(task.id);
58
- if (failure !== null || notification !== null) {
60
+ const recoveryNotification = notification?.type === "leader-recovery-failed"
61
+ ? notification
62
+ : null;
63
+ if (failure !== null || recoveryNotification !== null) {
59
64
  jobs.push({
60
65
  id: `${RECOVERY_PREFIX}${task.id}`,
61
66
  taskId: task.id,
62
67
  kind: "leader-recovery",
63
68
  status: "failed",
64
- detail: failure?.message ?? notification?.message ?? "Leader recovery failed.",
65
- updatedAt: failure?.lastFailedAt ?? notification?.updatedAt ?? task.updatedAt
69
+ detail: failure?.message ?? recoveryNotification?.message ?? "Leader recovery failed.",
70
+ updatedAt: failure?.lastFailedAt ?? recoveryNotification?.updatedAt ?? task.updatedAt
66
71
  });
67
72
  }
68
73
  }
@@ -88,13 +93,18 @@ function retryJob(args, store, options) {
88
93
  if (task.status !== "active") {
89
94
  throw usageError(`Leader recovery can only be retried for an active Task: ${task.id}.`);
90
95
  }
91
- if (tx.getLeaderFailure(task.id) === null && tx.getOperatorNotification(task.id) === null) {
96
+ if (tx.getLeaderFailure(task.id) === null
97
+ && tx.getOperatorNotification(task.id)?.type !== "leader-recovery-failed") {
92
98
  throw usageError(`Job not found: ${id}.`);
93
99
  }
94
100
  tx.clearLeaderFailure(task.id);
95
101
  tx.clearOperatorNotification(task.id);
96
- queueLeaderWakeup(tx, task.id, "recovery-retry", now);
102
+ enqueueWork(tx, { kind: "role", taskId: task.id, roleName: "leader" }, "recovery-retry", now, [{ type: "task", id: task.id }]);
97
103
  });
98
- runtime.notifyStateChanged(taskId);
104
+ const target = { kind: "role", taskId, roleName: "leader" };
105
+ if (runtime.notifyMailboxChanged !== undefined)
106
+ runtime.notifyMailboxChanged(target);
107
+ else
108
+ runtime.notifyStateChanged(taskId);
99
109
  return `Retry requested for ${id}\n`;
100
110
  }
@@ -1,18 +1,42 @@
1
1
  import { usageError } from "../errors/cliError.js";
2
+ import { createRoleSessionSet } from "../executor/agentExecutor.js";
3
+ import { listOperatorSessions, prepareOperatorNewSession, prepareOperatorResumeSession } from "../operator/operatorSessionHistory.js";
4
+ import { defaultTableWidth, renderTable } from "../output/table.js";
5
+ import { formatRelativeTimestamp } from "../output/timePresentation.js";
6
+ import { updateGlobalRole } from "../role/role.js";
2
7
  import { submitOperatorMessage } from "./taskCommands.js";
3
- /** Operator is intentionally small: interactive entry is owned by the tmux CLI path. */
8
+ import { readCommandText } from "./textInput.js";
9
+ /** Operator command parsing never launches or attaches a native process. */
4
10
  export function runOperatorCommand(args, store, options = {}) {
5
11
  const [command, ...rest] = args;
6
- if (command !== "submit") {
7
- throw usageError(command === undefined
8
- ? "Operator command is required."
9
- : `Unknown command: operator ${command}`);
12
+ switch (command) {
13
+ case "list": return listSessions(rest, store, options);
14
+ case "new": return newSession(rest, store);
15
+ case "resume": return resumeSession(rest, store);
16
+ case "submit": return submit(rest, store, options);
17
+ default:
18
+ throw usageError(command === undefined
19
+ ? "Operator command is required."
20
+ : `Unknown command: operator ${command}`);
10
21
  }
11
- const usage = "Operator submit usage: yui operator submit <body> [--task <id>].";
22
+ }
23
+ function submit(rest, store, options) {
24
+ const usage = "Operator submit usage: yui operator submit (<body>|--body-file <path|->) [--task <id>].";
12
25
  const positionals = [];
13
26
  let taskId;
27
+ let bodyFile;
14
28
  for (let index = 0; index < rest.length; index += 1) {
15
29
  const value = rest[index];
30
+ if (value === "--body-file") {
31
+ if (bodyFile !== undefined)
32
+ throw usageError("Option may only be specified once: --body-file.", usage);
33
+ const candidate = rest[index + 1];
34
+ if (candidate === undefined || candidate.startsWith("--"))
35
+ throw usageError("--body-file is required.", usage);
36
+ bodyFile = candidate;
37
+ index += 1;
38
+ continue;
39
+ }
16
40
  if (value !== "--task") {
17
41
  if (value.startsWith("--"))
18
42
  throw usageError(`Unsupported option: ${value}.`, usage);
@@ -28,11 +52,137 @@ export function runOperatorCommand(args, store, options = {}) {
28
52
  taskId = candidate.trim();
29
53
  index += 1;
30
54
  }
31
- if (positionals.length !== 1 || positionals[0].trim().length === 0) {
55
+ if (positionals.length > 1)
32
56
  throw usageError(usage);
33
- }
57
+ const body = readCommandText(positionals[0], bodyFile, "--body", usage);
58
+ return {
59
+ kind: "output",
60
+ output: submitOperatorMessage(body, taskId, store, options)
61
+ };
62
+ }
63
+ function listSessions(args, store, options) {
64
+ if (args.length !== 0)
65
+ throw usageError("Operator list usage: yui operator list.");
66
+ const sessions = listOperatorSessions(store.getGlobalRoleSessionSet("operator"));
67
+ const now = options.now?.() ?? new Date();
68
+ const output = sessions.length === 0
69
+ ? "○ No Operator sessions are available.\n"
70
+ : `${renderTable("Operator sessions", [
71
+ { header: "Updated", minWidth: 7, maxWidth: 12 },
72
+ { header: "Agent", minWidth: 6, maxWidth: 16 },
73
+ { header: "Conversation", minWidth: 20, maxWidth: 72 },
74
+ { header: "State", minWidth: 7, maxWidth: 10 }
75
+ ], sessions.map((session) => [
76
+ formatRelativeTimestamp(session.updatedAt, now),
77
+ adapterLabel(session.adapterId),
78
+ session.displayTitle,
79
+ session.state
80
+ ]), options.width ?? defaultTableWidth())}\n\n${sessions.length} session${sessions.length === 1 ? "" : "s"}\n`;
34
81
  return {
35
82
  kind: "output",
36
- output: submitOperatorMessage(positionals[0], taskId, store, options)
83
+ output,
84
+ data: { sessions }
85
+ };
86
+ }
87
+ function newSession(args, store) {
88
+ let agentId;
89
+ for (let index = 0; index < args.length; index += 1) {
90
+ const value = args[index];
91
+ if (value !== "--agent") {
92
+ throw usageError(value.startsWith("--")
93
+ ? `Unsupported option: ${value}.`
94
+ : `Unexpected argument: ${value}.`, "Operator new usage: yui operator new [--agent <id>].");
95
+ }
96
+ if (agentId !== undefined)
97
+ throw usageError("Option may only be specified once: --agent.");
98
+ agentId = requiredValue(args[index + 1], "--agent");
99
+ index += 1;
100
+ }
101
+ const role = requireOperator(store);
102
+ const targetAgentId = agentId ?? role.activeAgentId;
103
+ if (!Object.hasOwn(role.agentBindings, targetAgentId)) {
104
+ throw usageError(`Operator Agent is not bound: ${targetAgentId}.`);
105
+ }
106
+ return { kind: "session", action: "new", targetAgentId };
107
+ }
108
+ function resumeSession(args, store) {
109
+ let ref;
110
+ let last = false;
111
+ for (const value of args) {
112
+ if (value === "--last") {
113
+ if (last)
114
+ throw usageError("Option may only be specified once: --last.");
115
+ last = true;
116
+ continue;
117
+ }
118
+ if (value.startsWith("--"))
119
+ throw usageError(`Unsupported option: ${value}.`);
120
+ if (ref !== undefined) {
121
+ throw usageError("Operator resume accepts only one session reference.");
122
+ }
123
+ ref = value;
124
+ }
125
+ if (last && ref !== undefined) {
126
+ throw usageError("Operator resume accepts either a session reference or --last.");
127
+ }
128
+ const sessions = listOperatorSessions(store.getGlobalRoleSessionSet("operator"));
129
+ const selectedRef = last ? sessions[0]?.ref : ref;
130
+ if (selectedRef === undefined) {
131
+ throw usageError(last
132
+ ? "No Operator session is available to resume."
133
+ : "Operator resume requires a session selection.");
134
+ }
135
+ const selected = sessions.find((session) => session.ref === selectedRef);
136
+ if (selected === undefined)
137
+ throw usageError(`Operator session not found: ${selectedRef}.`);
138
+ const role = requireOperator(store);
139
+ if (!Object.hasOwn(role.agentBindings, selected.agentId)) {
140
+ throw usageError(`Operator session Agent is no longer bound: ${selected.agentId}.`);
141
+ }
142
+ return {
143
+ kind: "session",
144
+ action: "resume",
145
+ targetAgentId: selected.agentId,
146
+ ref: selected.ref
37
147
  };
38
148
  }
149
+ export function applyOperatorSessionControl(control, store, now = new Date()) {
150
+ return store.transaction((tx) => {
151
+ const role = requireOperator(tx);
152
+ const binding = role.agentBindings[control.targetAgentId];
153
+ if (binding === undefined) {
154
+ throw usageError(`Operator Agent is not bound: ${control.targetAgentId}.`);
155
+ }
156
+ const current = tx.getGlobalRoleSessionSet(role.name)
157
+ ?? createRoleSessionSet({ scope: "global", roleName: role.name }, role.activeAgentId, now);
158
+ const sessions = control.action === "new"
159
+ ? prepareOperatorNewSession(current, control.targetAgentId, now)
160
+ : prepareOperatorResumeSession(current, control.ref, now);
161
+ const selected = sessions.sessions[control.targetAgentId];
162
+ if (selected !== undefined && selected.adapterId !== binding.adapterId) {
163
+ throw usageError(`Operator session adapter does not match Agent: ${control.targetAgentId}.`);
164
+ }
165
+ const updatedRole = updateGlobalRole(role, { activeAgentId: control.targetAgentId }, now);
166
+ tx.saveGlobalRoleWithSessionSet(updatedRole, sessions);
167
+ return { role: updatedRole, sessions };
168
+ });
169
+ }
170
+ function requireOperator(store) {
171
+ const role = store.getGlobalRole("operator");
172
+ if (role === null)
173
+ throw usageError("Operator is not configured. Run yui setup first.");
174
+ return role;
175
+ }
176
+ function adapterLabel(adapterId) {
177
+ return adapterId === "codex"
178
+ ? "Codex"
179
+ : adapterId === "claude"
180
+ ? "Claude"
181
+ : adapterId;
182
+ }
183
+ function requiredValue(value, option) {
184
+ if (value === undefined || value.startsWith("--") || value.trim().length === 0) {
185
+ throw usageError(`${option} is required.`);
186
+ }
187
+ return value.trim();
188
+ }