@zq-silk/yui 0.14.0 → 0.14.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 (179) hide show
  1. package/ARCHITECTURE.md +64 -35
  2. package/README.md +258 -125
  3. package/dist/agent/argumentPolicy.js +2 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  5. package/dist/cli/commandCatalog.js +74 -109
  6. package/dist/cli/helpRenderer.js +1 -2
  7. package/dist/cli/interactionCandidates.js +10 -0
  8. package/dist/cli/interactionPolicy.js +76 -37
  9. package/dist/cli/roleWizard.js +5 -6
  10. package/dist/cli/updateOrchestrator.js +10 -8
  11. package/dist/cli/updatePorts.js +17 -13
  12. package/dist/cli/upgradeCommand.js +2 -5
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/durableJobCommands.js +1 -1
  18. package/dist/commands/executionAuditCommands.js +4 -9
  19. package/dist/commands/globalRoleCommands.js +17 -0
  20. package/dist/commands/profileCommands.js +200 -30
  21. package/dist/commands/sessionCommands.js +1 -1
  22. package/dist/commands/taskActor.js +29 -127
  23. package/dist/commands/taskChangeSetCommands.js +1 -3
  24. package/dist/commands/taskCommands.js +1771 -2107
  25. package/dist/commands/taskCompletionGate.js +27 -103
  26. package/dist/commands/taskContextCommand.js +47 -80
  27. package/dist/commands/taskExecutionCommands.js +9 -27
  28. package/dist/commands/taskInputCommands.js +10 -10
  29. package/dist/commands/taskIntegrationCommands.js +69 -61
  30. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  31. package/dist/commands/taskNextActionCommand.js +8 -29
  32. package/dist/commands/taskOverviewCommand.js +25 -3
  33. package/dist/commands/taskPublicationCommands.js +151 -85
  34. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  35. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  36. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  37. package/dist/commands/taskUpstreamCommands.js +79 -95
  38. package/dist/config/configCatalog.js +1 -1
  39. package/dist/context/contextSnapshot.js +17 -2
  40. package/dist/context/sessionBootstrapManifest.js +18 -1
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +1 -0
  63. package/dist/domain/agentResultTransport.js +60 -0
  64. package/dist/execution/executionHealth.js +111 -118
  65. package/dist/execution/reviewMainTurn.js +161 -0
  66. package/dist/execution/workItemExecution.js +454 -0
  67. package/dist/execution/workItemExecutionProjection.js +309 -0
  68. package/dist/execution/workItemMainTurn.js +164 -0
  69. package/dist/executor/agentExecutor.js +19 -19
  70. package/dist/executor/executorRegistry.js +7 -28
  71. package/dist/executor/fileRoleLaunchPlanner.js +51 -83
  72. package/dist/external/githubPublicationVerifier.js +114 -0
  73. package/dist/external/gitlabPublicationVerifier.js +158 -0
  74. package/dist/external/pinnedCommandRunner.js +73 -0
  75. package/dist/integration/changeSetManifest.js +0 -3
  76. package/dist/integration/deliveryObligation.js +43 -6
  77. package/dist/integration/gitIntegrationService.js +284 -101
  78. package/dist/integration/integrationAttempt.js +64 -14
  79. package/dist/integration/integrationQueueEntry.js +5 -35
  80. package/dist/integration/integrationQueueService.js +65 -194
  81. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  82. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  83. package/dist/message/message.js +16 -0
  84. package/dist/observability/executionAudit.js +4 -25
  85. package/dist/observability/faultClassification.js +58 -70
  86. package/dist/observability/orchestrationMetrics.js +8 -50
  87. package/dist/profile/agentProfile.js +60 -15
  88. package/dist/profile/agentProfileRuntime.js +100 -0
  89. package/dist/release/releaseWorkflowPorts.js +5 -68
  90. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  91. package/dist/repository/gitWorkspace.js +9 -67
  92. package/dist/repository/taskBaseFreshness.js +3 -1
  93. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  94. package/dist/repository/taskWorkspacePreparer.js +252 -411
  95. package/dist/resources/liveReferences.js +6 -55
  96. package/dist/resources/resourceDiscovery.js +1 -30
  97. package/dist/resources/resourceRegistrar.js +0 -23
  98. package/dist/review/deltaRecheck.js +8 -65
  99. package/dist/review/reviewAcceptance.js +22 -19
  100. package/dist/review/reviewConfig.js +0 -10
  101. package/dist/review/reviewDecision.js +2 -2
  102. package/dist/review/reviewRound.js +147 -273
  103. package/dist/review/reviewerAvailability.js +13 -23
  104. package/dist/role/role.js +3 -0
  105. package/dist/runtime/agentDriverObservation.js +1 -1
  106. package/dist/runtime/agentHost.js +51 -51
  107. package/dist/runtime/builtinAgentDrivers.js +9 -2
  108. package/dist/runtime/codexAppServerRuntime.js +23 -5
  109. package/dist/runtime/exactControlPlane.js +2 -267
  110. package/dist/runtime/firstProgressAdvisory.js +1 -1
  111. package/dist/runtime/index.js +1 -1
  112. package/dist/runtime/launchBroker.js +11 -11
  113. package/dist/runtime/lifecycleReservation.js +2 -2
  114. package/dist/runtime/managedCaller.js +111 -0
  115. package/dist/runtime/ports.js +16 -3
  116. package/dist/runtime/preallocatedNativeSession.js +2 -2
  117. package/dist/runtime/processExitObservation.js +3 -2
  118. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  119. package/dist/runtime/runtimeBinding.js +1 -1
  120. package/dist/runtime/runtimeObservation.js +33 -16
  121. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  122. package/dist/runtime/runtimeStopReceipt.js +11 -11
  123. package/dist/runtime/sessionLaunchRequest.js +2 -2
  124. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  125. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  126. package/dist/runtime/sessionReconciliation.js +4 -4
  127. package/dist/runtime/sessionTerminationGuard.js +9 -9
  128. package/dist/runtime/sessionTokenMetrics.js +5 -7
  129. package/dist/runtime/structuredProviderHost.js +5 -5
  130. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  131. package/dist/runtime/tmuxAdapters.js +32 -30
  132. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  133. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  134. package/dist/scheduler/roleTurnLiveness.js +6 -6
  135. package/dist/scheduler/roleTurnStall.js +8 -9
  136. package/dist/scheduler/taskExecutionProjection.js +19 -41
  137. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  138. package/dist/storage/sqliteSchema.js +1 -21
  139. package/dist/storage/sqliteStore.js +62 -47
  140. package/dist/storage/storageSchema.js +1 -8
  141. package/dist/storage/storageVersions.js +7 -4
  142. package/dist/storage/taskStore.js +8 -8
  143. package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
  144. package/dist/task/completionReadiness.js +17 -98
  145. package/dist/task/deliveryGuard.js +9 -11
  146. package/dist/task/draftPlan.js +142 -0
  147. package/dist/task/nextAction.js +179 -477
  148. package/dist/task/publicationVerification.js +1 -0
  149. package/dist/task/remoteDelivery.js +233 -0
  150. package/dist/task/task.js +108 -4
  151. package/dist/task/taskRecordReference.js +0 -1
  152. package/dist/turn/turn.js +109 -16
  153. package/dist/verification/verificationGateService.js +0 -26
  154. package/dist/web/assets/client/components.js +99 -121
  155. package/dist/web/assets/client/i18n.js +136 -58
  156. package/dist/web/assets/client/view.js +44 -0
  157. package/dist/web/assets/styles/cards.js +3 -15
  158. package/dist/web/assets/styles/widgets.js +4 -0
  159. package/dist/web/webSnapshot.js +29 -14
  160. package/dist/workItem/dependencyGate.js +34 -0
  161. package/dist/workItem/workItem.js +99 -266
  162. package/dist/workspace/workItemChangeSetManager.js +15 -59
  163. package/docs/task-local-identity.md +1 -1
  164. package/i18n/README.zh-CN.md +165 -59
  165. package/package.json +1 -1
  166. package/skills/yui-leader/SKILL.md +272 -782
  167. package/skills/yui-operator/SKILL.md +190 -365
  168. package/skills/yui-reviewer/SKILL.md +40 -33
  169. package/skills/yui-runtime/SKILL.md +27 -6
  170. package/skills/yui-worker/SKILL.md +19 -6
  171. package/dist/execution/candidateConvergence.js +0 -623
  172. package/dist/execution/executionGroup.js +0 -854
  173. package/dist/execution/resourceBroker.js +0 -426
  174. package/dist/executor/launchPlan.js +0 -16
  175. package/dist/review/reviewFinding.js +0 -240
  176. package/dist/review/reviewFindingLedger.js +0 -550
  177. package/dist/review/reviewOutcomeClassifier.js +0 -255
  178. package/dist/scheduler/resourceQueueProjection.js +0 -71
  179. package/dist/task/repairWave.js +0 -150
@@ -15,6 +15,7 @@ import { resolveWorktreeRoot } from "../repository/taskWorkspacePreparer.js";
15
15
  import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenanceLock.js";
16
16
  import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
17
17
  import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
18
+ import { advanceTaskProjectCommit } from "../task/task.js";
18
19
  import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
19
20
  import { recordIntegrationCheckJob, requireResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
20
21
  import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
@@ -51,7 +52,9 @@ const INTEGRATION_OPERATIONAL_ENVIRONMENT_NAMES = [
51
52
  "CURL_CA_BUNDLE",
52
53
  "REQUESTS_CA_BUNDLE"
53
54
  ];
54
- export const REMOTE_BASELINE_CONFLICT_PREFIX = "Remote baseline merge conflicts";
55
+ export const REMOTE_BASELINE_CONFLICT_PREFIX = "Upstream rebase conflicts";
56
+ const WORK_ITEM_MERGE_CONFLICT_PREFIX = "WorkItem merge conflicts";
57
+ const MANUAL_INTEGRATION_PREFIX = "Manual WorkItem integration";
55
58
  export class GitIntegrationService {
56
59
  store;
57
60
  git;
@@ -75,7 +78,7 @@ export class GitIntegrationService {
75
78
  #resourceRegistrar() {
76
79
  return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
77
80
  }
78
- async integrate(taskId, integrationId, options = {}) {
81
+ async integrate(taskId, integrationId) {
79
82
  const initial = requireIntegration(this.store, taskId, integrationId);
80
83
  // The whole Integration is one Git transaction against the Project's
81
84
  // repository: worktree creation, cherry-picks, checks, and the target ref
@@ -94,6 +97,13 @@ export class GitIntegrationService {
94
97
  const project = this.store.getProject(initial.projectId);
95
98
  if (project === null)
96
99
  throw new Error(`Project not found: ${initial.projectId}.`);
100
+ const taskWorkspace = this.store.getTaskWorkspace(task.id);
101
+ const taskRepository = taskWorkspace?.entries.find(({ projectId }) => projectId === project.id)?.path;
102
+ if (taskWorkspace === null
103
+ || taskWorkspace.owner.type !== "task"
104
+ || taskRepository === undefined) {
105
+ throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
106
+ }
97
107
  // Issue 08: a Project with a VerificationPlan gates through its plan
98
108
  // (bootstrap + L2) and reuses exact-SHA artifacts; an unconfigured
99
109
  // Project keeps the existing explicit check path unchanged.
@@ -103,11 +113,11 @@ export class GitIntegrationService {
103
113
  let managedWorkspace;
104
114
  try {
105
115
  prepared = await this.git.ensureIntegrationWorktree({
106
- repositoryPath: project.path,
116
+ repositoryPath: taskRepository,
107
117
  container: join(this.worktreeRoot, project.name),
108
118
  taskSegment: taskWorkspaceRefSegment(task),
109
119
  integrationId: initial.id,
110
- baseRef: initial.expectedHead
120
+ baseRef: initial.beforeCommit
111
121
  });
112
122
  workspace = {
113
123
  projectId: project.id,
@@ -129,7 +139,7 @@ export class GitIntegrationService {
129
139
  access: "write",
130
140
  path: prepared.path,
131
141
  branch: prepared.branch,
132
- baseRef: initial.expectedHead,
142
+ baseRef: initial.beforeCommit,
133
143
  baseCommit: prepared.baseCommit
134
144
  }]
135
145
  }, this.now());
@@ -140,52 +150,17 @@ export class GitIntegrationService {
140
150
  return this.#fail(initial, error, "integration-preparation");
141
151
  }
142
152
  if (initial.status === "validating") {
143
- return this.#recoverValidating(initial, workspace, project.path);
153
+ return this.#recoverValidating(initial, workspace, taskRepository);
144
154
  }
145
155
  let current = initial;
146
156
  // A check DurableJob is the source of truth for a running attempt that
147
157
  // already bound one: never re-apply commits or spawn a second job. The
148
158
  // job's terminal wakeup drives the resume through `integration continue`.
149
159
  if (current.status === "running" && current.jobId !== undefined) {
150
- return this.#resumeCheckJob(current, workspace, prepared.path, project.path, gate);
160
+ return this.#resumeCheckJob(current, workspace, prepared.path, taskRepository, gate);
151
161
  }
152
162
  try {
153
- if (options.remoteBaseline !== undefined) {
154
- const mergeRemote = this.git.mergeRemoteIntoWorktree;
155
- if (mergeRemote === undefined) {
156
- throw new Error("Integration Git workspace does not support remote baseline reconciliation.");
157
- }
158
- await mergeRemote.call(this.git, {
159
- repositoryPath: prepared.path,
160
- remoteUrl: options.remoteBaseline.remoteUrl,
161
- branch: options.remoteBaseline.branch
162
- });
163
- }
164
- // A remote-baseline Attempt starts from the exact previously committed
165
- // Task head, so its ChangeSets are already represented by that tree.
166
- // Keep their IDs as provenance/check evidence, but never cherry-pick the
167
- // source commits again. A manual continuation of a remote merge carries
168
- // the same semantic marker in its conflict report.
169
- const remoteOnly = options.remoteBaseline !== undefined
170
- || (current.resolution?.action === "manual-resolution"
171
- && current.conflict?.summary.startsWith(REMOTE_BASELINE_CONFLICT_PREFIX));
172
- const plan = remoteOnly
173
- ? []
174
- : await integrationCommitPlan(this.store, task.id, project.path, current.changeSetIds, current.expectedHead);
175
- let remaining = plan;
176
- if (current.resolution?.action === "manual-resolution"
177
- && current.conflict?.summary.startsWith(REMOTE_BASELINE_CONFLICT_PREFIX)) {
178
- await completeRemoteBaselineResolution(prepared.path);
179
- }
180
- else if (current.resolution?.action === "manual-resolution") {
181
- const resolvedCommit = await completeManualResolution(prepared.path);
182
- const resolvedIndex = plan.findIndex(({ commit }) => commit === resolvedCommit);
183
- if (resolvedIndex < 0) {
184
- throw new Error(`Manual resolution commit is not part of the Integration plan: ${resolvedCommit}.`);
185
- }
186
- remaining = plan.slice(resolvedIndex + 1);
187
- }
188
- const conflict = await this.#applyCommits(current, workspace, prepared.path, remaining);
163
+ const conflict = await this.#applySource(current, workspace, prepared.path, project.remoteUrl);
189
164
  if (conflict !== undefined) {
190
165
  return conflict;
191
166
  }
@@ -193,12 +168,12 @@ export class GitIntegrationService {
193
168
  // moved or its worktree is dirty, so the check commands never run on a
194
169
  // target that cannot be advanced. advanceTargetRef re-verifies both
195
170
  // after the checks, so a move during the gate is still fenced at CAS.
196
- await assertTargetReadyForChecks(project.path, current.targetRef, current.expectedHead);
171
+ await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit);
197
172
  if (gate !== undefined) {
198
- return this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, project.path, gate);
173
+ return this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
199
174
  }
200
175
  if (this.jobPort !== undefined && current.checkCommands.length > 0) {
201
- return this.#startCheckJob(current, workspace, prepared.path, managedWorkspace, project.path);
176
+ return this.#startCheckJob(current, workspace, prepared.path, managedWorkspace, taskRepository);
202
177
  }
203
178
  const checkedHead = await gitLine(["-C", prepared.path, "rev-parse", "HEAD^{commit}"]);
204
179
  const checkResults = await this.#runChecks(current, managedWorkspace, prepared.path);
@@ -221,9 +196,8 @@ export class GitIntegrationService {
221
196
  checks: checkResults
222
197
  }, this.now());
223
198
  this.store.saveIntegrationAttempt(task.id, current);
224
- await advanceTargetRef(project.path, current.targetRef, candidateCommit, current.expectedHead);
225
- const committed = updateIntegrationAttempt(current, { status: "committed" }, this.now());
226
- this.store.saveIntegrationAttempt(task.id, committed);
199
+ await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit);
200
+ const committed = this.#recordCommitted(current);
227
201
  return this.#terminalResult("committed", committed, workspace);
228
202
  }
229
203
  catch (error) {
@@ -236,11 +210,18 @@ export class GitIntegrationService {
236
210
  return { status: "blocked", attempt: pending, workspace };
237
211
  }
238
212
  if (current.status === "validating" && current.candidateCommit !== undefined) {
239
- const target = await resolveRef(project.path, current.targetRef);
213
+ const target = await resolveRef(taskRepository, current.targetRef);
240
214
  if (target === current.candidateCommit) {
241
- const committed = updateIntegrationAttempt(current, { status: "committed" }, this.now());
242
- this.store.saveIntegrationAttempt(task.id, committed);
243
- return this.#terminalResult("committed", committed, workspace);
215
+ try {
216
+ await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit);
217
+ const committed = this.#recordCommitted(current);
218
+ return this.#terminalResult("committed", committed, workspace);
219
+ }
220
+ catch {
221
+ // Preserve the original failure below; a target ref at the
222
+ // candidate is insufficient when its checked-out tree is dirty or
223
+ // incomplete after an interrupted CAS application.
224
+ }
244
225
  }
245
226
  }
246
227
  return this.#fail(current, error, "integration", workspace);
@@ -263,13 +244,17 @@ export class GitIntegrationService {
263
244
  const project = this.store.getProject(integration.projectId);
264
245
  if (project === null)
265
246
  throw new Error(`Project not found: ${integration.projectId}.`);
247
+ const taskRepository = this.store.getTaskWorkspace(task.id)?.entries.find(({ projectId }) => projectId === project.id)?.path;
248
+ if (taskRepository === undefined) {
249
+ throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
250
+ }
266
251
  const managedWorkspace = this.store.getIntegrationWorkspace(integration.taskId, integration.id);
267
252
  if (managedWorkspace !== null) {
268
253
  const runtime = this.#runtimePreparation(integration, managedWorkspace);
269
254
  this.runtimeIsolation.cleanup(runtime, integration.status === "committed" ? "completion" : "failure");
270
255
  }
271
256
  const result = await this.git.removeIntegrationWorktree({
272
- repositoryPath: project.path,
257
+ repositoryPath: taskRepository,
273
258
  container: join(this.worktreeRoot, project.name),
274
259
  taskSegment: taskWorkspaceRefSegment(task),
275
260
  integrationId: integration.id,
@@ -417,7 +402,7 @@ export class GitIntegrationService {
417
402
  level: "L2",
418
403
  commit: job.head,
419
404
  targetRef: attempt.targetRef,
420
- baseHead: attempt.expectedHead
405
+ baseHead: attempt.beforeCommit
421
406
  });
422
407
  try {
423
408
  const artifact = await recordGateArtifactFromJob(this.store, this.home, identity, gate.plan, job, this.now());
@@ -451,9 +436,8 @@ export class GitIntegrationService {
451
436
  checks
452
437
  }, this.now());
453
438
  this.store.saveIntegrationAttempt(attempt.taskId, validating);
454
- await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.expectedHead);
455
- const committed = updateIntegrationAttempt(validating, { status: "committed" }, this.now());
456
- this.store.saveIntegrationAttempt(attempt.taskId, committed);
439
+ await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit);
440
+ const committed = this.#recordCommitted(validating);
457
441
  return this.#terminalResult("committed", committed, workspace);
458
442
  }
459
443
  async #runChecks(attempt, workspace, path) {
@@ -503,7 +487,7 @@ export class GitIntegrationService {
503
487
  level: "L2",
504
488
  commit: candidateCommit,
505
489
  targetRef: attempt.targetRef,
506
- baseHead: attempt.expectedHead
490
+ baseHead: attempt.beforeCommit
507
491
  });
508
492
  if (gate.mode !== "record") {
509
493
  const existing = await lookupReusableGateArtifact(this.store, identity);
@@ -558,25 +542,136 @@ export class GitIntegrationService {
558
542
  checks
559
543
  }, this.now());
560
544
  this.store.saveIntegrationAttempt(attempt.taskId, validating);
561
- await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.expectedHead);
562
- const committed = updateIntegrationAttempt(validating, { status: "committed" }, this.now());
563
- this.store.saveIntegrationAttempt(attempt.taskId, committed);
545
+ await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit);
546
+ const committed = this.#recordCommitted(validating);
564
547
  return this.#terminalResult("committed", committed, workspace);
565
548
  }
566
549
  #runtimePreparation(attempt, workspace) {
567
550
  return this.runtimeIsolation.preflight({
568
551
  workspace,
569
- launchId: integrationRuntimeLaunchId(this.home, attempt.id),
552
+ runtimeGenerationId: integrationRuntimeRuntimeGenerationId(this.home, attempt.id),
570
553
  generationId: "integration-checks",
571
554
  allowExactActive: true
572
555
  });
573
556
  }
574
- async #applyCommits(attempt, workspace, candidatePath, commits) {
575
- for (const { changeSetId, commit } of commits) {
557
+ async #applySource(attempt, workspace, candidatePath, remoteUrl) {
558
+ if (attempt.source.kind === "historical-change-sets") {
559
+ throw new Error(`Historical Integration cannot be resumed under the current source contract: ${attempt.id}.`);
560
+ }
561
+ if (attempt.source.kind === "upstream") {
562
+ if (remoteUrl === undefined) {
563
+ throw new Error(`Project has no remote URL for upstream Integration: ${attempt.projectId}.`);
564
+ }
565
+ if (attempt.resolution?.action === "manual-resolution"
566
+ && attempt.conflict?.summary.startsWith(REMOTE_BASELINE_CONFLICT_PREFIX)) {
567
+ await continueUpstreamRebase(candidatePath);
568
+ return undefined;
569
+ }
570
+ const fetchRemote = this.git.fetchRemoteHeadIntoWorktree;
571
+ if (fetchRemote === undefined) {
572
+ throw new Error("Integration Git workspace cannot fetch an upstream source.");
573
+ }
574
+ const fetched = await fetchRemote.call(this.git, {
575
+ repositoryPath: candidatePath,
576
+ remoteUrl,
577
+ branch: attempt.source.branch
578
+ });
579
+ if (fetched.commit !== attempt.source.remoteCommit) {
580
+ throw new Error(`Upstream branch moved before Integration: expected ${attempt.source.remoteCommit}, fetched ${fetched.commit}.`);
581
+ }
582
+ if (await this.git.isAncestor(candidatePath, attempt.source.remoteCommit, attempt.beforeCommit)) {
583
+ return undefined;
584
+ }
585
+ try {
586
+ await git([
587
+ "-C", candidatePath,
588
+ "-c", "user.name=Yui",
589
+ "-c", "user.email=yui@local",
590
+ "rebase", "--onto",
591
+ attempt.source.remoteCommit,
592
+ attempt.source.taskBaseCommit,
593
+ workspace.branch
594
+ ]);
595
+ }
596
+ catch (error) {
597
+ const affectedPaths = await conflictedPaths(candidatePath);
598
+ if (affectedPaths.length === 0)
599
+ throw error;
600
+ const pending = requireResolutionDecision(attempt, {
601
+ affectedPaths,
602
+ summary: `${REMOTE_BASELINE_CONFLICT_PREFIX} in ${attempt.targetRef}.`
603
+ }, this.now());
604
+ this.store.saveIntegrationAttempt(attempt.taskId, pending);
605
+ return { status: "blocked", attempt: pending, workspace };
606
+ }
607
+ return undefined;
608
+ }
609
+ if (attempt.source.strategy === "manual") {
610
+ if (attempt.resolution?.action === "manual-resolution"
611
+ && attempt.conflict?.summary.startsWith(MANUAL_INTEGRATION_PREFIX)) {
612
+ if (!await this.git.isClean(candidatePath)) {
613
+ throw new Error("Manual Integration workspace must be clean and committed.");
614
+ }
615
+ return undefined;
616
+ }
617
+ const pending = requireResolutionDecision(attempt, {
618
+ affectedPaths: [],
619
+ summary: `${MANUAL_INTEGRATION_PREFIX}; apply the selected WorkItem result in the Integration workspace.`
620
+ }, this.now());
621
+ this.store.saveIntegrationAttempt(attempt.taskId, pending);
622
+ return { status: "blocked", attempt: pending, workspace };
623
+ }
624
+ if (attempt.source.strategy === "ff") {
625
+ await git([
626
+ "-C", candidatePath,
627
+ "merge", "--ff-only",
628
+ attempt.source.resultCommit
629
+ ]);
630
+ return undefined;
631
+ }
632
+ if (attempt.source.strategy === "merge") {
633
+ if (attempt.resolution?.action === "manual-resolution"
634
+ && attempt.conflict?.summary.startsWith(WORK_ITEM_MERGE_CONFLICT_PREFIX)) {
635
+ await completeMergeResolution(candidatePath);
636
+ return undefined;
637
+ }
638
+ try {
639
+ await git([
640
+ "-C", candidatePath,
641
+ "-c", "user.name=Yui",
642
+ "-c", "user.email=yui@local",
643
+ "merge", "--no-edit", "--no-ff",
644
+ attempt.source.resultCommit
645
+ ]);
646
+ }
647
+ catch (error) {
648
+ const affectedPaths = await conflictedPaths(candidatePath);
649
+ if (affectedPaths.length === 0)
650
+ throw error;
651
+ const pending = requireResolutionDecision(attempt, {
652
+ affectedPaths,
653
+ summary: `${WORK_ITEM_MERGE_CONFLICT_PREFIX} in ${attempt.targetRef}.`
654
+ }, this.now());
655
+ this.store.saveIntegrationAttempt(attempt.taskId, pending);
656
+ return { status: "blocked", attempt: pending, workspace };
657
+ }
658
+ return undefined;
659
+ }
660
+ const commits = await commitsBetween(candidatePath, attempt.source.startCommit, attempt.source.resultCommit, attempt.source.workItemId);
661
+ let remaining = commits;
662
+ if (attempt.resolution?.action === "manual-resolution") {
663
+ const resolvedCommit = await completeCherryPickResolution(candidatePath);
664
+ const resolvedIndex = commits.findIndex(({ commit }) => commit === resolvedCommit);
665
+ if (resolvedIndex < 0) {
666
+ throw new Error(`Manual resolution commit is not part of the WorkItem result: ${resolvedCommit}.`);
667
+ }
668
+ remaining = commits.slice(resolvedIndex + 1);
669
+ }
670
+ for (const { label, commit } of remaining) {
576
671
  // Fast-forward when the commit is a direct descendant of HEAD: this
577
672
  // preserves the original commit SHA, which exact-SHA review evidence
578
673
  // relies on. Fall back to cherry-pick when the target moved since the
579
- // ChangeSet was based (the commit is no longer a direct descendant).
674
+ // WorkItem was based (the commit is no longer a direct descendant).
580
675
  if (await gitSucceeds(["-C", candidatePath, "merge", "--ff-only", commit])) {
581
676
  continue;
582
677
  }
@@ -594,7 +689,7 @@ export class GitIntegrationService {
594
689
  }
595
690
  const pending = requireResolutionDecision(attempt, {
596
691
  affectedPaths,
597
- summary: `ChangeSet ${changeSetId} conflicts with ${attempt.targetRef}.`
692
+ summary: `${label} conflicts with ${attempt.targetRef}.`
598
693
  }, this.now());
599
694
  this.store.saveIntegrationAttempt(attempt.taskId, pending);
600
695
  return { status: "blocked", attempt: pending, workspace };
@@ -607,21 +702,46 @@ export class GitIntegrationService {
607
702
  return this.#fail(attempt, new Error("Validating Integration is missing its candidate commit or checks."), "integration-recovery", workspace);
608
703
  }
609
704
  const target = await resolveRef(repositoryPath, attempt.targetRef);
610
- if (target === attempt.expectedHead) {
705
+ if (target === attempt.beforeCommit) {
611
706
  try {
612
- await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.expectedHead);
707
+ await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit);
613
708
  }
614
709
  catch (error) {
615
710
  return this.#fail(attempt, error, "integration-recovery", workspace);
616
711
  }
617
712
  }
618
713
  else if (target !== attempt.candidateCommit) {
619
- return this.#fail(attempt, new Error(`Target moved to ${target}; expected ${attempt.expectedHead}.`), "integration-recovery", workspace);
714
+ return this.#fail(attempt, new Error(`Target moved to ${target}; expected ${attempt.beforeCommit}.`), "integration-recovery", workspace);
620
715
  }
621
- const committed = updateIntegrationAttempt(attempt, { status: "committed" }, this.now());
622
- this.store.saveIntegrationAttempt(attempt.taskId, committed);
716
+ await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit);
717
+ const committed = this.#recordCommitted(attempt);
623
718
  return this.#terminalResult("committed", committed, workspace);
624
719
  }
720
+ #recordCommitted(attempt) {
721
+ if (attempt.candidateCommit === undefined) {
722
+ throw new Error(`Committed Integration has no candidate commit: ${attempt.id}.`);
723
+ }
724
+ const candidateCommit = attempt.candidateCommit;
725
+ return this.store.transaction((tx) => {
726
+ const task = tx.getTask(attempt.taskId);
727
+ if (task === null)
728
+ throw new Error(`Task not found: ${attempt.taskId}.`);
729
+ const binding = task.projectBindings.find(({ projectId }) => projectId === attempt.projectId);
730
+ if (binding === undefined) {
731
+ throw new Error(`Task Project binding not found: ${task.id}/${attempt.projectId}.`);
732
+ }
733
+ if (binding.currentCommit !== candidateCommit) {
734
+ tx.saveTask(advanceTaskProjectCommit(task, attempt.projectId, attempt.beforeCommit, candidateCommit, this.now()));
735
+ }
736
+ const committed = updateIntegrationAttempt(attempt, {
737
+ status: "committed",
738
+ afterCommit: candidateCommit,
739
+ summary: integrationSummary(attempt)
740
+ }, this.now());
741
+ tx.saveIntegrationAttempt(attempt.taskId, committed);
742
+ return committed;
743
+ });
744
+ }
625
745
  #fail(attempt, error, checkName, workspace) {
626
746
  const failed = updateIntegrationAttempt(attempt, {
627
747
  status: "failed",
@@ -651,6 +771,33 @@ export class GitIntegrationService {
651
771
  };
652
772
  }
653
773
  }
774
+ function integrationSummary(attempt) {
775
+ const unchanged = attempt.beforeCommit === attempt.candidateCommit;
776
+ if (attempt.source.kind === "upstream") {
777
+ return unchanged
778
+ ? `Upstream ${attempt.source.remoteCommit} was already represented; Task head was unchanged.`
779
+ : `Rebased Task changes onto upstream ${attempt.source.remoteCommit}.`;
780
+ }
781
+ if (attempt.source.kind === "historical-change-sets") {
782
+ return unchanged
783
+ ? "Historical Integration was already represented; Task head was unchanged."
784
+ : "Historical Integration advanced the Task head.";
785
+ }
786
+ if (attempt.source.strategy === "manual") {
787
+ const decision = attempt.resolution;
788
+ if (decision?.action !== "manual-resolution") {
789
+ throw new Error(`Manual Integration has no Task-control resolution decision: ${attempt.id}.`);
790
+ }
791
+ return unchanged
792
+ ? `WorkItem ${attempt.source.workItemId} was intentionally not applied; `
793
+ + `Task head was unchanged. Rationale: ${decision.rationale}`
794
+ : `Integrated WorkItem ${attempt.source.workItemId} with manual resolution. `
795
+ + `Rationale: ${decision.rationale}`;
796
+ }
797
+ return unchanged
798
+ ? `WorkItem ${attempt.source.workItemId} result was already represented; Task head was unchanged.`
799
+ : `Integrated WorkItem ${attempt.source.workItemId} with ${attempt.source.strategy}.`;
800
+ }
654
801
  async function isEmptyCherryPick(path, commit) {
655
802
  let cherryPickHead;
656
803
  try {
@@ -664,32 +811,23 @@ async function isEmptyCherryPick(path, commit) {
664
811
  return cherryPickHead === commit
665
812
  && await gitSucceeds(["-C", path, "diff", "--cached", "--quiet"]);
666
813
  }
667
- async function integrationCommitPlan(store, taskId, repositoryPath, changeSetIds, expectedHead) {
814
+ async function commitsBetween(repositoryPath, startCommit, resultCommit, workItemId) {
815
+ const commits = (await git([
816
+ "-C", repositoryPath, "rev-list", "--reverse",
817
+ `${startCommit}..${resultCommit}`
818
+ ])).trim().split("\n").filter(Boolean);
668
819
  const plan = [];
669
- for (const changeSetId of changeSetIds) {
670
- const changeSet = store.getChangeSet(taskId, changeSetId);
671
- if (changeSet === null)
672
- throw new Error(`ChangeSet not found: ${changeSetId}.`);
673
- const commits = (await git([
674
- "-C", repositoryPath, "rev-list", "--reverse",
675
- `${changeSet.baseCommit}..${changeSet.headCommit}`
676
- ])).trim().split("\n").filter(Boolean);
677
- for (const commit of commits) {
678
- // A Task-main recovery may capture commits after they are already
679
- // present on the exact target. Treat those commits as applied rather
680
- // than attempting an empty cherry-pick; the later checks and CAS still
681
- // fence the committed Integration to expectedHead.
682
- if (await gitSucceeds([
683
- "-C", repositoryPath,
684
- "merge-base", "--is-ancestor", commit, expectedHead
685
- ]))
686
- continue;
687
- plan.push({ changeSetId, commit });
688
- }
820
+ for (const commit of commits) {
821
+ if (await gitSucceeds([
822
+ "-C", repositoryPath,
823
+ "merge-base", "--is-ancestor", commit, "HEAD"
824
+ ]))
825
+ continue;
826
+ plan.push({ label: `WorkItem ${workItemId} commit ${commit}`, commit });
689
827
  }
690
828
  return plan;
691
829
  }
692
- async function completeManualResolution(path) {
830
+ async function completeCherryPickResolution(path) {
693
831
  const unmerged = (await git(["-C", path, "diff", "--name-only", "--diff-filter=U"])).trim();
694
832
  if (unmerged.length > 0) {
695
833
  throw new Error(`Manual resolution is incomplete: ${unmerged.split("\n").join(", ")}.`);
@@ -713,16 +851,16 @@ async function completeManualResolution(path) {
713
851
  }
714
852
  return cherryPickHead;
715
853
  }
716
- async function completeRemoteBaselineResolution(path) {
717
- const unmerged = (await git(["-C", path, "diff", "--name-only", "--diff-filter=U"])).trim();
718
- if (unmerged.length > 0) {
719
- throw new Error(`Manual remote baseline resolution is incomplete: ${unmerged.split("\n").join(", ")}.`);
854
+ async function completeMergeResolution(path) {
855
+ const affected = await conflictedPaths(path);
856
+ if (affected.length > 0) {
857
+ throw new Error(`Manual merge resolution is incomplete: ${affected.join(", ")}.`);
720
858
  }
721
859
  try {
722
860
  await gitLine(["-C", path, "rev-parse", "--verify", "MERGE_HEAD"]);
723
861
  }
724
862
  catch {
725
- throw new Error("Manual remote baseline resolution has no active merge.");
863
+ throw new Error("Manual WorkItem resolution has no active merge.");
726
864
  }
727
865
  await git([
728
866
  "-C", path,
@@ -731,6 +869,40 @@ async function completeRemoteBaselineResolution(path) {
731
869
  "commit", "--no-edit"
732
870
  ]);
733
871
  }
872
+ async function continueUpstreamRebase(path) {
873
+ const affected = await conflictedPaths(path);
874
+ if (affected.length > 0) {
875
+ throw new RemoteBaselineConflictError(affected, `${REMOTE_BASELINE_CONFLICT_PREFIX}: ${affected.join(", ")}.`);
876
+ }
877
+ try {
878
+ await gitLine(["-C", path, "rev-parse", "--verify", "REBASE_HEAD"]);
879
+ }
880
+ catch {
881
+ throw new Error("Manual upstream resolution has no active rebase.");
882
+ }
883
+ try {
884
+ await git([
885
+ "-C", path,
886
+ "-c", "user.name=Yui",
887
+ "-c", "user.email=yui@local",
888
+ "-c", "core.editor=true",
889
+ "rebase", "--continue"
890
+ ]);
891
+ }
892
+ catch (error) {
893
+ const nextAffected = await conflictedPaths(path);
894
+ if (nextAffected.length > 0) {
895
+ throw new RemoteBaselineConflictError(nextAffected, `${REMOTE_BASELINE_CONFLICT_PREFIX}: ${nextAffected.join(", ")}.`, { cause: error });
896
+ }
897
+ throw error;
898
+ }
899
+ }
900
+ async function conflictedPaths(path) {
901
+ return (await git([
902
+ "-C", path,
903
+ "diff", "--name-only", "--diff-filter=U"
904
+ ])).trim().split("\n").filter(Boolean);
905
+ }
734
906
  /**
735
907
  * Map a terminal check job back to the attempt's CheckResult[] shape. The
736
908
  * runner stops at the first failing step, so unreached checks are "skipped"
@@ -925,7 +1097,7 @@ function integrationRuntimeRoot() {
925
1097
  const uid = typeof process.getuid === "function" ? process.getuid() : 0;
926
1098
  return join("/tmp", `yi-${uid.toString(36)}`);
927
1099
  }
928
- function integrationRuntimeLaunchId(home, integrationId) {
1100
+ function integrationRuntimeRuntimeGenerationId(home, integrationId) {
929
1101
  const homeDigest = createHash("sha256")
930
1102
  .update(resolve(home))
931
1103
  .digest("hex");
@@ -1098,7 +1270,18 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
1098
1270
  if (current !== expectedHead) {
1099
1271
  throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
1100
1272
  }
1101
- await git(["-C", checkout, "merge", "--ff-only", "--no-edit", candidateCommit]);
1273
+ // The candidate may be a rebased upstream result and therefore need not be
1274
+ // a descendant of the old Task head. Advance the branch with a real
1275
+ // compare-and-swap, then update only this checkout's index and worktree.
1276
+ // `read-tree` never moves the ref, so an external ref change is not
1277
+ // overwritten while the checked-out files are synchronized.
1278
+ await git([
1279
+ "-C", repositoryPath, "update-ref",
1280
+ ref,
1281
+ candidateCommit,
1282
+ expectedHead
1283
+ ]);
1284
+ await git(["-C", checkout, "read-tree", "--reset", "-u", candidateCommit]);
1102
1285
  const advanced = await resolveRef(repositoryPath, targetRef);
1103
1286
  if (advanced !== candidateCommit) {
1104
1287
  throw new Error(`Integration target did not advance to candidate: ${targetRef}.`);