@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.
- package/ARCHITECTURE.md +64 -35
- package/README.md +258 -125
- package/dist/agent/argumentPolicy.js +2 -1
- package/dist/agent/managedRuntimeEnvironment.js +5 -8
- package/dist/cli/commandCatalog.js +74 -109
- package/dist/cli/helpRenderer.js +1 -2
- package/dist/cli/interactionCandidates.js +10 -0
- package/dist/cli/interactionPolicy.js +76 -37
- package/dist/cli/roleWizard.js +5 -6
- package/dist/cli/updateOrchestrator.js +10 -8
- package/dist/cli/updatePorts.js +17 -13
- package/dist/cli/upgradeCommand.js +2 -5
- package/dist/cli.js +268 -231
- package/dist/commands/agentCommands.js +13 -0
- package/dist/commands/configCommands.js +7 -19
- package/dist/commands/configOverview.js +3 -2
- package/dist/commands/durableJobCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +4 -9
- package/dist/commands/globalRoleCommands.js +17 -0
- package/dist/commands/profileCommands.js +200 -30
- package/dist/commands/sessionCommands.js +1 -1
- package/dist/commands/taskActor.js +29 -127
- package/dist/commands/taskChangeSetCommands.js +1 -3
- package/dist/commands/taskCommands.js +1771 -2107
- package/dist/commands/taskCompletionGate.js +27 -103
- package/dist/commands/taskContextCommand.js +47 -80
- package/dist/commands/taskExecutionCommands.js +9 -27
- package/dist/commands/taskInputCommands.js +10 -10
- package/dist/commands/taskIntegrationCommands.js +69 -61
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -29
- package/dist/commands/taskOverviewCommand.js +25 -3
- package/dist/commands/taskPublicationCommands.js +151 -85
- package/dist/commands/taskPublicationVerifyCommand.js +181 -0
- package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
- package/dist/commands/taskRoleRuntimeStatus.js +5 -6
- package/dist/commands/taskUpstreamCommands.js +79 -95
- package/dist/config/configCatalog.js +1 -1
- package/dist/context/contextSnapshot.js +17 -2
- package/dist/context/sessionBootstrapManifest.js +18 -1
- package/dist/context/sourceTurnContext.js +30 -0
- package/dist/context/turnContextPack.js +107 -48
- package/dist/context/turnInputContract.js +11 -4
- package/dist/context/wakeNotification.js +84 -22
- package/dist/controller/agentRuntimeObserver.js +9 -12
- package/dist/controller/clientRuntime.js +13 -13
- package/dist/controller/controller.js +11 -11
- package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
- package/dist/controller/jobControl.js +11 -10
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +2 -2
- package/dist/controller/runtime.js +18 -61
- package/dist/controller/runtimeEventInbox.js +57 -34
- package/dist/controller/runtimeEventProcessor.js +36 -96
- package/dist/controller/runtimeHookTurnFence.js +33 -57
- package/dist/controller/runtimeLaunchCoordinator.js +44 -40
- package/dist/controller/runtimeObservationHook.js +18 -12
- package/dist/controller/sessionNotify.js +43 -28
- package/dist/controller/sessionOwnerReconciliation.js +21 -21
- package/dist/controller/structuredProviderObservation.js +13 -16
- package/dist/coordination/workMailboxQueue.js +132 -0
- package/dist/core/controllerServer.js +1 -0
- package/dist/domain/agentResultTransport.js +60 -0
- package/dist/execution/executionHealth.js +111 -118
- package/dist/execution/reviewMainTurn.js +161 -0
- package/dist/execution/workItemExecution.js +454 -0
- package/dist/execution/workItemExecutionProjection.js +309 -0
- package/dist/execution/workItemMainTurn.js +164 -0
- package/dist/executor/agentExecutor.js +19 -19
- package/dist/executor/executorRegistry.js +7 -28
- package/dist/executor/fileRoleLaunchPlanner.js +51 -83
- package/dist/external/githubPublicationVerifier.js +114 -0
- package/dist/external/gitlabPublicationVerifier.js +158 -0
- package/dist/external/pinnedCommandRunner.js +73 -0
- package/dist/integration/changeSetManifest.js +0 -3
- package/dist/integration/deliveryObligation.js +43 -6
- package/dist/integration/gitIntegrationService.js +284 -101
- package/dist/integration/integrationAttempt.js +64 -14
- package/dist/integration/integrationQueueEntry.js +5 -35
- package/dist/integration/integrationQueueService.js +65 -194
- package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
- package/dist/lifecycle/exactTurnTerminalization.js +100 -97
- package/dist/message/message.js +16 -0
- package/dist/observability/executionAudit.js +4 -25
- package/dist/observability/faultClassification.js +58 -70
- package/dist/observability/orchestrationMetrics.js +8 -50
- package/dist/profile/agentProfile.js +60 -15
- package/dist/profile/agentProfileRuntime.js +100 -0
- package/dist/release/releaseWorkflowPorts.js +5 -68
- package/dist/repository/executionLaneGitSnapshot.js +80 -0
- package/dist/repository/gitWorkspace.js +9 -67
- package/dist/repository/taskBaseFreshness.js +3 -1
- package/dist/repository/taskWorkspaceCoordinator.js +32 -4
- package/dist/repository/taskWorkspacePreparer.js +252 -411
- package/dist/resources/liveReferences.js +6 -55
- package/dist/resources/resourceDiscovery.js +1 -30
- package/dist/resources/resourceRegistrar.js +0 -23
- package/dist/review/deltaRecheck.js +8 -65
- package/dist/review/reviewAcceptance.js +22 -19
- package/dist/review/reviewConfig.js +0 -10
- package/dist/review/reviewDecision.js +2 -2
- package/dist/review/reviewRound.js +147 -273
- package/dist/review/reviewerAvailability.js +13 -23
- package/dist/role/role.js +3 -0
- package/dist/runtime/agentDriverObservation.js +1 -1
- package/dist/runtime/agentHost.js +51 -51
- package/dist/runtime/builtinAgentDrivers.js +9 -2
- package/dist/runtime/codexAppServerRuntime.js +23 -5
- package/dist/runtime/exactControlPlane.js +2 -267
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +11 -11
- package/dist/runtime/lifecycleReservation.js +2 -2
- package/dist/runtime/managedCaller.js +111 -0
- package/dist/runtime/ports.js +16 -3
- package/dist/runtime/preallocatedNativeSession.js +2 -2
- package/dist/runtime/processExitObservation.js +3 -2
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/runtimeBinding.js +1 -1
- package/dist/runtime/runtimeObservation.js +33 -16
- package/dist/runtime/runtimeSessionCandidate.js +2 -2
- package/dist/runtime/runtimeStopReceipt.js +11 -11
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/sessionOwnerIdentity.js +9 -9
- package/dist/runtime/sessionOwnerRegistry.js +13 -13
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTerminationGuard.js +9 -9
- package/dist/runtime/sessionTokenMetrics.js +5 -7
- package/dist/runtime/structuredProviderHost.js +5 -5
- package/dist/runtime/taskRuntimeIsolation.js +24 -24
- package/dist/runtime/tmuxAdapters.js +32 -30
- package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
- package/dist/scheduler/leaderWakeupProcessor.js +78 -55
- package/dist/scheduler/roleTurnLiveness.js +6 -6
- package/dist/scheduler/roleTurnStall.js +8 -9
- package/dist/scheduler/taskExecutionProjection.js +19 -41
- package/dist/scheduler/taskObservabilityProjection.js +75 -122
- package/dist/storage/sqliteSchema.js +1 -21
- package/dist/storage/sqliteStore.js +62 -47
- package/dist/storage/storageSchema.js +1 -8
- package/dist/storage/storageVersions.js +7 -4
- package/dist/storage/taskStore.js +8 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
- package/dist/task/completionReadiness.js +17 -98
- package/dist/task/deliveryGuard.js +9 -11
- package/dist/task/draftPlan.js +142 -0
- package/dist/task/nextAction.js +179 -477
- package/dist/task/publicationVerification.js +1 -0
- package/dist/task/remoteDelivery.js +233 -0
- package/dist/task/task.js +108 -4
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/turn/turn.js +109 -16
- package/dist/verification/verificationGateService.js +0 -26
- package/dist/web/assets/client/components.js +99 -121
- package/dist/web/assets/client/i18n.js +136 -58
- package/dist/web/assets/client/view.js +44 -0
- package/dist/web/assets/styles/cards.js +3 -15
- package/dist/web/assets/styles/widgets.js +4 -0
- package/dist/web/webSnapshot.js +29 -14
- package/dist/workItem/dependencyGate.js +34 -0
- package/dist/workItem/workItem.js +99 -266
- package/dist/workspace/workItemChangeSetManager.js +15 -59
- package/docs/task-local-identity.md +1 -1
- package/i18n/README.zh-CN.md +165 -59
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +272 -782
- package/skills/yui-operator/SKILL.md +190 -365
- package/skills/yui-reviewer/SKILL.md +40 -33
- package/skills/yui-runtime/SKILL.md +27 -6
- package/skills/yui-worker/SKILL.md +19 -6
- package/dist/execution/candidateConvergence.js +0 -623
- package/dist/execution/executionGroup.js +0 -854
- package/dist/execution/resourceBroker.js +0 -426
- package/dist/executor/launchPlan.js +0 -16
- package/dist/review/reviewFinding.js +0 -240
- package/dist/review/reviewFindingLedger.js +0 -550
- package/dist/review/reviewOutcomeClassifier.js +0 -255
- package/dist/scheduler/resourceQueueProjection.js +0 -71
- 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 = "
|
|
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
|
|
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:
|
|
116
|
+
repositoryPath: taskRepository,
|
|
107
117
|
container: join(this.worktreeRoot, project.name),
|
|
108
118
|
taskSegment: taskWorkspaceRefSegment(task),
|
|
109
119
|
integrationId: initial.id,
|
|
110
|
-
baseRef: initial.
|
|
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.
|
|
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,
|
|
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,
|
|
160
|
+
return this.#resumeCheckJob(current, workspace, prepared.path, taskRepository, gate);
|
|
151
161
|
}
|
|
152
162
|
try {
|
|
153
|
-
|
|
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(
|
|
171
|
+
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit);
|
|
197
172
|
if (gate !== undefined) {
|
|
198
|
-
return this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace,
|
|
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,
|
|
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(
|
|
225
|
-
const committed =
|
|
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(
|
|
213
|
+
const target = await resolveRef(taskRepository, current.targetRef);
|
|
240
214
|
if (target === current.candidateCommit) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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:
|
|
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.
|
|
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.
|
|
455
|
-
const committed =
|
|
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.
|
|
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.
|
|
562
|
-
const committed =
|
|
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
|
-
|
|
552
|
+
runtimeGenerationId: integrationRuntimeRuntimeGenerationId(this.home, attempt.id),
|
|
570
553
|
generationId: "integration-checks",
|
|
571
554
|
allowExactActive: true
|
|
572
555
|
});
|
|
573
556
|
}
|
|
574
|
-
async #
|
|
575
|
-
|
|
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
|
-
//
|
|
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:
|
|
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.
|
|
705
|
+
if (target === attempt.beforeCommit) {
|
|
611
706
|
try {
|
|
612
|
-
await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.
|
|
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.
|
|
714
|
+
return this.#fail(attempt, new Error(`Target moved to ${target}; expected ${attempt.beforeCommit}.`), "integration-recovery", workspace);
|
|
620
715
|
}
|
|
621
|
-
|
|
622
|
-
this
|
|
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
|
|
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
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
|
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
|
|
717
|
-
const
|
|
718
|
-
if (
|
|
719
|
-
throw new Error(`Manual
|
|
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
|
|
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
|
|
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
|
-
|
|
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}.`);
|