@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,16 +1,29 @@
1
1
  import { createAgentRun } from "../run/agentRun.js";
2
+ import { markYuiRunInput } from "../run/runIdentity.js";
3
+ import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
4
+ import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
5
+ import { effectiveLaunchSnapshotsCompatible } from "../executor/effectiveLaunch.js";
6
+ import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
2
7
  import { recordLeaderFailure } from "./leaderFailure.js";
3
8
  import { createLeaderRecoveryNotification } from "./operatorNotification.js";
4
- export async function processLeaderWakeups(store, delivery, now) {
9
+ import { isSchedulerTaskWorkspaceReady } from "./ports.js";
10
+ export async function processLeaderWakeups(store, delivery, now, selection) {
5
11
  const results = [];
6
- for (const wakeup of store.listPendingWakeups()) {
12
+ const wakeups = selection === undefined || selection.full
13
+ ? store.listPendingWakeups()
14
+ : [...selection.taskIds].flatMap((taskId) => {
15
+ const wakeup = store.getPendingWakeup(taskId);
16
+ return wakeup === null ? [] : [wakeup];
17
+ });
18
+ for (const wakeup of wakeups) {
7
19
  const task = store.getTask(wakeup.taskId);
8
20
  const role = store.getRole(wakeup.taskId, "leader");
9
21
  if (task === null || task.status !== "active" || role === null) {
10
22
  results.push({ taskId: wakeup.taskId, status: "skipped", reason: "unavailable" });
11
23
  continue;
12
24
  }
13
- if (task.repositoryId !== undefined && task.cwd === undefined) {
25
+ const taskWorkspace = store.getTaskWorkspace(task.id);
26
+ if (!isSchedulerTaskWorkspaceReady(task, taskWorkspace)) {
14
27
  results.push({ taskId: task.id, status: "skipped", reason: "workspace-not-ready" });
15
28
  continue;
16
29
  }
@@ -23,73 +36,290 @@ export async function processLeaderWakeups(store, delivery, now) {
23
36
  continue;
24
37
  }
25
38
  // This check deliberately precedes every tmux operation. A pending wake is
26
- // durable state, not text that may be injected into a busy Agent composer.
39
+ // durable state, not text that may be injected into a busy Agent process.
27
40
  if (store.getActiveAgentRun(task.id, role.name) !== null) {
28
41
  results.push({ taskId: task.id, status: "skipped", reason: "busy" });
29
42
  continue;
30
43
  }
31
- const existingSession = store.getRoleSession(task.id, role.name);
44
+ if (typeof store.hasInFlightTurn === "function"
45
+ && store.hasInFlightTurn(task.id, role.name)) {
46
+ results.push({ taskId: task.id, status: "skipped", reason: "busy" });
47
+ continue;
48
+ }
49
+ const reopening = wakeup.reasons.includes("task-reopened");
50
+ const existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
32
51
  let effectiveSession = existingSession;
33
52
  let claimed = false;
53
+ let deliveryAttempted = false;
34
54
  let run = null;
55
+ let prepared;
56
+ let preStartFencePersisted = false;
35
57
  try {
36
- const mode = hasNativeSession(existingSession) ? "resume" : "new";
37
- const input = leaderWakeupInput(task.id, wakeup.reasons, store.getTaskBrief(task.id), store.listDecisions(task.id), store.listMilestones(task.id));
38
- run = createAgentRun(store.nextAgentRunId(task.id), task.id, role.name, mode, input, now);
39
- const prepared = await delivery.prepareRoleSession({
58
+ if (existingSession !== null && !hasNativeSession(existingSession)) {
59
+ const sessionIsTerminal = existingSession.status === "stopped"
60
+ || existingSession.status === "broken";
61
+ if (!sessionIsTerminal) {
62
+ // An opaque live Session has no provider identity that can be safely
63
+ // rebound. A host absence observation is not a verified stop; keep
64
+ // the wake durable until an explicit exact cleanup/reset settles it.
65
+ if (existingSession.launchId !== undefined) {
66
+ await delivery.inspectRole({
67
+ taskId: task.id,
68
+ roleName: role.name,
69
+ agentId: existingSession.agentId,
70
+ adapterId: existingSession.adapterId
71
+ });
72
+ }
73
+ results.push({
74
+ taskId: task.id,
75
+ status: "skipped",
76
+ reason: "recovery-blocked"
77
+ });
78
+ continue;
79
+ }
80
+ if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
81
+ scope: "task",
82
+ taskId: task.id,
83
+ roleName: role.name
84
+ })))) {
85
+ // A stopped/broken Session is eligible for a fresh mode only after
86
+ // its exact runtime cleanup/reservation lane has settled.
87
+ results.push({
88
+ taskId: task.id,
89
+ status: "skipped",
90
+ reason: "recovery-blocked"
91
+ });
92
+ continue;
93
+ }
94
+ }
95
+ const compatibleSession = existingSession !== null
96
+ && effectiveLaunchSnapshotsCompatible(existingSession.effective, role.effective);
97
+ const reopenIdentityDrift = reopening
98
+ && hasNativeSession(existingSession)
99
+ && !compatibleSession;
100
+ if (hasNativeSession(existingSession) && !compatibleSession
101
+ && !reopenIdentityDrift
102
+ && existingSession.status !== "stopped" && existingSession.status !== "broken") {
103
+ throw new Error(`Leader Session is incompatible with desired effective launch: ${task.id}/${role.name}.`);
104
+ }
105
+ const mode = hasNativeSession(existingSession) && compatibleSession ? "resume" : "new";
106
+ const runId = store.peekNextAgentRunId(task.id);
107
+ const input = markYuiRunInput(leaderWakeupInput(task.id, runId, wakeup.reasons, task.projectBindings), runId, taskRoleSessionTitle(task, role.name));
108
+ run = createAgentRun(runId, task.id, role.name, mode, input, now, {
109
+ ...(role.managedWorkspace === undefined
110
+ ? {}
111
+ : { workspace: role.managedWorkspace }),
112
+ effective: role.effective
113
+ });
114
+ const claim = store.saveLeaderDispatch({
115
+ task,
116
+ role,
117
+ run,
118
+ // An incompatible reopened generation is intentionally not rebound to
119
+ // a new native host. Keep the old fixed Session as evidence while
120
+ // claiming a short-lived Run so the existing failure path can record a
121
+ // durable, retryable recovery obligation before any provider call.
122
+ session: reopenIdentityDrift ? null : existingSession,
123
+ wakeup,
124
+ now
125
+ });
126
+ if (claim !== "claimed") {
127
+ results.push({ taskId: task.id, status: "skipped", reason: claim });
128
+ continue;
129
+ }
130
+ claimed = true;
131
+ if (reopenIdentityDrift) {
132
+ throw new Error(`Leader reopen refused after effective launch identity drift: ${task.id}/${role.name}.`);
133
+ }
134
+ prepared = await delivery.prepareRoleSession({
40
135
  taskId: task.id,
41
136
  roleName: role.name,
42
- agentId: role.activeAgentId,
43
- adapterId: role.adapterId,
137
+ agentId: role.effective.agentId,
138
+ adapterId: role.effective.adapterId,
139
+ effective: role.effective,
140
+ workspace: role.effective.workspace.root,
141
+ ...(run.workspace === undefined
142
+ ? {}
143
+ : { managedWorkspace: run.workspace }),
44
144
  mode,
45
- ...(mode === "resume" ? { nativeSessionId: existingSession.nativeSessionId } : {})
145
+ runId: run.id,
146
+ ...(mode === "resume" ? { nativeSessionId: existingSession.nativeSessionId } : {}),
147
+ beforeHostStart: (preflight) => {
148
+ persistPreStartFence(store, task, role, run, existingSession, mode, preflight, now);
149
+ effectiveSession = preflightSession(run.effective, existingSession, mode, preflight);
150
+ preStartFencePersisted = true;
151
+ }
46
152
  });
153
+ // A fresh Codex host may already carry the exact Run prompt in its
154
+ // launch argv. Once preparation returns that transport fact, any
155
+ // later readiness or aggregate-write failure is delivery uncertainty,
156
+ // not a launch failure: preserve the Run and its reservation for the
157
+ // matching provider Hook instead of terminalizing it.
158
+ deliveryAttempted = prepared.inputSubmittedAtLaunch === true;
159
+ // Persist the exact preparation fence before waiting on provider
160
+ // readiness. A pre-input lifecycle Hook may fire during that wait; it
161
+ // must be able to resolve this Run/Session/launch generation from durable
162
+ // state rather than an in-memory delivery object. Fresh providers that
163
+ // discover their native Session later intentionally persist `null`.
164
+ if (!preStartFencePersisted) {
165
+ const preparedFenceSession = prepared.session === undefined
166
+ ? existingSession
167
+ : validateReadySession(run.effective, existingSession, mode, prepared.session);
168
+ effectiveSession = preparedFenceSession;
169
+ store.saveRoleRunPrepared({
170
+ task,
171
+ role,
172
+ run,
173
+ session: preparedFenceSession,
174
+ ...(prepared.launchId === undefined ? {} : { launchId: prepared.launchId }),
175
+ now
176
+ });
177
+ }
47
178
  const ready = await delivery.waitUntilReady(prepared);
179
+ deliveryAttempted = deliveryAttempted
180
+ || ready.prepared.inputSubmittedAtLaunch === true;
48
181
  const latestTask = store.getTask(task.id);
49
182
  if (latestTask === null || latestTask.status !== "active") {
183
+ delivery.forgetPrepared?.({
184
+ taskId: task.id,
185
+ roleName: role.name,
186
+ runId: run.id,
187
+ ...(prepared.launchId === undefined
188
+ ? {}
189
+ : { launchId: prepared.launchId })
190
+ });
50
191
  results.push({ taskId: task.id, status: "skipped", reason: "unavailable" });
51
192
  continue;
52
193
  }
53
- effectiveSession = validateReadySession(role.activeAgentId, existingSession, mode, ready.session);
54
- const claim = store.saveLeaderDispatch({ task, role, run, session: effectiveSession, wakeup, now });
55
- if (claim !== "claimed") {
56
- results.push({ taskId: task.id, status: "skipped", reason: claim });
194
+ effectiveSession = validateReadySession(run.effective, existingSession, mode, ready.session);
195
+ store.saveRoleRunPrepared({
196
+ task,
197
+ role,
198
+ run,
199
+ session: effectiveSession,
200
+ ...(ready.prepared.launchId === undefined
201
+ ? {}
202
+ : { launchId: ready.prepared.launchId }),
203
+ now
204
+ });
205
+ if (ready.prepared.inputSubmittedAtLaunch === true) {
206
+ // A fresh Codex command may carry the exact first prompt in its launch
207
+ // argv. That is transport evidence only: the matching Provider Hook
208
+ // still owns Run acceptance. Persist the push fence without writing a
209
+ // second terminal prompt, then leave the reservation for the async
210
+ // SessionStart/UserPromptSubmit fold.
211
+ store.saveRoleRunDelivery({
212
+ task,
213
+ role,
214
+ run,
215
+ session: effectiveSession,
216
+ ...(ready.prepared.launchId === undefined
217
+ ? {}
218
+ : { launchId: ready.prepared.launchId }),
219
+ now
220
+ });
221
+ delivery.forgetPrepared?.({
222
+ taskId: task.id,
223
+ roleName: role.name,
224
+ runId: run.id,
225
+ ...(ready.prepared.launchId === undefined
226
+ ? {}
227
+ : { launchId: ready.prepared.launchId })
228
+ });
229
+ results.push({ taskId: task.id, runId: run.id, status: "dispatched" });
57
230
  continue;
58
231
  }
59
- claimed = true;
60
- await delivery.sendOnce({
232
+ deliveryAttempted = true;
233
+ const outcome = await delivery.sendOnce({
61
234
  delivery: ready,
62
- receiptId: `agent-run:${run.id}`,
235
+ receiptId: formatAgentRunReceiptId(task.id, run.id),
63
236
  text: input
64
237
  });
65
- store.saveRoleRunDelivery({ task, role, run, session: effectiveSession, now });
66
- results.push({ taskId: task.id, status: "dispatched" });
67
- }
68
- catch (error) {
69
- const detail = error instanceof Error ? error.message : String(error);
70
- const message = `Leader dispatch failed: ${detail}`;
71
- store.saveLeaderDispatchFailure({
238
+ if (outcome === "busy" || outcome === "unavailable") {
239
+ results.push({
240
+ taskId: task.id,
241
+ runId: run.id,
242
+ status: "skipped",
243
+ reason: "not-ready"
244
+ });
245
+ continue;
246
+ }
247
+ store.saveRoleRunDelivery({
72
248
  task,
73
249
  role,
250
+ run,
74
251
  session: effectiveSession,
75
- failure: recordLeaderFailure(task.id, effectiveSession?.nativeSessionId ?? "(unregistered)", message, now, store.getLeaderFailure(task.id)),
76
- notification: createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)),
77
- ...(claimed && run !== null ? { claimed: { run, wakeup } } : {}),
252
+ ...(ready.prepared.launchId === undefined
253
+ ? {}
254
+ : { launchId: ready.prepared.launchId }),
78
255
  now
79
256
  });
80
- results.push({ taskId: task.id, status: "failed", error: message });
257
+ results.push({ taskId: task.id, runId: run.id, status: "dispatched" });
258
+ }
259
+ catch (error) {
260
+ const detail = error instanceof Error ? error.message : String(error);
261
+ const message = `Leader dispatch failed: ${detail}`;
262
+ if (claimed && run !== null) {
263
+ // Once delivery begins, a send may have succeeded even when receipt
264
+ // observation or the aggregate write failed. Preserve the exact
265
+ // durable Run and let receipt-backed active delivery recover it.
266
+ if (deliveryAttempted) {
267
+ results.push({
268
+ taskId: task.id,
269
+ runId: run.id,
270
+ status: "failed",
271
+ reason: "delivery-uncertain",
272
+ error: message
273
+ });
274
+ continue;
275
+ }
276
+ delivery.forgetPrepared?.({
277
+ taskId: task.id,
278
+ roleName: role.name,
279
+ runId: run.id,
280
+ ...(prepared?.launchId === undefined
281
+ ? {}
282
+ : { launchId: prepared.launchId })
283
+ });
284
+ const failureResult = store.saveLeaderDispatchFailure({
285
+ task,
286
+ role,
287
+ session: effectiveSession,
288
+ claimed: { run, wakeup },
289
+ failure: recordLeaderFailure(task.id, effectiveSession?.nativeSessionId ?? "(unregistered)", message, now, store.getLeaderFailure(task.id)),
290
+ notification: createLeaderRecoveryNotification(task.id, message, now, store.getOperatorNotification(task.id)),
291
+ now
292
+ });
293
+ if (failureResult === "state-changed") {
294
+ results.push({ taskId: task.id, status: "skipped", reason: "state-changed" });
295
+ }
296
+ else {
297
+ results.push({ taskId: task.id, runId: run.id, status: "failed", error: message });
298
+ }
299
+ continue;
300
+ }
301
+ results.push({
302
+ taskId: task.id,
303
+ runId: run?.id,
304
+ status: "failed",
305
+ reason: "not-ready",
306
+ error: message
307
+ });
81
308
  }
82
309
  }
83
310
  return results;
84
311
  }
85
- function validateReadySession(activeAgentId, existing, mode, session) {
312
+ function validateReadySession(effective, existing, mode, session) {
86
313
  if (mode === "new" && session === null)
87
314
  return null;
88
315
  if (session === null)
89
316
  throw new Error("Leader resume returned no fixed native session.");
90
- if (session.agentId !== activeAgentId) {
317
+ if (session.agentId !== effective.agentId || session.adapterId !== effective.adapterId) {
91
318
  throw new Error(`Ready session belongs to another Agent: ${session.agentId}.`);
92
319
  }
320
+ if (!effectiveLaunchSnapshotsCompatible(session.effective, effective)) {
321
+ throw new Error("Ready Leader session effective snapshot changed.");
322
+ }
93
323
  if (!hasNativeSession(session)) {
94
324
  throw new Error("Ready Leader session has no native session id.");
95
325
  }
@@ -98,46 +328,57 @@ function validateReadySession(activeAgentId, existing, mode, session) {
98
328
  }
99
329
  return { ...session, status: "running" };
100
330
  }
331
+ function persistPreStartFence(store, task, role, run, existingSession, mode, preflight, now) {
332
+ if (preflight.owner.scope !== "task"
333
+ || preflight.owner.taskId !== task.id
334
+ || preflight.owner.roleName !== role.name
335
+ || preflight.runId !== run.id
336
+ || preflight.launchId.trim().length === 0) {
337
+ throw new Error(`Pre-start launch fence changed the Leader Run: ${task.id}/${role.name}.`);
338
+ }
339
+ const session = preflightSession(run.effective, existingSession, mode, preflight);
340
+ store.saveRoleRunPrepared({
341
+ task,
342
+ role,
343
+ run,
344
+ session,
345
+ launchId: preflight.launchId,
346
+ now
347
+ });
348
+ }
349
+ function preflightSession(effective, existing, mode, preflight) {
350
+ const session = preflight.nativeSessionId === undefined
351
+ ? null
352
+ : {
353
+ agentId: preflight.agentId,
354
+ adapterId: preflight.adapterId,
355
+ nativeSessionId: preflight.nativeSessionId,
356
+ launchId: preflight.launchId,
357
+ status: "ready",
358
+ effective: preflight.effective
359
+ };
360
+ return validateReadySession(effective, existing, mode, session);
361
+ }
101
362
  function hasNativeSession(session) {
102
363
  return session !== null &&
103
364
  typeof session.nativeSessionId === "string" &&
104
365
  session.nativeSessionId.trim().length > 0;
105
366
  }
106
- function leaderWakeupInput(taskId, reasons, brief, decisions, milestones) {
367
+ function leaderWakeupInput(taskId, runId, reasons, projectBindings) {
107
368
  const lines = [
108
- `Yui wakeup reasons: ${reasons.join(", ")}.`
369
+ "Follow the injected yui-leader Skill for this Yui wakeup.",
370
+ `Current Leader Run: ${runId}.`,
371
+ `For every Leader decision, milestone, or Work Item lifecycle command that is meaningful progress, carry this exact current-turn assertion on that command: YUI_LEADER_ACTION_RUN_ID=${runId} YUI_LEADER_ACTION_RECEIPT_ID=${formatAgentRunReceiptId(taskId, runId)}. The native Session environment may retain an older YUI_RUN_ID/launch; never copy those values, and never reuse this assertion after the turn changes.`,
372
+ `Yui wakeup reasons: ${reasons.join(", ")}.`,
373
+ `Read the authoritative context with yui task context ${taskId}.`,
374
+ "Keep the context layers separate: Yui Core owns durable identity, lifecycle, access, workspace, and exact-yield safety; the generic role Skill owns portable collaboration behavior; Project Policy/Knowledge owns project-specific build, test, migration, release, and review rules; the Task Contract owns this Task's objective, scope, acceptance, and evidence.",
375
+ "For role-run-stalled or runtime-health attention, diagnose from the exact Run/Event/Session and related WorkItem/Review/Integration records. Preserve the current fence and write a Task Message only for a new root cause, impact, recovery action, acceptance decision, or user-relevant conclusion; an unchanged healthy wait is zero Message.",
376
+ projectBindings.length === 0
377
+ ? "This Task has no bound Project Policy; do not invent repository-specific rules."
378
+ : `Project Policy references: ${projectBindings.map((binding) => `${binding.directory} (${binding.projectId})`).join(", ")}. Read each with yui project show <project>, then yui project knowledge list <project> and yui project knowledge show <project> <knowledge>.`,
379
+ "Use narrower Task message, WorkItem, decision, milestone, and input commands only when a specific record needs closer inspection.",
380
+ `When the requested outcome is finished and there are no active Worker Runs or unresolved inputs, complete the Task with yui task complete ${taskId} --summary-file - and a quoted heredoc containing the final outcome and evidence.`,
381
+ `Before ending this turn, if the Task was not completed and no InputRequest terminalized this Run, release the active fence with yui task run yield ${runId} --summary-file - and a quoted heredoc containing the current result or waiting state. In particular, yield before waiting for Worker results; do not end the native turn while this Run remains active. The yield command must be the final tool action: after it succeeds, stop immediately and do not inspect, poll, accept, or perform further work in the same native turn.`
109
382
  ];
110
- if (brief !== null) {
111
- lines.push(`Objective: ${brief.objective}`);
112
- if (brief.boundaries.length > 0) {
113
- lines.push("Boundaries:");
114
- for (const boundary of brief.boundaries) {
115
- lines.push(` - ${boundary}`);
116
- }
117
- }
118
- if (brief.currentFocus.trim().length > 0) {
119
- lines.push(`Current focus: ${brief.currentFocus}`);
120
- }
121
- if (brief.leaderSummary.trim().length > 0) {
122
- lines.push(`Leader summary: ${brief.leaderSummary}`);
123
- }
124
- }
125
- const activeDecisions = decisions.filter((d) => d.status === "active").slice(-3);
126
- if (activeDecisions.length > 0) {
127
- lines.push("Active decisions:");
128
- for (const decision of activeDecisions) {
129
- lines.push(` - ${decision.title}: ${decision.rationale}`);
130
- }
131
- }
132
- const recentMilestones = [...milestones]
133
- .sort((a, b) => b.createdAt.localeCompare(a.createdAt))
134
- .slice(0, 3);
135
- if (recentMilestones.length > 0) {
136
- lines.push("Recent milestones:");
137
- for (const milestone of recentMilestones) {
138
- lines.push(` - ${milestone.title}`);
139
- }
140
- }
141
- lines.push(`Inspect yui task context ${taskId}, which includes open and recently resolved input requests; then continue Leader stewardship. Use narrower show/list commands only when one record needs closer inspection.`);
142
383
  return lines.join("\n");
143
384
  }
@@ -1,85 +1,148 @@
1
- export async function processOperatorInputNotifications(store, delivery) {
2
- const requests = store.listOpenInputRequests();
3
- if (requests.length === 0)
1
+ import { createInputRequestOperatorPresentation, createLeaderRecoveryOperatorPresentation, createLeaderStallOperatorPresentation, createTaskTerminalOperatorPresentation } from "../interaction/operatorPresentation.js";
2
+ export async function processOperatorInputNotifications(store, delivery, selection, now = new Date()) {
3
+ if (selection !== undefined && !selection.full && !selection.operator)
4
4
  return [];
5
+ const targetMailbox = { kind: "operator" };
6
+ const mailbox = store.getWorkMailbox(targetMailbox);
7
+ if (mailbox === null || (mailbox.pending === null && mailbox.processing === null))
8
+ return [];
9
+ const pending = mailbox.pending;
10
+ const claim = store.claimWorkMailbox({
11
+ target: targetMailbox,
12
+ batchId: pending === null
13
+ ? "operator-recovery"
14
+ : `operator:${pending.fromSequence}-${pending.toSequence}`,
15
+ owner: "controller",
16
+ now
17
+ });
18
+ if (claim.status === "empty")
19
+ return [];
20
+ const processing = claim.processing;
21
+ const requests = processing.batch.refs
22
+ .flatMap((ref) => ref.type === "input"
23
+ ? [store.getInputRequest(ref.taskId, ref.id)]
24
+ : [])
25
+ .filter((request) => request !== null && request.status === "open")
26
+ .map((request) => ({ kind: "input", request }));
27
+ const notifications = processing.batch.refs
28
+ .filter((ref) => ref.type === "task")
29
+ .map((ref) => store.getOperatorNotification(ref.id))
30
+ .filter((notification) => notification !== null)
31
+ .map((notification) => ({ kind: "notification", notification }));
32
+ const attentions = deduplicateAttention([...requests, ...notifications]);
33
+ if (attentions.length === 0) {
34
+ store.completeWorkMailbox(targetMailbox, processing.batchId);
35
+ return [];
36
+ }
5
37
  const target = store.getOperatorDeliveryTarget();
6
38
  if (target === null || delivery.notifyOperatorInputOnce === undefined) {
7
39
  const reason = target === null ? "operator-unavailable" : "delivery-unsupported";
8
- return requests.map((request) => skipped(request, reason));
40
+ store.releaseWorkMailbox(targetMailbox, processing.batchId);
41
+ return attentions.map((attention) => skipped(attention, reason));
9
42
  }
10
43
  const results = [];
11
- for (const [index, request] of requests.entries()) {
44
+ for (const [index, attention] of attentions.entries()) {
12
45
  try {
46
+ const presentation = createAttentionPresentation(attention, store);
13
47
  const outcome = await delivery.notifyOperatorInputOnce({
14
48
  ...target,
15
- receiptId: `input-request:${request.id}`,
16
- text: renderOperatorInputNotification(request)
49
+ receiptId: presentation.receiptId,
50
+ text: presentation.text
17
51
  });
18
52
  if (outcome === "unavailable") {
19
- results.push(skipped(request, "operator-unavailable"));
20
- results.push(...requests.slice(index + 1).map((pending) => (skipped(pending, "operator-unavailable"))));
53
+ store.releaseWorkMailbox(targetMailbox, processing.batchId);
54
+ results.push(skipped(attention, "operator-unavailable"));
55
+ results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-unavailable"))));
21
56
  break;
22
57
  }
23
58
  else if (outcome === "not-ready") {
24
- results.push(skipped(request, "operator-not-ready"));
25
- results.push(...requests.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
59
+ store.releaseWorkMailbox(targetMailbox, processing.batchId);
60
+ results.push(skipped(attention, "operator-not-ready"));
61
+ results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
26
62
  break;
27
63
  }
28
64
  else {
29
65
  results.push({
30
- inputRequestId: request.id,
31
- taskId: request.taskId,
66
+ ...attentionIdentity(attention),
32
67
  status: outcome
33
68
  });
34
69
  if (outcome === "sent") {
35
- results.push(...requests.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
70
+ if (index === attentions.length - 1) {
71
+ store.completeWorkMailbox(targetMailbox, processing.batchId);
72
+ }
73
+ else {
74
+ store.releaseWorkMailbox(targetMailbox, processing.batchId);
75
+ }
76
+ results.push(...attentions.slice(index + 1).map((pending) => (skipped(pending, "operator-not-ready"))));
36
77
  break;
37
78
  }
38
79
  }
39
80
  }
40
81
  catch (error) {
82
+ store.releaseWorkMailbox(targetMailbox, processing.batchId);
41
83
  results.push({
42
- inputRequestId: request.id,
43
- taskId: request.taskId,
84
+ ...attentionIdentity(attention),
44
85
  status: "failed",
45
86
  error: error instanceof Error ? error.message : String(error)
46
87
  });
88
+ break;
47
89
  }
48
90
  }
91
+ if (results.length > 0 && results.every((result) => result.status === "already-sent")) {
92
+ store.completeWorkMailbox(targetMailbox, processing.batchId);
93
+ }
49
94
  return results;
50
95
  }
51
- function renderOperatorInputNotification(request) {
52
- const recommendedChoiceKey = request.policy.kind === "recommended"
53
- ? request.policy.recommendedChoiceKey
54
- : undefined;
55
- const recommendedChoice = recommendedChoiceKey === undefined
56
- ? undefined
57
- : request.choices.find((choice) => choice.key === recommendedChoiceKey);
58
- return [
59
- "A Task Leader is waiting for user input. Present this question to the user; do not answer it yourself.",
60
- `Task: ${request.taskId}`,
61
- `Input: ${request.id}`,
62
- `Question: ${request.question}`,
63
- ...(request.choices.length === 0
64
- ? ["Answer type: free text"]
65
- : ["Choices:", ...request.choices.map((choice) => ` ${choice.key}: ${choice.label}`)]),
66
- ...(request.policy.kind === "required"
67
- ? ["Decision policy: user response required; there is no automatic fallback."]
68
- : [
69
- `Agent recommendation: ${recommendedChoice.key}: ${recommendedChoice.label}`,
70
- `Automatic fallback after: ${request.policy.timeoutAt}`
71
- ]),
72
- `Inspect: yui task input show ${request.id}`,
73
- request.choices.length === 0
74
- ? `After the user replies: yui task input answer ${request.id} --text "<answer>"`
75
- : `After the user chooses: yui task input answer ${request.id} --choice <key>`
76
- ].join("\n");
77
- }
78
- function skipped(request, reason) {
96
+ function skipped(attention, reason) {
79
97
  return {
80
- inputRequestId: request.id,
81
- taskId: request.taskId,
98
+ ...attentionIdentity(attention),
82
99
  status: "skipped",
83
100
  reason
84
101
  };
85
102
  }
103
+ function attentionIdentity(attention) {
104
+ if (attention.kind === "input") {
105
+ return { inputRequestId: attention.request.id, taskId: attention.request.taskId };
106
+ }
107
+ if (attention.notification.type === "leader-recovery-failed") {
108
+ return {
109
+ recoveryTaskId: attention.notification.taskId,
110
+ taskId: attention.notification.taskId
111
+ };
112
+ }
113
+ if (attention.notification.type === "leader-stalled") {
114
+ return {
115
+ stallTaskId: attention.notification.taskId,
116
+ taskId: attention.notification.taskId
117
+ };
118
+ }
119
+ return {
120
+ terminalTaskId: attention.notification.taskId,
121
+ taskId: attention.notification.taskId
122
+ };
123
+ }
124
+ function createAttentionPresentation(attention, store) {
125
+ if (attention.kind === "input") {
126
+ return createInputRequestOperatorPresentation(attention.request, store.getPresentationContext());
127
+ }
128
+ if (attention.notification.type === "leader-recovery-failed") {
129
+ return createLeaderRecoveryOperatorPresentation(attention.notification);
130
+ }
131
+ return attention.notification.type === "leader-stalled"
132
+ ? createLeaderStallOperatorPresentation(attention.notification)
133
+ : createTaskTerminalOperatorPresentation(attention.notification);
134
+ }
135
+ function deduplicateAttention(attentions) {
136
+ const seen = new Set();
137
+ return attentions.filter((attention) => {
138
+ const key = attention.kind === "input"
139
+ ? `input:${attention.request.taskId}:${attention.request.id}`
140
+ : attention.notification.type === "leader-stalled"
141
+ ? `stall:${attention.notification.taskId}:${attention.notification.runId}:${attention.notification.progressAt}`
142
+ : `recovery:${attention.notification.taskId}:${attention.notification.createdAt}`;
143
+ if (seen.has(key))
144
+ return false;
145
+ seen.add(key);
146
+ return true;
147
+ });
148
+ }