@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
@@ -0,0 +1,695 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
4
+ import { dirname, join, relative, resolve, sep } from "node:path";
5
+ import { promisify } from "node:util";
6
+ import { selectEnvironment } from "../agent/launchEnvironment.js";
7
+ import { controllerSocketPath } from "../core/controllerEndpoint.js";
8
+ import { NodeGitWorkspace, RemoteBaselineConflictError } from "../repository/gitWorkspace.js";
9
+ import { resolveWorktreeRoot } from "../repository/taskWorkspacePreparer.js";
10
+ import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
11
+ import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
12
+ import { requireLeaderDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
13
+ import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
14
+ const executeFile = promisify(execFile);
15
+ const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
16
+ "PATH",
17
+ "USER",
18
+ "LOGNAME",
19
+ "SHELL",
20
+ "TERM",
21
+ "COLORTERM",
22
+ "LANG",
23
+ "LANGUAGE",
24
+ "LC_ALL",
25
+ "LC_ADDRESS",
26
+ "LC_COLLATE",
27
+ "LC_CTYPE",
28
+ "LC_IDENTIFICATION",
29
+ "LC_MEASUREMENT",
30
+ "LC_MESSAGES",
31
+ "LC_MONETARY",
32
+ "LC_NAME",
33
+ "LC_NUMERIC",
34
+ "LC_PAPER",
35
+ "LC_TELEPHONE",
36
+ "LC_TIME",
37
+ "TZ",
38
+ "SSL_CERT_FILE",
39
+ "SSL_CERT_DIR",
40
+ "NODE_EXTRA_CA_CERTS",
41
+ "CURL_CA_BUNDLE",
42
+ "REQUESTS_CA_BUNDLE"
43
+ ];
44
+ export const REMOTE_BASELINE_CONFLICT_PREFIX = "Remote baseline merge conflicts";
45
+ export class GitIntegrationService {
46
+ store;
47
+ git;
48
+ now;
49
+ home;
50
+ worktreeRoot;
51
+ environment;
52
+ runtimeIsolation;
53
+ constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), environment = process.env, runtimeIsolation = defaultIntegrationRuntimeIsolation(home)) {
54
+ this.store = store;
55
+ this.git = git;
56
+ this.now = now;
57
+ this.home = resolve(home);
58
+ this.worktreeRoot = resolveWorktreeRoot(home, store.getConfig().defaultWorkspace);
59
+ this.environment = { ...environment };
60
+ this.runtimeIsolation = runtimeIsolation;
61
+ }
62
+ async integrate(taskId, integrationId, options = {}) {
63
+ const initial = requireIntegration(this.store, taskId, integrationId);
64
+ const task = this.store.getTask(initial.taskId);
65
+ if (task === null || !task.projectBindings.some(({ projectId }) => projectId === initial.projectId)) {
66
+ throw new Error(`Integration Task Project is unavailable: ${initial.taskId}.`);
67
+ }
68
+ if (task.status !== "active") {
69
+ throw new Error(`Integration Task is not active: ${task.id}/${task.status}.`);
70
+ }
71
+ const project = this.store.getProject(initial.projectId);
72
+ if (project === null)
73
+ throw new Error(`Project not found: ${initial.projectId}.`);
74
+ let prepared;
75
+ let workspace;
76
+ let managedWorkspace;
77
+ try {
78
+ prepared = await this.git.ensureIntegrationWorktree({
79
+ repositoryPath: project.path,
80
+ container: join(this.worktreeRoot, project.name),
81
+ taskId: task.id,
82
+ integrationId: initial.id,
83
+ baseRef: initial.expectedHead
84
+ });
85
+ workspace = {
86
+ projectId: project.id,
87
+ path: prepared.path,
88
+ branch: prepared.branch,
89
+ baseCommit: prepared.baseCommit
90
+ };
91
+ const existingWorkspace = this.store.getIntegrationWorkspace(task.id, initial.id);
92
+ managedWorkspace = existingWorkspace ?? createManagedWorkspace({
93
+ owner: {
94
+ type: "integration-attempt",
95
+ taskId: task.id,
96
+ integrationAttemptId: initial.id
97
+ },
98
+ root: prepared.path,
99
+ entries: [{
100
+ projectId: project.id,
101
+ directory: project.name,
102
+ access: "write",
103
+ path: prepared.path,
104
+ branch: prepared.branch,
105
+ baseRef: initial.expectedHead,
106
+ baseCommit: prepared.baseCommit
107
+ }]
108
+ }, this.now());
109
+ this.store.saveManagedWorkspace(managedWorkspace);
110
+ }
111
+ catch (error) {
112
+ return this.#fail(initial, error, "integration-preparation");
113
+ }
114
+ if (initial.status === "validating") {
115
+ return this.#recoverValidating(initial, workspace, project.path);
116
+ }
117
+ let current = initial;
118
+ try {
119
+ if (options.remoteBaseline !== undefined) {
120
+ const mergeRemote = this.git.mergeRemoteIntoWorktree;
121
+ if (mergeRemote === undefined) {
122
+ throw new Error("Integration Git workspace does not support remote baseline reconciliation.");
123
+ }
124
+ await mergeRemote.call(this.git, {
125
+ repositoryPath: prepared.path,
126
+ remoteUrl: options.remoteBaseline.remoteUrl,
127
+ branch: options.remoteBaseline.branch
128
+ });
129
+ }
130
+ // A remote-baseline Attempt starts from the exact previously committed
131
+ // Task head, so its ChangeSets are already represented by that tree.
132
+ // Keep their IDs as provenance/check evidence, but never cherry-pick the
133
+ // source commits again. A manual continuation of a remote merge carries
134
+ // the same semantic marker in its conflict report.
135
+ const remoteOnly = options.remoteBaseline !== undefined
136
+ || (current.resolution?.action === "manual-resolution"
137
+ && current.conflict?.summary.startsWith(REMOTE_BASELINE_CONFLICT_PREFIX));
138
+ const plan = remoteOnly
139
+ ? []
140
+ : await integrationCommitPlan(this.store, task.id, project.path, current.changeSetIds, current.expectedHead);
141
+ let remaining = plan;
142
+ if (current.resolution?.action === "manual-resolution"
143
+ && current.conflict?.summary.startsWith(REMOTE_BASELINE_CONFLICT_PREFIX)) {
144
+ await completeRemoteBaselineResolution(prepared.path);
145
+ }
146
+ else if (current.resolution?.action === "manual-resolution") {
147
+ const resolvedCommit = await completeManualResolution(prepared.path);
148
+ const resolvedIndex = plan.findIndex(({ commit }) => commit === resolvedCommit);
149
+ if (resolvedIndex < 0) {
150
+ throw new Error(`Manual resolution commit is not part of the Integration plan: ${resolvedCommit}.`);
151
+ }
152
+ remaining = plan.slice(resolvedIndex + 1);
153
+ }
154
+ const conflict = await this.#applyCommits(current, workspace, prepared.path, remaining);
155
+ if (conflict !== undefined) {
156
+ return conflict;
157
+ }
158
+ const checkResults = await this.#runChecks(current, managedWorkspace, prepared.path);
159
+ if (checkResults.some((check) => check.outcome === "failed")) {
160
+ current = updateIntegrationAttempt(current, {
161
+ status: "failed",
162
+ checks: checkResults
163
+ }, this.now());
164
+ this.store.saveIntegrationAttempt(task.id, current);
165
+ return this.#terminalResult("failed", current, workspace);
166
+ }
167
+ const candidateCommit = await gitLine(["-C", prepared.path, "rev-parse", "HEAD^{commit}"]);
168
+ current = updateIntegrationAttempt(current, {
169
+ status: "validating",
170
+ candidateCommit,
171
+ checks: checkResults
172
+ }, this.now());
173
+ this.store.saveIntegrationAttempt(task.id, current);
174
+ await advanceTargetRef(project.path, current.targetRef, candidateCommit, current.expectedHead);
175
+ const committed = updateIntegrationAttempt(current, { status: "committed" }, this.now());
176
+ this.store.saveIntegrationAttempt(task.id, committed);
177
+ return this.#terminalResult("committed", committed, workspace);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof RemoteBaselineConflictError) {
181
+ const pending = requireLeaderDecision(current, {
182
+ affectedPaths: error.affectedPaths,
183
+ summary: error.message
184
+ }, this.now());
185
+ this.store.saveIntegrationAttempt(task.id, pending);
186
+ return { status: "blocked", attempt: pending, workspace };
187
+ }
188
+ if (current.status === "validating" && current.candidateCommit !== undefined) {
189
+ const target = await resolveRef(project.path, current.targetRef);
190
+ if (target === current.candidateCommit) {
191
+ const committed = updateIntegrationAttempt(current, { status: "committed" }, this.now());
192
+ this.store.saveIntegrationAttempt(task.id, committed);
193
+ return this.#terminalResult("committed", committed, workspace);
194
+ }
195
+ }
196
+ return this.#fail(current, error, "integration", workspace);
197
+ }
198
+ }
199
+ async cleanup(integration) {
200
+ const task = this.store.getTask(integration.taskId);
201
+ if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
202
+ throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
203
+ }
204
+ const project = this.store.getProject(integration.projectId);
205
+ if (project === null)
206
+ throw new Error(`Project not found: ${integration.projectId}.`);
207
+ const managedWorkspace = this.store.getIntegrationWorkspace(integration.taskId, integration.id);
208
+ if (managedWorkspace !== null) {
209
+ const runtime = this.#runtimePreparation(integration, managedWorkspace);
210
+ this.runtimeIsolation.cleanup(runtime, integration.status === "committed" ? "completion" : "failure");
211
+ }
212
+ const result = await this.git.removeIntegrationWorktree({
213
+ repositoryPath: project.path,
214
+ container: join(this.worktreeRoot, project.name),
215
+ taskId: task.id,
216
+ integrationId: integration.id,
217
+ discardChanges: integration.status === "failed"
218
+ });
219
+ if (result !== "dirty") {
220
+ await rm(integrationCheckDirectory(this.home, task.id, integration.id), {
221
+ recursive: true,
222
+ force: true
223
+ });
224
+ this.store.removeManagedWorkspace({
225
+ type: "integration-attempt",
226
+ taskId: integration.taskId,
227
+ integrationAttemptId: integration.id
228
+ });
229
+ }
230
+ return result;
231
+ }
232
+ async #runChecks(attempt, workspace, path) {
233
+ if (attempt.checkCommands.length === 0)
234
+ return [];
235
+ const runtime = this.#runtimePreparation(attempt, workspace);
236
+ this.runtimeIsolation.activate(runtime);
237
+ let cleanupReason = "failure";
238
+ try {
239
+ const environment = await integrationCheckEnvironment(this.environment, runtime);
240
+ const checks = await runChecks(path, attempt.checkCommands, this.home, attempt.taskId, attempt.id, environment);
241
+ cleanupReason = checks.some(({ outcome }) => outcome === "failed")
242
+ ? "failure"
243
+ : "completion";
244
+ return checks;
245
+ }
246
+ finally {
247
+ this.runtimeIsolation.cleanup(runtime, cleanupReason);
248
+ }
249
+ }
250
+ #runtimePreparation(attempt, workspace) {
251
+ return this.runtimeIsolation.preflight({
252
+ workspace,
253
+ launchId: integrationRuntimeLaunchId(this.home, attempt.id),
254
+ generationId: "integration-checks",
255
+ allowExactActive: true
256
+ });
257
+ }
258
+ async #applyCommits(attempt, workspace, candidatePath, commits) {
259
+ for (const { changeSetId, commit } of commits) {
260
+ try {
261
+ await git(["-C", candidatePath, "cherry-pick", commit]);
262
+ }
263
+ catch {
264
+ const affectedPaths = (await git([
265
+ "-C", candidatePath, "diff", "--name-only", "--diff-filter=U"
266
+ ])).trim().split("\n").filter(Boolean);
267
+ if (affectedPaths.length === 0
268
+ && await isEmptyCherryPick(candidatePath, commit)) {
269
+ await git(["-C", candidatePath, "cherry-pick", "--skip"]);
270
+ continue;
271
+ }
272
+ const pending = requireLeaderDecision(attempt, {
273
+ affectedPaths,
274
+ summary: `ChangeSet ${changeSetId} conflicts with ${attempt.targetRef}.`
275
+ }, this.now());
276
+ this.store.saveIntegrationAttempt(attempt.taskId, pending);
277
+ return { status: "blocked", attempt: pending, workspace };
278
+ }
279
+ }
280
+ return undefined;
281
+ }
282
+ async #recoverValidating(attempt, workspace, repositoryPath) {
283
+ if (attempt.candidateCommit === undefined || attempt.checks === undefined) {
284
+ return this.#fail(attempt, new Error("Validating Integration is missing its candidate commit or checks."), "integration-recovery", workspace);
285
+ }
286
+ const target = await resolveRef(repositoryPath, attempt.targetRef);
287
+ if (target === attempt.expectedHead) {
288
+ try {
289
+ await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.expectedHead);
290
+ }
291
+ catch (error) {
292
+ return this.#fail(attempt, error, "integration-recovery", workspace);
293
+ }
294
+ }
295
+ else if (target !== attempt.candidateCommit) {
296
+ return this.#fail(attempt, new Error(`Target moved to ${target}; expected ${attempt.expectedHead}.`), "integration-recovery", workspace);
297
+ }
298
+ const committed = updateIntegrationAttempt(attempt, { status: "committed" }, this.now());
299
+ this.store.saveIntegrationAttempt(attempt.taskId, committed);
300
+ return this.#terminalResult("committed", committed, workspace);
301
+ }
302
+ #fail(attempt, error, checkName, workspace) {
303
+ const failed = updateIntegrationAttempt(attempt, {
304
+ status: "failed",
305
+ checks: [
306
+ ...(attempt.checks ?? []),
307
+ {
308
+ name: checkName,
309
+ outcome: "failed",
310
+ details: error instanceof Error ? error.message : String(error)
311
+ }
312
+ ]
313
+ }, this.now());
314
+ this.store.saveIntegrationAttempt(attempt.taskId, failed);
315
+ return this.#terminalResult("failed", failed, workspace);
316
+ }
317
+ #terminalResult(status, attempt, workspace) {
318
+ if (status === "committed") {
319
+ if (workspace === undefined) {
320
+ throw new Error(`Committed Integration has no workspace: ${attempt.id}.`);
321
+ }
322
+ return { status, attempt, workspace };
323
+ }
324
+ return {
325
+ status,
326
+ attempt,
327
+ ...(workspace === undefined ? {} : { workspace })
328
+ };
329
+ }
330
+ }
331
+ async function isEmptyCherryPick(path, commit) {
332
+ let cherryPickHead;
333
+ try {
334
+ cherryPickHead = await gitLine([
335
+ "-C", path, "rev-parse", "--verify", "CHERRY_PICK_HEAD^{commit}"
336
+ ]);
337
+ }
338
+ catch {
339
+ return false;
340
+ }
341
+ return cherryPickHead === commit
342
+ && await gitSucceeds(["-C", path, "diff", "--cached", "--quiet"]);
343
+ }
344
+ async function integrationCommitPlan(store, taskId, repositoryPath, changeSetIds, expectedHead) {
345
+ const plan = [];
346
+ for (const changeSetId of changeSetIds) {
347
+ const changeSet = store.getChangeSet(taskId, changeSetId);
348
+ if (changeSet === null)
349
+ throw new Error(`ChangeSet not found: ${changeSetId}.`);
350
+ const commits = (await git([
351
+ "-C", repositoryPath, "rev-list", "--reverse",
352
+ `${changeSet.baseCommit}..${changeSet.headCommit}`
353
+ ])).trim().split("\n").filter(Boolean);
354
+ for (const commit of commits) {
355
+ // A direct Task-main recovery may capture commits after they are already
356
+ // present on the exact target. Treat those commits as applied rather
357
+ // than attempting an empty cherry-pick; the later checks and CAS still
358
+ // fence the committed Integration to expectedHead.
359
+ if (await gitSucceeds([
360
+ "-C", repositoryPath,
361
+ "merge-base", "--is-ancestor", commit, expectedHead
362
+ ]))
363
+ continue;
364
+ plan.push({ changeSetId, commit });
365
+ }
366
+ }
367
+ return plan;
368
+ }
369
+ async function completeManualResolution(path) {
370
+ const unmerged = (await git(["-C", path, "diff", "--name-only", "--diff-filter=U"])).trim();
371
+ if (unmerged.length > 0) {
372
+ throw new Error(`Manual resolution is incomplete: ${unmerged.split("\n").join(", ")}.`);
373
+ }
374
+ let cherryPickHead;
375
+ try {
376
+ cherryPickHead = await gitLine([
377
+ "-C", path, "rev-parse", "--verify", "CHERRY_PICK_HEAD"
378
+ ]);
379
+ }
380
+ catch {
381
+ throw new Error("Manual resolution has no active cherry-pick.");
382
+ }
383
+ const emptyResolution = await gitSucceeds(["-C", path, "diff", "--cached", "--quiet"]);
384
+ if (emptyResolution) {
385
+ await git(["-C", path, "cherry-pick", "--skip"]);
386
+ }
387
+ else {
388
+ await git(["-C", path, "-c", "user.name=Yui", "-c", "user.email=yui@local",
389
+ "cherry-pick", "--continue"]);
390
+ }
391
+ return cherryPickHead;
392
+ }
393
+ async function completeRemoteBaselineResolution(path) {
394
+ const unmerged = (await git(["-C", path, "diff", "--name-only", "--diff-filter=U"])).trim();
395
+ if (unmerged.length > 0) {
396
+ throw new Error(`Manual remote baseline resolution is incomplete: ${unmerged.split("\n").join(", ")}.`);
397
+ }
398
+ try {
399
+ await gitLine(["-C", path, "rev-parse", "--verify", "MERGE_HEAD"]);
400
+ }
401
+ catch {
402
+ throw new Error("Manual remote baseline resolution has no active merge.");
403
+ }
404
+ await git([
405
+ "-C", path,
406
+ "-c", "user.name=Yui",
407
+ "-c", "user.email=yui@local",
408
+ "commit", "--no-edit"
409
+ ]);
410
+ }
411
+ async function runChecks(path, commands, home, taskId, integrationId, environment) {
412
+ if (commands.length === 0)
413
+ return [];
414
+ const outputDirectory = integrationCheckDirectory(home, taskId, integrationId);
415
+ await rm(outputDirectory, { recursive: true, force: true });
416
+ await mkdir(outputDirectory, { recursive: true, mode: 0o700 });
417
+ const results = [];
418
+ for (const [index, command] of commands.entries()) {
419
+ const absoluteLogPath = join(outputDirectory, `${String(index + 1).padStart(3, "0")}.log`);
420
+ const logPath = relative(home, absoluteLogPath).split(sep).join("/");
421
+ const result = await runCheck(path, command, absoluteLogPath, logPath, environment);
422
+ results.push(result);
423
+ if (result.outcome === "failed")
424
+ break;
425
+ }
426
+ return results;
427
+ }
428
+ async function runCheck(cwd, command, absoluteLogPath, logPath, environment) {
429
+ const output = await open(absoluteLogPath, "w", 0o600);
430
+ let completion;
431
+ try {
432
+ completion = await spawnCheck(command, cwd, output.fd, environment);
433
+ }
434
+ finally {
435
+ await output.close();
436
+ }
437
+ const outputSize = (await stat(absoluteLogPath)).size;
438
+ const outputReference = outputSize === 0
439
+ ? {}
440
+ : { logPath };
441
+ if (outputSize === 0)
442
+ await rm(absoluteLogPath, { force: true });
443
+ if (completion.error === undefined
444
+ && !completion.timedOut
445
+ && completion.code === 0) {
446
+ return {
447
+ name: command,
448
+ outcome: "passed",
449
+ ...outputReference
450
+ };
451
+ }
452
+ const diagnostic = outputSize === 0
453
+ ? undefined
454
+ : await lastCompleteDiagnosticLine(absoluteLogPath);
455
+ return {
456
+ name: command,
457
+ outcome: "failed",
458
+ details: [
459
+ checkFailureReason(completion),
460
+ ...(diagnostic === undefined ? [] : [diagnostic])
461
+ ].join(" "),
462
+ ...outputReference
463
+ };
464
+ }
465
+ async function spawnCheck(command, cwd, outputFd, environment) {
466
+ let child;
467
+ try {
468
+ child = spawn("/bin/sh", ["-lc", command], {
469
+ cwd,
470
+ env: environment,
471
+ stdio: ["ignore", outputFd, outputFd]
472
+ });
473
+ }
474
+ catch (error) {
475
+ return {
476
+ code: null,
477
+ signal: null,
478
+ error: error instanceof Error ? error : new Error(String(error)),
479
+ timedOut: false
480
+ };
481
+ }
482
+ let timedOut = false;
483
+ const timeout = setTimeout(() => {
484
+ timedOut = true;
485
+ child.kill("SIGTERM");
486
+ }, 30 * 60_000);
487
+ timeout.unref();
488
+ const completion = await new Promise((resolve) => {
489
+ child.once("error", (error) => {
490
+ resolve({ code: null, signal: null, error });
491
+ });
492
+ child.once("close", (code, signal) => {
493
+ resolve({ code, signal });
494
+ });
495
+ });
496
+ clearTimeout(timeout);
497
+ return { ...completion, timedOut };
498
+ }
499
+ async function integrationCheckEnvironment(source, runtime) {
500
+ const home = join(runtime.descriptor.roots.data, "home");
501
+ try {
502
+ await mkdir(home, { mode: 0o700 });
503
+ }
504
+ catch (error) {
505
+ if (!isNodeCode(error, "EEXIST"))
506
+ throw error;
507
+ }
508
+ const homeMetadata = await lstat(home);
509
+ if (!homeMetadata.isDirectory() || homeMetadata.isSymbolicLink()) {
510
+ throw new Error("Integration runtime HOME is not an owned directory.");
511
+ }
512
+ return Object.freeze({
513
+ ...selectEnvironment(source, INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES),
514
+ PATH: source.PATH || `${dirname(process.execPath)}:/usr/local/bin:/usr/bin:/bin`,
515
+ HOME: home,
516
+ TMPDIR: runtime.descriptor.roots.temporary,
517
+ TMP: runtime.descriptor.roots.temporary,
518
+ TEMP: runtime.descriptor.roots.temporary,
519
+ TMUX_TMPDIR: runtime.descriptor.roots.temporary,
520
+ ...runtime.environment
521
+ });
522
+ }
523
+ function isNodeCode(error, code) {
524
+ return typeof error === "object"
525
+ && error !== null
526
+ && "code" in error
527
+ && error.code === code;
528
+ }
529
+ function defaultIntegrationRuntimeIsolation(home) {
530
+ const controlHome = resolve(home);
531
+ return new FileTaskRuntimeIsolation({
532
+ runtimeRoot: integrationRuntimeRoot(),
533
+ pathLayout: "compact",
534
+ controlPlane: {
535
+ yuiHome: controlHome,
536
+ controllerSocketPath: controllerSocketPath(controlHome),
537
+ tmuxNamespace: yuiTmuxServerName(controlHome),
538
+ globalInstallPaths: [process.execPath]
539
+ }
540
+ });
541
+ }
542
+ function integrationRuntimeRoot() {
543
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
544
+ return join("/tmp", `yi-${uid.toString(36)}`);
545
+ }
546
+ function integrationRuntimeLaunchId(home, integrationId) {
547
+ const homeDigest = createHash("sha256")
548
+ .update(resolve(home))
549
+ .digest("hex");
550
+ return `${integrationId}-${homeDigest}`;
551
+ }
552
+ function checkFailureReason(completion) {
553
+ if (completion.timedOut)
554
+ return "Command timed out after 1800 seconds.";
555
+ if (completion.error !== undefined) {
556
+ return `Command failed to start: ${completion.error.message}`;
557
+ }
558
+ if (completion.code !== null)
559
+ return `Command exited with code ${completion.code}.`;
560
+ if (completion.signal !== null)
561
+ return `Command terminated by ${completion.signal}.`;
562
+ return "Command failed.";
563
+ }
564
+ async function lastCompleteDiagnosticLine(path) {
565
+ const handle = await open(path, "r");
566
+ try {
567
+ const info = await handle.stat();
568
+ const length = Math.min(info.size, 64 * 1024);
569
+ if (length === 0)
570
+ return undefined;
571
+ const buffer = Buffer.alloc(length);
572
+ await handle.read(buffer, 0, length, info.size - length);
573
+ let text = buffer.toString("utf8");
574
+ if (info.size > length) {
575
+ const firstLineEnd = text.indexOf("\n");
576
+ if (firstLineEnd < 0)
577
+ return undefined;
578
+ text = text.slice(firstLineEnd + 1);
579
+ }
580
+ const line = text.split(/\r?\n/u).map((value) => value.trim()).filter(Boolean).at(-1);
581
+ return line === undefined || line.length > 1_000 ? undefined : line;
582
+ }
583
+ finally {
584
+ await handle.close();
585
+ }
586
+ }
587
+ function integrationCheckDirectory(home, taskId, integrationId) {
588
+ return join(home, "artifacts", "integration-checks", taskId, integrationId);
589
+ }
590
+ async function git(args) {
591
+ try {
592
+ const result = await executeFile("git", [...args], {
593
+ encoding: "utf8",
594
+ maxBuffer: 16 * 1024 * 1024,
595
+ timeout: 120_000
596
+ });
597
+ return result.stdout;
598
+ }
599
+ catch (error) {
600
+ const stderr = typeof error === "object" && error !== null && "stderr" in error
601
+ ? String(error.stderr).trim()
602
+ : "";
603
+ throw new Error(stderr.length === 0 ? "Git command failed." : `Git command failed: ${stderr}`, {
604
+ cause: error
605
+ });
606
+ }
607
+ }
608
+ async function gitSucceeds(args) {
609
+ try {
610
+ await git(args);
611
+ return true;
612
+ }
613
+ catch {
614
+ return false;
615
+ }
616
+ }
617
+ async function gitLine(args) {
618
+ const value = (await git(args)).trim();
619
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(value)) {
620
+ throw new Error("Git returned an invalid commit.");
621
+ }
622
+ return value;
623
+ }
624
+ function requireIntegration(store, taskId, id) {
625
+ const attempt = store.getIntegrationAttempt(taskId, id);
626
+ if (attempt === null) {
627
+ throw new Error(`Integration Attempt not found: ${taskId}/${id}.`);
628
+ }
629
+ return attempt;
630
+ }
631
+ function fullTargetRef(value) {
632
+ if (value.startsWith("refs/"))
633
+ return value;
634
+ if (value.startsWith("-") || /[\r\n]/u.test(value)) {
635
+ throw new Error("Integration target ref is invalid.");
636
+ }
637
+ return `refs/heads/${value}`;
638
+ }
639
+ async function resolveRef(repositoryPath, ref) {
640
+ return gitLine([
641
+ "-C", repositoryPath, "rev-parse", "--verify", "--end-of-options",
642
+ `${fullTargetRef(ref)}^{commit}`
643
+ ]);
644
+ }
645
+ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expectedHead) {
646
+ const ref = fullTargetRef(targetRef);
647
+ const checkedOutPaths = await checkedOutWorktreePaths(repositoryPath, ref);
648
+ if (checkedOutPaths.length === 0) {
649
+ await git([
650
+ "-C", repositoryPath, "update-ref",
651
+ ref,
652
+ candidateCommit,
653
+ expectedHead
654
+ ]);
655
+ return;
656
+ }
657
+ if (checkedOutPaths.length > 1) {
658
+ throw new Error(`Integration target is checked out in multiple worktrees: ${targetRef}.`);
659
+ }
660
+ const checkout = checkedOutPaths[0];
661
+ const status = await git([
662
+ "-C", checkout, "status", "--porcelain=v1", "--untracked-files=all"
663
+ ]);
664
+ if (status.trim().length > 0) {
665
+ throw new Error(`Integration target worktree is not clean: ${checkout}.`);
666
+ }
667
+ const current = await gitLine(["-C", checkout, "rev-parse", "HEAD^{commit}"]);
668
+ if (current !== expectedHead) {
669
+ throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
670
+ }
671
+ await git(["-C", checkout, "merge", "--ff-only", "--no-edit", candidateCommit]);
672
+ const advanced = await resolveRef(repositoryPath, targetRef);
673
+ if (advanced !== candidateCommit) {
674
+ throw new Error(`Integration target did not advance to candidate: ${targetRef}.`);
675
+ }
676
+ const finalStatus = await git([
677
+ "-C", checkout, "status", "--porcelain=v1", "--untracked-files=all"
678
+ ]);
679
+ if (finalStatus.trim().length > 0) {
680
+ throw new Error(`Integration target worktree became dirty: ${checkout}.`);
681
+ }
682
+ }
683
+ async function checkedOutWorktreePaths(repositoryPath, targetRef) {
684
+ const porcelain = (await git([
685
+ "-C", repositoryPath, "worktree", "list", "--porcelain"
686
+ ])).trim();
687
+ if (porcelain.length === 0)
688
+ return [];
689
+ return porcelain.split(/\n\n+/u).flatMap((record) => {
690
+ const lines = record.split("\n");
691
+ const path = lines.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
692
+ const branch = lines.find((line) => line.startsWith("branch "))?.slice("branch ".length);
693
+ return path !== undefined && branch === targetRef ? [path] : [];
694
+ });
695
+ }