@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,3 +1,6 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
3
+ import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
1
4
  export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes) {
2
5
  const taskOpenInputRequestCount = store.listInputRequests(taskId)
3
6
  .filter((request) => request.status === "open").length;
@@ -12,13 +15,16 @@ export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes) {
12
15
  export function renderTaskRoleRuntimeStatus(status) {
13
16
  const activeRun = status.activeRun === null
14
17
  ? "-"
15
- : `${status.activeRun.id} (${status.activeRun.deliveredAt === undefined ? "queued" : "delivered"})`;
18
+ : `${status.activeRun.id} (${activeRunDeliveryLabel(status.activeRun)})`;
16
19
  const activeWork = status.activeWork === null
17
20
  ? "-"
18
21
  : `${status.activeWork.id} (${status.activeWork.status}) ${status.activeWork.title}`;
19
22
  const nativeSession = status.nativeSession === null
20
23
  ? "not recorded"
21
- : `${status.nativeSession.nativeSessionId} (${status.nativeSession.status}, ${status.nativeSession.adapterId})`;
24
+ : `${status.nativeSession.nativeSessionId} (${status.nativeSession.status}, ${status.nativeSession.adapterId}, effective r${status.nativeSession.effective.sourceDesiredRevision})`;
25
+ const effectiveLaunch = status.effectiveLaunch === null
26
+ ? "not started"
27
+ : `${status.effectiveLaunch.agentId}/${status.effectiveLaunch.adapterId}; r${status.effectiveLaunch.sourceDesiredRevision}; Profile intent=${status.effectiveLaunch.profileAccess}; permission=${status.effectiveLaunch.permission.strategy}`;
22
28
  const tmux = status.tmux.state === "missing"
23
29
  ? "missing"
24
30
  : [
@@ -28,11 +34,7 @@ export function renderTaskRoleRuntimeStatus(status) {
28
34
  status.tmux.target
29
35
  ].filter((value) => value !== undefined).join(", ");
30
36
  const workspaceDetails = status.workspace.managed
31
- ? [
32
- ` Repository ${status.workspace.repositoryId}`,
33
- ` Branch ${status.workspace.branch}`,
34
- ` Base ${status.workspace.baseRef} (${status.workspace.baseCommit})`
35
- ]
37
+ ? status.workspace.entries.map((entry) => (` Project ${entry.directory} (${entry.access}) ${entry.branch} @ ${entry.baseCommit}`))
36
38
  : [];
37
39
  return [
38
40
  `Task Role status: ${status.taskId}/${status.roleName}`,
@@ -41,12 +43,23 @@ export function renderTaskRoleRuntimeStatus(status) {
41
43
  ` Reason ${status.healthReason}`,
42
44
  ` Open inputs ${status.openInputRequestCount}`,
43
45
  ` Agent ${status.agentId}`,
46
+ ` Desired launch r${status.desiredRevision}; Profile intent=${status.role.defaultAccess}`,
47
+ ` Effective launch ${effectiveLaunch}`,
48
+ ` Desired drift ${status.effectiveLaunch === null
49
+ ? "-"
50
+ : status.launchDrift ? "pending next launch" : "none"}`,
51
+ ` Run/session ${status.runSessionDrift ? "snapshot mismatch" : "snapshot consistent"}`,
44
52
  ` Role state ${status.role.status}`,
45
53
  ` Active work ${activeWork}`,
46
54
  ` Active run ${activeRun}`,
55
+ ` Run attention ${status.stall.active
56
+ ? `needs-attention (${status.stall.kind ?? "execution-stalled"}; no durable progress since ${status.stall.progressAt ?? "unknown"})`
57
+ : "none"}`,
47
58
  ` Native session ${nativeSession}`,
59
+ ` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
60
+ ` Fresh launch ${status.freshLaunchAllowed ? "allowed" : "blocked"}`,
48
61
  ` tmux pane ${tmux}`,
49
- ` Workspace ${status.workspace.path}`,
62
+ ` Workspace ${status.workspace.managed ? status.workspace.root : status.workspace.path}`,
50
63
  ...workspaceDetails
51
64
  ].join("\n").concat("\n");
52
65
  }
@@ -56,8 +69,22 @@ export function taskRoleActiveWorkLabel(status) {
56
69
  return status.activeRun === null ? "-" : status.activeRun.id;
57
70
  }
58
71
  export function taskRoleNativeSessionLabel(status) {
72
+ if (status.runtimeCleanupPending && status.nativeSession === null)
73
+ return "reset (cleanup-pending)";
59
74
  return status.nativeSession?.status ?? "unbound";
60
75
  }
76
+ export function inspectTaskRoleSessionRecovery(taskId, roleName, store) {
77
+ const sessions = store.getTaskRoleSessionSet(taskId, roleName);
78
+ const target = runtimeLifecycleTarget({ scope: "task", taskId, roleName });
79
+ const runtimeMailbox = store.getWorkMailbox(target);
80
+ return {
81
+ taskId,
82
+ roleName,
83
+ runtimeCleanupPending: hasRuntimeCleanupObligation(runtimeMailbox),
84
+ freshLaunchAllowed: (sessions === null
85
+ || sessions.sessions[sessions.activeAgentId] === undefined) && !hasRuntimeLifecycleWork(runtimeMailbox)
86
+ };
87
+ }
61
88
  export function taskRoleOpenInputLabel(status) {
62
89
  return status.roleName === "leader" && status.openInputRequestCount > 0
63
90
  ? String(status.openInputRequestCount)
@@ -76,7 +103,15 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
76
103
  ? null
77
104
  : store.getWorkItem(taskId, activeRun.workItemId);
78
105
  const sessions = store.getTaskRoleSessionSet(taskId, role.name);
79
- const nativeSession = sessions?.sessions[role.activeAgentId] ?? null;
106
+ const effectiveAgentId = activeRun?.effective.agentId ?? sessions?.activeAgentId;
107
+ const nativeSession = effectiveAgentId === undefined
108
+ ? null
109
+ : sessions?.sessions[effectiveAgentId] ?? null;
110
+ const effectiveLaunch = activeRun?.effective ?? nativeSession?.effective ?? null;
111
+ const runSessionDrift = activeRun !== null
112
+ && nativeSession !== null
113
+ && !isDeepStrictEqual(activeRun.effective, nativeSession.effective);
114
+ const recovery = inspectTaskRoleSessionRecovery(taskId, role.name, store);
80
115
  const tmux = pane === undefined
81
116
  ? { state: "missing" }
82
117
  : {
@@ -86,15 +121,42 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
86
121
  ...(pane.pid === undefined ? {} : { pid: pane.pid }),
87
122
  currentCommand: pane.currentCommand
88
123
  };
89
- const managedWorkspace = store.getRoleWorkspace(taskId, role.name);
124
+ // The active Run snapshot is authoritative for the live Role session. It
125
+ // may point at a ReviewRound-owned workspace, which is intentionally
126
+ // distinct from the WorkItem Develop workspace.
127
+ const managedWorkspace = activeRun?.workspace
128
+ ?? (activeRun?.workItemId === undefined
129
+ ? store.getTaskWorkspace(taskId)
130
+ : store.getWorkItemWorkspace(taskId, activeRun.workItemId));
90
131
  const workspace = managedWorkspace === null
91
132
  ? { managed: false, path: role.workspace }
92
133
  : { ...managedWorkspace, managed: true };
93
- const health = calculateHealth(role, activeRun, nativeSession, tmux, openInputRequestCount);
134
+ const events = store.listEvents(taskId);
135
+ const stalled = activeRun !== null && isRoleRunStalled(events, activeRun.id);
136
+ const stallProgressAt = activeRun === null
137
+ ? undefined
138
+ : latestStallProgressAt(events, activeRun.id);
139
+ const stallKind = activeRun === null
140
+ ? undefined
141
+ : latestStallKind(events, activeRun.id);
142
+ const health = calculateHealth(role, activeRun, nativeSession, recovery.runtimeCleanupPending, tmux, openInputRequestCount, stalled);
143
+ const stall = activeRun === null
144
+ ? { active: false }
145
+ : {
146
+ active: stalled,
147
+ ...(stallProgressAt === undefined
148
+ ? {}
149
+ : { progressAt: stallProgressAt }),
150
+ ...(stallKind === undefined ? {} : { kind: stallKind })
151
+ };
94
152
  return {
95
- taskId,
96
- roleName: role.name,
97
- agentId: role.activeAgentId,
153
+ ...recovery,
154
+ agentId: effectiveLaunch?.agentId ?? role.activeAgentId,
155
+ desiredRevision: role.launchRevision,
156
+ effectiveLaunch,
157
+ launchDrift: effectiveLaunch !== null
158
+ && effectiveLaunch.sourceDesiredRevision !== role.launchRevision,
159
+ runSessionDrift,
98
160
  ...health,
99
161
  openInputRequestCount,
100
162
  role,
@@ -102,16 +164,40 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
102
164
  activeWork,
103
165
  nativeSession,
104
166
  tmux,
105
- workspace
167
+ workspace,
168
+ stall
106
169
  };
107
170
  }
108
- function calculateHealth(role, activeRun, nativeSession, tmux, openInputRequestCount) {
171
+ function latestStallKind(events, runId) {
172
+ const event = [...events]
173
+ .filter((candidate) => candidate.type === "run.stalled" && candidate.payload.runId === runId)
174
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
175
+ return event?.payload.kind === "delivery-stalled" || event?.payload.kind === "execution-stalled"
176
+ ? event.payload.kind
177
+ : undefined;
178
+ }
179
+ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending, tmux, openInputRequestCount, stalled) {
180
+ if (runtimeCleanupPending && nativeSession === null) {
181
+ return {
182
+ health: "needs-attention",
183
+ healthReason: "the native Session was reset; verified runtime cleanup is pending"
184
+ };
185
+ }
109
186
  if (role.status === "failed" || role.status === "exited") {
110
187
  return { health: "failed", healthReason: `persisted Role state is ${role.status}` };
111
188
  }
112
189
  if (nativeSession?.status === "broken") {
113
190
  return { health: "failed", healthReason: "the active native session is broken" };
114
191
  }
192
+ const awaitingProviderAcceptance = activeRun?.pushedAt !== undefined
193
+ && activeRun.deliveredAt === undefined;
194
+ if (awaitingProviderAcceptance
195
+ && tmux.state !== "running") {
196
+ return {
197
+ health: "needs-attention",
198
+ healthReason: "the pushed active Run is awaiting provider acceptance and has no live tmux pane"
199
+ };
200
+ }
115
201
  if (tmux.state === "exited") {
116
202
  return { health: "failed", healthReason: "the tmux pane has exited" };
117
203
  }
@@ -125,6 +211,12 @@ function calculateHealth(role, activeRun, nativeSession, tmux, openInputRequestC
125
211
  if (activeRun.deliveredAt !== undefined && tmux.state !== "running") {
126
212
  return { health: "needs-attention", healthReason: "the delivered active Run has no live tmux pane" };
127
213
  }
214
+ if (stalled) {
215
+ return {
216
+ health: "needs-attention",
217
+ healthReason: "the live active Run has no durable progress in the configured stall window"
218
+ };
219
+ }
128
220
  }
129
221
  if (activeRun === null && role.status === "running") {
130
222
  return { health: "needs-attention", healthReason: "the Role is running without an active Run" };
@@ -142,11 +234,25 @@ function calculateHealth(role, activeRun, nativeSession, tmux, openInputRequestC
142
234
  };
143
235
  }
144
236
  if (activeRun !== null) {
145
- return activeRun.deliveredAt === undefined
146
- ? { health: "starting", healthReason: "the active Run is awaiting tmux delivery" }
147
- : { health: "running", healthReason: "the active Run has a live tmux pane" };
237
+ if (activeRun.deliveredAt !== undefined) {
238
+ return { health: "running", healthReason: "the active Run has a live tmux pane" };
239
+ }
240
+ if (activeRun.pushedAt !== undefined) {
241
+ return {
242
+ health: "awaiting-provider-acceptance",
243
+ healthReason: "the pushed active Run is awaiting provider acceptance"
244
+ };
245
+ }
246
+ return { health: "starting", healthReason: "the active Run is awaiting tmux delivery" };
148
247
  }
149
248
  return tmux.state === "running"
150
249
  ? { health: "ready", healthReason: "the native Agent pane is ready without active work" }
151
250
  : { health: "idle", healthReason: "there is no active work or live tmux pane" };
152
251
  }
252
+ function activeRunDeliveryLabel(run) {
253
+ if (run.deliveredAt !== undefined)
254
+ return "delivered";
255
+ if (run.pushedAt !== undefined)
256
+ return "pushed (awaiting provider acceptance)";
257
+ return "queued";
258
+ }
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { usageError } from "../errors/cliError.js";
3
+ export function readCommandText(inline, file, label, usage) {
4
+ if ((inline === undefined) === (file === undefined)) {
5
+ throw usageError(`Specify exactly one of ${label} or ${label}-file.`, usage);
6
+ }
7
+ const value = file === undefined
8
+ ? inline
9
+ : readFileSync(file === "-" ? 0 : file, "utf8");
10
+ const normalized = value.trim();
11
+ if (normalized.length === 0 || normalized.includes("\0")) {
12
+ throw usageError(`${label} is required.`, usage);
13
+ }
14
+ return normalized;
15
+ }
@@ -6,34 +6,38 @@ import { activationBlock, activationEnd, activationIsAutomatic, activationStart,
6
6
  export function installCompletion(store, shell, installation, env, identity, activate) {
7
7
  validateInstallation(installation);
8
8
  writeManagedScript(shell, installation.scriptPath, identity);
9
- const config = store.getConfig();
10
- store.saveConfig({
11
- ...config,
12
- completionInstallations: {
13
- ...config.completionInstallations,
14
- [shell]: installation
15
- }
9
+ store.transaction((tx) => {
10
+ const config = tx.getConfig();
11
+ tx.saveConfig({
12
+ ...config,
13
+ completionInstallations: {
14
+ ...config.completionInstallations,
15
+ [shell]: installation
16
+ }
17
+ });
16
18
  });
17
19
  if (!activationIsAutomatic(shell, installation, env, identity) && activate) {
18
20
  writeActivationBlock(shell, installation, identity);
19
21
  }
20
22
  }
21
23
  export function uninstallCompletion(store, shell, identity) {
22
- const config = store.getConfig();
23
- const installation = config.completionInstallations?.[shell];
24
- if (installation === undefined)
25
- return;
26
- assertManagedScriptRemovable(shell, installation.scriptPath, identity);
27
- assertActivationRemovable(shell, installation, identity);
28
- removeManagedScript(shell, installation.scriptPath, identity);
29
- removeActivationBlock(shell, installation, identity);
30
- const installations = { ...config.completionInstallations };
31
- delete installations[shell];
32
- store.saveConfig({
33
- ...config,
34
- completionInstallations: Object.keys(installations).length === 0
35
- ? undefined
36
- : installations
24
+ store.transaction((tx) => {
25
+ const config = tx.getConfig();
26
+ const installation = config.completionInstallations?.[shell];
27
+ if (installation === undefined)
28
+ return;
29
+ assertManagedScriptRemovable(shell, installation.scriptPath, identity);
30
+ assertActivationRemovable(shell, installation, identity);
31
+ removeManagedScript(shell, installation.scriptPath, identity);
32
+ removeActivationBlock(shell, installation, identity);
33
+ const installations = { ...config.completionInstallations };
34
+ delete installations[shell];
35
+ if (Object.keys(installations).length > 0) {
36
+ tx.saveConfig({ ...config, completionInstallations: installations });
37
+ return;
38
+ }
39
+ const { completionInstallations: _removed, ...withoutInstallations } = config;
40
+ tx.saveConfig(withoutInstallations);
37
41
  });
38
42
  }
39
43
  function assertManagedScriptRemovable(shell, path, identity) {
@@ -1,9 +1,10 @@
1
- export const DEFAULT_RECONCILIATION_INTERVAL_SECONDS = 30;
1
+ export const DEFAULT_RECONCILIATION_INTERVAL_SECONDS = 120;
2
2
  export const MIN_RECONCILIATION_INTERVAL_SECONDS = 5;
3
3
  export const MAX_RECONCILIATION_INTERVAL_SECONDS = 300;
4
4
  /**
5
- * Resolves the durable Yui setting used for periodic full reconciliation.
6
- * Command-triggered scans remain immediate and do not use this interval.
5
+ * Resolves the durable Yui setting used for low-frequency recovery
6
+ * reconciliation. Normal durable state changes wake the Controller through
7
+ * its event queue and do not wait for this interval.
7
8
  */
8
9
  export function reconciliationIntervalMilliseconds(value) {
9
10
  const seconds = value ?? DEFAULT_RECONCILIATION_INTERVAL_SECONDS;
@@ -1,10 +1,6 @@
1
- import { readFileSync } from "node:fs";
2
- import { join } from "node:path";
3
- import { dataError } from "../errors/cliError.js";
4
1
  /** Compatibility entry point used by the restored Task workflow. */
5
- export function compileDispatchInput(_store, taskId, role, input) {
6
- const configuredSkillBodies = readConfiguredSkills(role.skills ?? []);
7
- return buildRoleContext({ taskId, role, input, configuredSkillBodies });
2
+ export function compileDispatchInput(_store, taskId, role, input, workContext = {}) {
3
+ return buildRoleContext({ taskId, role, input, ...workContext });
8
4
  }
9
5
  export function buildRoleContext(context) {
10
6
  return context.role.name === "leader"
@@ -17,53 +13,109 @@ export function buildLeaderContext(context) {
17
13
  export function buildWorkerContext(context) {
18
14
  return renderDispatchContext("worker", context);
19
15
  }
16
+ const WORKER_RUN_COMPLETION_MARKER = "Yui Role Run completion requirement:";
17
+ const WORKER_RUN_COMPLETION_REQUIREMENT = [
18
+ WORKER_RUN_COMPLETION_MARKER,
19
+ "Before ending, read the exact current Run ID from the managed first line and execute "
20
+ + "`yui task run yield <current-run-id> --summary-file - <<'YUI_SUMMARY'` "
21
+ + "followed by the outcome, evidence, and a closing `YUI_SUMMARY` line, "
22
+ + "replacing the placeholder with that ID.",
23
+ "If you cannot finally determine success, failure, completeness, or the correct "
24
+ + "disposition, do not guess, silently stop, or hide uncertainty behind a success "
25
+ + "summary. Use this exact yield path and label the handoff uncertain, incomplete, "
26
+ + "blocked, or requiring Leader judgment.",
27
+ "Report the most complete truthful evidence available and, when applicable: exact Run, "
28
+ + "WorkItem, and native Session identity; actions actually performed; changed paths "
29
+ + "and commit/worktree state; checks actually run and their outcomes; provider, runtime, "
30
+ + "or permission errors; the last confirmed lifecycle boundary; work not performed; "
31
+ + "unresolved assumptions or decisions; residual risks; confidence; and bounded next options.",
32
+ "Yield submits immutable Run evidence and a Candidate, or Review evidence only. It never "
33
+ + "implies Leader acceptance, WorkItem completion, ChangeSet capture, Integration, or "
34
+ + "Task completion. Review Runs report findings, verification gaps, and limits; the "
35
+ + "Leader decides disposition.",
36
+ "This exact control-plane permission does not grant repository writes, broad Bash, "
37
+ + "external effects, or cross-Run control.",
38
+ "If the exact yield is denied, do not retry, broaden permissions, use a wrapper, mutate "
39
+ + "Yui state, or invent delivery evidence. Truthfully surface the blocker through the "
40
+ + "supported provider failure boundary; there is no fallback protocol.",
41
+ "Yielding closes the Run and hands the WorkItem to the Leader for acceptance; "
42
+ + "a final response alone does neither.",
43
+ "The yield command must be your final tool action. After it succeeds, stop immediately: "
44
+ + "do not inspect, poll, accept, or perform any further work in the same native turn."
45
+ ].join("\n");
46
+ export function ensureWorkerRunCompletionRequirement(input) {
47
+ return input.endsWith(`\n\n${WORKER_RUN_COMPLETION_REQUIREMENT}`)
48
+ ? input
49
+ : `${input}\n\n${WORKER_RUN_COMPLETION_REQUIREMENT}`;
50
+ }
20
51
  function renderDispatchContext(kind, context) {
21
52
  const profile = context.role;
53
+ const binding = profile.agentBindings[profile.activeAgentId];
22
54
  const profileLines = [
23
55
  `Task: ${context.taskId}`,
24
56
  `Role: ${profile.name}`,
25
57
  `Active Agent: ${profile.activeAgentId}`,
58
+ `Runtime: ${binding.adapterId}; model: ${binding.config.model ?? "CLI default"}; effort: ${binding.config.effort ?? "CLI default"}; permission: ${binding.config.permission.strategy}`,
26
59
  profile.description === undefined ? null : `Description: ${profile.description}`,
27
60
  ...(profile.responsibilities ?? []).map((item) => `Responsibility: ${item}`),
28
61
  ...(profile.constraints ?? []).map((item) => `Constraint: ${item}`),
29
- profile.expectedOutput === undefined ? null : `Expected output: ${profile.expectedOutput}`,
30
- profile.systemPrompt === undefined ? null : `Role instruction: ${profile.systemPrompt}`,
31
- profile.skills === undefined || profile.skills.length === 0
32
- ? null
33
- : `Configured role skills: ${profile.skills.join(", ")}`
62
+ profile.expectedOutput === undefined ? null : `Expected output: ${profile.expectedOutput}`
34
63
  ].filter((line) => line !== null);
35
- return [
36
- readSystemSkill(kind),
64
+ const workLines = kind === "worker"
65
+ ? renderWorkerScope(context)
66
+ : [];
67
+ const rendered = [
68
+ `Follow the injected yui-${kind} Skill for this Yui dispatch.`,
69
+ renderContextLayers(context, kind),
37
70
  profileLines.join("\n"),
38
- ...(context.configuredSkillBodies ?? []).map((body) => body.trim()).filter(Boolean),
71
+ workLines.length === 0 ? null : workLines.join("\n"),
39
72
  "Yui dispatch:",
40
73
  requireText(context.input, "dispatch input")
41
- ].filter(Boolean).join("\n\n");
74
+ ].filter((section) => section !== null && section.length > 0)
75
+ .join("\n\n");
76
+ return kind === "worker"
77
+ ? ensureWorkerRunCompletionRequirement(rendered)
78
+ : rendered;
42
79
  }
43
- function readConfiguredSkills(skills) {
44
- const yuiHome = process.env.YUI_HOME;
45
- if (skills.length === 0)
46
- return [];
47
- if (yuiHome === undefined) {
48
- throw dataError("YUI_HOME is required to load configured Role skills.");
49
- }
50
- return skills.map((skill) => {
51
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(skill)) {
52
- throw dataError(`Invalid configured Skill id: ${skill}`);
53
- }
54
- try {
55
- return readFileSync(join(yuiHome, "skills", skill, "SKILL.md"), "utf8").trim();
56
- }
57
- catch (error) {
58
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
59
- throw dataError(`Configured Skill not found: ${skill}`);
60
- }
61
- throw error;
62
- }
63
- });
80
+ function renderContextLayers(context, kind) {
81
+ const projects = context.projectPolicy ?? [];
82
+ const policyLines = projects.length === 0
83
+ ? ["- none (this Task is not Project-backed)"]
84
+ : projects.map(({ projectId, directory }) => {
85
+ const label = directory === undefined ? projectId : `${directory} (${projectId})`;
86
+ return `- ${label}: \`yui project show ${projectId}\`; then \`yui project knowledge list ${projectId}\` and show the relevant entries`;
87
+ });
88
+ return [
89
+ "Context layers:",
90
+ "- Yui Core: durable identity, lifecycle, access, workspace, and exact handoff safety.",
91
+ `- Generic ${kind} Skill: reusable role behavior and evidence discipline.`,
92
+ "- Project Policy: project-owned build, test, migration, release, and review rules; it is not a Yui Core default.",
93
+ ...["Project Policy references:", ...policyLines],
94
+ "- Task Contract: the current Task brief, WorkItem objective, acceptance criteria, and dispatch input.",
95
+ "Do not import rules from another Project or Task, and do not infer Project Policy from repository files alone."
96
+ ].join("\n");
64
97
  }
65
- function readSystemSkill(kind) {
66
- return readFileSync(new URL(`../../skills/yui-${kind}/SKILL.md`, import.meta.url), "utf8").trim();
98
+ function renderWorkerScope(context) {
99
+ if (context.workItem === undefined || context.workspace === undefined)
100
+ return [];
101
+ const writable = context.workspace.entries.filter(({ access }) => access === "write");
102
+ const contextOnly = context.workspace.entries.filter(({ access }) => access === "read");
103
+ const projectLines = (label, entries) => [
104
+ `${label}:`,
105
+ ...(entries.length === 0
106
+ ? ["- none"]
107
+ : entries.map(({ directory, projectId }) => `- ${directory} (${projectId})`))
108
+ ];
109
+ return [
110
+ `WorkItem: ${context.workItem.id}`,
111
+ `Workspace root: ${context.workspace.root}`,
112
+ ...projectLines("Writable Projects", writable),
113
+ ...projectLines("Context-only Projects", contextOnly),
114
+ `Read the full Task state with \`yui task context ${context.taskId}\`.`,
115
+ `Read the current WorkItem scope with \`yui task work list ${context.taskId}\`.`,
116
+ "Modify only Writable Projects. If another Project must change, stop and ask "
117
+ + "the Task Leader to expand this WorkItem scope before continuing."
118
+ ];
67
119
  }
68
120
  function requireText(value, label) {
69
121
  const normalized = value.trim();
@@ -0,0 +1,119 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
6
+ const BUILTIN_YUI_SKILLS = new Set([
7
+ "yui-operator",
8
+ "yui-leader",
9
+ "yui-worker",
10
+ "yui-reviewer"
11
+ ]);
12
+ /**
13
+ * Compiles stable Role policy for an Agent-native instruction channel.
14
+ * Dynamic wakeups and Run assignments deliberately remain outside this value.
15
+ */
16
+ export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
17
+ const kind = roleSessionKind(role, owner, options.purpose ?? "execution");
18
+ const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
19
+ const skillIds = unique([
20
+ ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
21
+ ...(role.skills ?? [])
22
+ ]);
23
+ const skills = loadYuiSkillContexts(yuiHome, skillIds);
24
+ return {
25
+ developerInstructions: renderDeveloperInstructions(kind, role, owner),
26
+ skills,
27
+ ...(yuiHome === undefined
28
+ ? {}
29
+ : { managedContextFile: roleContextFile(yuiHome, role, owner, kind) })
30
+ };
31
+ }
32
+ function renderDeveloperInstructions(kind, role, owner) {
33
+ const core = renderRoleCore(kind, role, owner);
34
+ const profile = [
35
+ role.description === undefined ? null : `Role description: ${role.description}`,
36
+ ...(role.responsibilities ?? []).map((value) => `Role responsibility: ${value}`),
37
+ ...(role.constraints ?? []).map((value) => `Role constraint: ${value}`),
38
+ role.expectedOutput === undefined ? null : `Expected output: ${role.expectedOutput}`,
39
+ role.systemPrompt === undefined ? null : `Additional Role instructions:\n${role.systemPrompt}`
40
+ ].filter((value) => value !== null);
41
+ return [...core, ...profile].join("\n");
42
+ }
43
+ function roleSessionKind(role, owner, purpose) {
44
+ if (owner.scope === "global") {
45
+ return role.name === SYSTEM_OPERATOR_ROLE ? "operator" : "global";
46
+ }
47
+ if (purpose === "review")
48
+ return "reviewer";
49
+ return role.name === SYSTEM_LEADER_ROLE ? "leader" : "worker";
50
+ }
51
+ function renderRoleCore(kind, role, owner) {
52
+ switch (kind) {
53
+ case "operator":
54
+ return [
55
+ "You are the global Yui Operator and the user's CLI proxy.",
56
+ "Manage Yui through its CLI; do not perform Task implementation work.",
57
+ "Follow the injected yui-operator Skill when coordinating Yui."
58
+ ];
59
+ case "global":
60
+ return [
61
+ `You are global Yui Role ${role.name}.`,
62
+ "Follow the configured Role profile and the user's instructions."
63
+ ];
64
+ case "leader":
65
+ return [
66
+ `You are the Yui Leader for Task ${owner.scope === "task" ? owner.taskId : role.name}.`,
67
+ "Own Task stewardship and delegate bounded implementation work.",
68
+ "Follow the injected yui-leader Skill for Yui coordination."
69
+ ];
70
+ case "reviewer":
71
+ return [
72
+ `You are Yui review Role ${role.name} for Task ${owner.scope === "task" ? owner.taskId : role.name}.`,
73
+ "Review only the exact frozen ReviewRound scope assigned to this Run.",
74
+ "Follow the injected yui-reviewer Skill while reviewing Yui work."
75
+ ];
76
+ case "worker":
77
+ return [
78
+ `You are Yui Role ${role.name}${owner.scope === "task" ? ` for Task ${owner.taskId}` : ""}.`,
79
+ "Execute only the work delegated to this Role and report through Yui.",
80
+ "Follow the injected yui-worker Skill while handling Yui work."
81
+ ];
82
+ }
83
+ }
84
+ export function loadYuiSkillContexts(yuiHome, skillIds) {
85
+ return unique(skillIds).map((id) => loadSkill(yuiHome, id, BUILTIN_YUI_SKILLS.has(id)));
86
+ }
87
+ function loadSkill(yuiHome, id, builtIn) {
88
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) {
89
+ throw new Error(`Invalid configured Skill id: ${id}.`);
90
+ }
91
+ if (!builtIn && yuiHome === undefined) {
92
+ throw new Error("YUI_HOME is required to load configured Role skills.");
93
+ }
94
+ const path = builtIn
95
+ ? resolve(fileURLToPath(new URL(`../../skills/${id}/`, import.meta.url)))
96
+ : resolve(join(yuiHome, "skills", id));
97
+ try {
98
+ return { id, path, content: readFileSync(join(path, "SKILL.md"), "utf8").trim() };
99
+ }
100
+ catch (error) {
101
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
102
+ throw new Error(`Configured Skill not found: ${id}.`);
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+ function unique(values) {
108
+ return [...new Set(values)];
109
+ }
110
+ function roleContextFile(yuiHome, role, owner, kind) {
111
+ const identity = createHash("sha256").update(JSON.stringify([
112
+ owner.scope,
113
+ ...(owner.scope === "task" ? [owner.taskId] : []),
114
+ kind,
115
+ role.name,
116
+ role.activeAgentId
117
+ ])).digest("hex");
118
+ return resolve(join(yuiHome, "runtime", "session-contexts", `${identity}.md`));
119
+ }