@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstat, mkdir, readlink, readdir, rmdir, symlink, unlink } from "node:fs/promises";
|
|
1
|
+
import { lstat, mkdir, readlink, readdir, rm, rmdir, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { retireTaskRoleSessionsForWorkspace } from "../executor/agentExecutor.js";
|
|
@@ -7,14 +7,15 @@ import { updateRole } from "../role/role.js";
|
|
|
7
7
|
import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
8
8
|
import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
|
|
9
9
|
import { StorageConflictError } from "../storage/taskStore.js";
|
|
10
|
-
import { activateTask, bindTaskWorkspaceIdentity } from "../task/task.js";
|
|
10
|
+
import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits } from "../task/task.js";
|
|
11
|
+
import { validateDraftTaskForActivation } from "../task/draftPlan.js";
|
|
11
12
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
12
13
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
13
14
|
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
14
15
|
import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
15
16
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
16
17
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
17
|
-
import { generateTaskWorkspaceIdentity,
|
|
18
|
+
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
18
19
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
19
20
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
20
21
|
const MAIN_WORKTREE = "main";
|
|
@@ -120,6 +121,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
120
121
|
changed: false
|
|
121
122
|
};
|
|
122
123
|
}
|
|
124
|
+
validateDraftTaskForActivation(this.store, task);
|
|
123
125
|
const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
124
126
|
try {
|
|
125
127
|
return await this.#prepareTaskWorkspaceLocked(current.id, true);
|
|
@@ -190,6 +192,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
190
192
|
if (!activate && task.status !== "active") {
|
|
191
193
|
throw new Error(`Task is not open for workspace preparation: ${task.id}.`);
|
|
192
194
|
}
|
|
195
|
+
if (activate)
|
|
196
|
+
validateDraftTaskForActivation(this.store, task);
|
|
193
197
|
const existing = this.store.getTaskWorkspace(task.id);
|
|
194
198
|
if (activate && (task.workspaceIdentity !== undefined
|
|
195
199
|
|| task.cwd !== undefined
|
|
@@ -226,6 +230,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
226
230
|
|| latest.projectBindings.length !== 0) {
|
|
227
231
|
throw new Error(`Task changed while preparing its Gitless workspace: ${task.id}.`);
|
|
228
232
|
}
|
|
233
|
+
if (activate)
|
|
234
|
+
validateDraftTaskForActivation(tx, latest);
|
|
229
235
|
const current = tx.getTaskWorkspace(task.id);
|
|
230
236
|
// `createManagedWorkspace` stamps a fresh updatedAt on every call.
|
|
231
237
|
// Gitless preparation is idempotent, so compare only stable identity
|
|
@@ -285,13 +291,12 @@ export class FileTaskWorkspacePreparer {
|
|
|
285
291
|
const taskSegment = taskWorkspaceRefSegment(workspaceIdentity === undefined ? task : { ...task, workspaceIdentity });
|
|
286
292
|
const root = this.#taskWorkspaceRoot(task.id);
|
|
287
293
|
const prepared = [];
|
|
288
|
-
const
|
|
294
|
+
const createdClonePaths = new Set();
|
|
289
295
|
const baselines = new Map();
|
|
290
296
|
try {
|
|
291
|
-
// Resolve every
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// has been gathered successfully.
|
|
297
|
+
// Resolve every remote baseline before creating any Task clone. A later
|
|
298
|
+
// Project failure therefore cannot leave an earlier Project adopted,
|
|
299
|
+
// and activation remains one all-or-nothing boundary.
|
|
295
300
|
for (const binding of task.projectBindings) {
|
|
296
301
|
const project = requireProject(this.store, binding.projectId);
|
|
297
302
|
const previous = existing?.entries.find(({ projectId }) => projectId === project.id);
|
|
@@ -302,37 +307,20 @@ export class FileTaskWorkspacePreparer {
|
|
|
302
307
|
});
|
|
303
308
|
continue;
|
|
304
309
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
&& !looksLikeCommit(binding.baseRef);
|
|
308
|
-
if (useRemoteDefault) {
|
|
309
|
-
const resolver = this.git.resolveRemoteBaseline;
|
|
310
|
-
if (typeof resolver !== "function") {
|
|
311
|
-
throw new Error(`Git workspace cannot resolve the remote baseline for Project: ${project.id}.`);
|
|
312
|
-
}
|
|
313
|
-
const remote = await resolver.call(this.git, {
|
|
314
|
-
repositoryPath: project.path,
|
|
315
|
-
remoteUrl: project.remoteUrl,
|
|
316
|
-
// The binding captured the configured development ref at Task
|
|
317
|
-
// creation; use that snapshot even if the Project catalog was
|
|
318
|
-
// edited before this first workspace preparation.
|
|
319
|
-
developmentRef: binding.baseRef
|
|
320
|
-
});
|
|
321
|
-
baselines.set(project.id, {
|
|
322
|
-
baseRef: remote.commit,
|
|
323
|
-
recordedBaseRef: remote.commit,
|
|
324
|
-
expectedCommit: remote.commit,
|
|
325
|
-
pinTask: true,
|
|
326
|
-
remoteBaseline: remote
|
|
327
|
-
});
|
|
328
|
-
continue;
|
|
310
|
+
if (project.remoteUrl === undefined) {
|
|
311
|
+
throw new Error(`Task activation requires a configured remoteUrl for Project: ${project.id}.`);
|
|
329
312
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
313
|
+
const remote = await this.git.resolveRemoteHead({
|
|
314
|
+
remoteUrl: project.remoteUrl,
|
|
315
|
+
// The binding captures the configured development branch at Draft
|
|
316
|
+
// creation. Project checkout state is intentionally irrelevant.
|
|
317
|
+
branch: binding.baseRef
|
|
318
|
+
});
|
|
333
319
|
baselines.set(project.id, {
|
|
334
|
-
baseRef:
|
|
335
|
-
recordedBaseRef: binding.baseRef
|
|
320
|
+
baseRef: remote.commit,
|
|
321
|
+
recordedBaseRef: binding.baseRef,
|
|
322
|
+
expectedCommit: remote.commit,
|
|
323
|
+
remoteBaseline: remote
|
|
336
324
|
});
|
|
337
325
|
}
|
|
338
326
|
for (const binding of task.projectBindings) {
|
|
@@ -342,15 +330,25 @@ export class FileTaskWorkspacePreparer {
|
|
|
342
330
|
if (baseline === undefined) {
|
|
343
331
|
throw new Error(`Task Project baseline was not resolved: ${project.id}.`);
|
|
344
332
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
333
|
+
const identity = worktreeIdentity(taskSegment, MAIN_WORKTREE);
|
|
334
|
+
const destination = join(this.#projectContainer(project.name), identity.directory);
|
|
335
|
+
const physical = previous === undefined
|
|
336
|
+
? await this.git.clone({
|
|
337
|
+
remoteUrl: project.remoteUrl,
|
|
338
|
+
destination,
|
|
339
|
+
branch: baseline.remoteBaseline.branch,
|
|
340
|
+
localBranch: identity.branch
|
|
341
|
+
})
|
|
342
|
+
: await this.git.inspect(previous.path, "HEAD");
|
|
343
|
+
if (previous === undefined)
|
|
344
|
+
createdClonePaths.add(physical.root);
|
|
345
|
+
const physicalBranch = await this.git.headRef(physical.root);
|
|
346
|
+
if (physical.root !== (previous?.path ?? destination)
|
|
347
|
+
|| physicalBranch !== (previous?.branch ?? identity.branch)) {
|
|
348
|
+
throw new Error(`Task main clone identity changed: ${task.id}/${project.id}.`);
|
|
349
|
+
}
|
|
352
350
|
if (previous !== undefined
|
|
353
|
-
&& !await this.git.isAncestor(
|
|
351
|
+
&& !await this.git.isAncestor(previous.path, previous.baseCommit, physical.baseCommit)) {
|
|
354
352
|
throw new Error(formatWorkspacePreflightError({
|
|
355
353
|
kind: "physical-drift",
|
|
356
354
|
reason: `Task main physical HEAD left its recorded lineage: ${task.id}/${project.id}.`,
|
|
@@ -363,7 +361,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
363
361
|
}
|
|
364
362
|
if (baseline.expectedCommit !== undefined
|
|
365
363
|
&& physical.baseCommit !== baseline.expectedCommit) {
|
|
366
|
-
throw new Error(`Task Project
|
|
364
|
+
throw new Error(`Task Project clone did not start at the fetched remote baseline: ${project.id}.`);
|
|
367
365
|
}
|
|
368
366
|
const provenance = await captureTaskBaseProvenance({
|
|
369
367
|
git: this.git,
|
|
@@ -379,11 +377,12 @@ export class FileTaskWorkspacePreparer {
|
|
|
379
377
|
projectId: project.id,
|
|
380
378
|
directory: binding.directory,
|
|
381
379
|
access: "write",
|
|
382
|
-
path: physical.
|
|
383
|
-
branch:
|
|
380
|
+
path: physical.root,
|
|
381
|
+
branch: previous?.branch ?? identity.branch,
|
|
384
382
|
baseRef: previous?.baseRef ?? baseline.recordedBaseRef,
|
|
385
383
|
baseCommit: previous?.baseCommit ?? physical.baseCommit
|
|
386
384
|
},
|
|
385
|
+
currentCommit: physical.baseCommit,
|
|
387
386
|
provenance
|
|
388
387
|
});
|
|
389
388
|
}
|
|
@@ -402,6 +401,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
402
401
|
if (!isDeepStrictEqual(latest.projectBindings, task.projectBindings)) {
|
|
403
402
|
throw new Error(`Task Projects changed while preparing its workspace: ${task.id}.`);
|
|
404
403
|
}
|
|
404
|
+
if (activate)
|
|
405
|
+
validateDraftTaskForActivation(tx, latest);
|
|
405
406
|
// Revalidate the Project catalog path after acquiring the fence: a
|
|
406
407
|
// concurrent migration must not leave a persisted worktree whose Git
|
|
407
408
|
// common dir is the old external checkout. A changed path rides the
|
|
@@ -432,16 +433,20 @@ export class FileTaskWorkspacePreparer {
|
|
|
432
433
|
: existing === null || !sameManagedWorkspace(current, existing)) {
|
|
433
434
|
throw new StorageConflictError(`Task workspace changed while preparing its workspace: ${task.id}.`);
|
|
434
435
|
}
|
|
435
|
-
const pinnedBindings = latest.projectBindings.map((binding) => {
|
|
436
|
-
const baseline = baselines.get(binding.projectId);
|
|
437
|
-
return baseline?.pinTask === true
|
|
438
|
-
? { ...binding, baseRef: baseline.baseRef }
|
|
439
|
-
: binding;
|
|
440
|
-
});
|
|
441
436
|
const timestamp = this.now();
|
|
442
|
-
let persistedTask =
|
|
443
|
-
|
|
444
|
-
|
|
437
|
+
let persistedTask = latest;
|
|
438
|
+
if (activate) {
|
|
439
|
+
persistedTask = bindTaskProjectCommits(persistedTask, prepared.map(({ entry }) => ({
|
|
440
|
+
projectId: entry.projectId,
|
|
441
|
+
commit: entry.baseCommit
|
|
442
|
+
})), timestamp);
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
persistedTask = synchronizeTaskProjectCommits(persistedTask, prepared.map(({ entry, currentCommit }) => ({
|
|
446
|
+
projectId: entry.projectId,
|
|
447
|
+
commit: currentCommit
|
|
448
|
+
})), timestamp);
|
|
449
|
+
}
|
|
445
450
|
if (workspaceIdentity !== undefined) {
|
|
446
451
|
// The identity is persisted only now that every managed ref was
|
|
447
452
|
// created successfully. A concurrent prepare that bound the same
|
|
@@ -457,8 +462,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
457
462
|
tx.saveTask(persistedTask);
|
|
458
463
|
}
|
|
459
464
|
tx.saveManagedWorkspace(preserveWorkspaceCreatedAt(workspace, current));
|
|
460
|
-
|
|
461
|
-
|
|
465
|
+
const adoptedProvenance = prepared
|
|
466
|
+
.filter(({ entry }) => (current?.entries.every(({ projectId }) => projectId !== entry.projectId) ?? true))
|
|
467
|
+
.map(({ provenance }) => provenance);
|
|
468
|
+
if (adoptedProvenance.length > 0) {
|
|
469
|
+
recordTaskBaseProvenanceEvents(tx, task.id, adoptedProvenance, timestamp);
|
|
462
470
|
}
|
|
463
471
|
for (const role of tx.listRoles(task.id)) {
|
|
464
472
|
// The Role field is only a cwd/snapshot hint. Preserve it while a
|
|
@@ -517,6 +525,15 @@ export class FileTaskWorkspacePreparer {
|
|
|
517
525
|
// branches too, so a retry mints a clean identity without half-created
|
|
518
526
|
// refs behind. Adopted (already catalogued) worktrees are never touched.
|
|
519
527
|
await this.#discardUnadoptedEntries(task, taskSegment, prepared, MAIN_WORKTREE, true);
|
|
528
|
+
const recorded = new Set(prepared.map(({ entry }) => entry.path));
|
|
529
|
+
for (const path of createdClonePaths) {
|
|
530
|
+
if (recorded.has(path))
|
|
531
|
+
continue;
|
|
532
|
+
const removal = await this.git.removeStrandedWorktree(path);
|
|
533
|
+
if (removal === "dirty") {
|
|
534
|
+
throw new Error(`Unadopted Task clone is dirty and was retained at ${path}; inspect it and retry.`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
520
537
|
if (activate)
|
|
521
538
|
await removeWorkspaceView(root);
|
|
522
539
|
throw error;
|
|
@@ -558,25 +575,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
558
575
|
*/
|
|
559
576
|
async #mintTaskWorkspaceIdentity(task) {
|
|
560
577
|
const home = this.store.getHomeIdentity();
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
});
|
|
567
|
-
const mainBranch = taskMainBranch(taskWorkspaceRefSegmentFromIdentity(identity));
|
|
568
|
-
let conflict = false;
|
|
569
|
-
for (const binding of task.projectBindings) {
|
|
570
|
-
const project = requireProject(this.store, binding.projectId);
|
|
571
|
-
if (await this.git.refExists(project.path, mainBranch)) {
|
|
572
|
-
conflict = true;
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (!conflict)
|
|
577
|
-
return identity;
|
|
578
|
-
}
|
|
579
|
-
throw new Error(`Could not mint a unique Task workspace identity for ${task.id}.`);
|
|
578
|
+
return generateTaskWorkspaceIdentity({
|
|
579
|
+
home,
|
|
580
|
+
taskId: task.id,
|
|
581
|
+
now: this.now()
|
|
582
|
+
});
|
|
580
583
|
}
|
|
581
584
|
async snapshotCandidateWorkspace(workspace) {
|
|
582
585
|
if (workspace.owner.type === "review-round") {
|
|
@@ -635,34 +638,52 @@ export class FileTaskWorkspacePreparer {
|
|
|
635
638
|
}
|
|
636
639
|
return { schemaVersion: 1, projects };
|
|
637
640
|
}
|
|
641
|
+
/** Freeze one clean committed source head set before any replicated Lane is minted. */
|
|
642
|
+
async snapshotExecutionLaneInputHeads(workspace, projectIds) {
|
|
643
|
+
if (workspace.owner.type !== "task" && workspace.owner.type !== "work-item") {
|
|
644
|
+
throw new Error("Execution Lane input must come from Task main or a WorkItem workspace.");
|
|
645
|
+
}
|
|
646
|
+
const stored = this.store.getManagedWorkspace(workspace.owner);
|
|
647
|
+
if (stored === null || !isDeepStrictEqual(stored, workspace)) {
|
|
648
|
+
throw new Error("Execution Lane input workspace is not the durable owner.");
|
|
649
|
+
}
|
|
650
|
+
return this.#snapshotWorkspaceHeads(workspace, projectIds, "Execution Lane input");
|
|
651
|
+
}
|
|
638
652
|
async snapshotDirectTaskMain(workspace, projectIds) {
|
|
639
653
|
if (workspace.owner.type !== "task") {
|
|
640
654
|
throw new Error("Only Task main can become a direct Candidate source.");
|
|
641
655
|
}
|
|
656
|
+
const projects = await this.#snapshotWorkspaceHeads(workspace, projectIds, "Direct Candidate");
|
|
657
|
+
return createDirectTaskMainSnapshot(workspace, projectIds, projects);
|
|
658
|
+
}
|
|
659
|
+
async #snapshotWorkspaceHeads(workspace, projectIds, boundary) {
|
|
642
660
|
const selected = new Set(projectIds);
|
|
661
|
+
if (selected.size !== projectIds.length) {
|
|
662
|
+
throw new Error(`${boundary} Project scope is duplicated.`);
|
|
663
|
+
}
|
|
643
664
|
const entries = workspace.entries.filter(({ projectId }) => selected.has(projectId));
|
|
644
665
|
if (entries.length !== selected.size) {
|
|
645
|
-
throw new Error(
|
|
666
|
+
throw new Error(`${boundary} Project scope does not match its managed workspace.`);
|
|
646
667
|
}
|
|
647
668
|
const projects = [];
|
|
648
669
|
for (const entry of entries) {
|
|
649
670
|
if (entry.access !== "write") {
|
|
650
|
-
throw new Error(
|
|
671
|
+
throw new Error(`${boundary} Project is not writable: ${entry.projectId}.`);
|
|
651
672
|
}
|
|
652
673
|
if (!await this.git.isClean(entry.path)) {
|
|
653
|
-
throw new Error(
|
|
674
|
+
throw new Error(`${boundary} workspace must be clean and committed: ${entry.projectId}.`);
|
|
654
675
|
}
|
|
655
676
|
const branch = await this.git.headRef(entry.path);
|
|
656
677
|
if (branch !== entry.branch) {
|
|
657
|
-
throw new Error(
|
|
678
|
+
throw new Error(`${boundary} workspace left its managed branch: ${entry.projectId}/${branch}.`);
|
|
658
679
|
}
|
|
659
680
|
const headCommit = (await this.git.inspect(entry.path, "HEAD")).baseCommit;
|
|
660
681
|
if (!await this.git.isAncestor(entry.path, entry.baseCommit, headCommit)) {
|
|
661
|
-
throw new Error(
|
|
682
|
+
throw new Error(`${boundary} workspace does not descend from its recorded base: ${entry.projectId}.`);
|
|
662
683
|
}
|
|
663
684
|
projects.push({ projectId: entry.projectId, headCommit });
|
|
664
685
|
}
|
|
665
|
-
return
|
|
686
|
+
return projects;
|
|
666
687
|
}
|
|
667
688
|
async prepareWorkItemWorkspace(taskId, workItemId) {
|
|
668
689
|
const item = requireWorkItem(this.store, taskId, workItemId);
|
|
@@ -722,9 +743,9 @@ export class FileTaskWorkspacePreparer {
|
|
|
722
743
|
: requestedBaseRef ?? head.baseCommit;
|
|
723
744
|
const requestedBase = previous?.access === "write" || requestedBaseRef === undefined
|
|
724
745
|
? null
|
|
725
|
-
: await this.git.inspect(
|
|
746
|
+
: await this.git.inspect(mainEntry.path, requestedBaseRef);
|
|
726
747
|
const physical = await this.git.ensureWorktree({
|
|
727
|
-
repositoryPath:
|
|
748
|
+
repositoryPath: mainEntry.path,
|
|
728
749
|
container: this.#projectContainer(project.name),
|
|
729
750
|
taskSegment,
|
|
730
751
|
roleName: item.id,
|
|
@@ -761,7 +782,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
761
782
|
if (requestedBaseRef !== undefined && requestedBaseRef !== previous.baseRef) {
|
|
762
783
|
throw new Error(`Existing WorkItem Project base ref changed: ${item.id}/${project.id}.`);
|
|
763
784
|
}
|
|
764
|
-
if (!await this.git.isAncestor(
|
|
785
|
+
if (!await this.git.isAncestor(mainEntry.path, previous.baseCommit, physical.baseCommit)) {
|
|
765
786
|
throw new Error(`Existing WorkItem Project HEAD does not descend from its frozen base: `
|
|
766
787
|
+ `${item.id}/${project.id}.`);
|
|
767
788
|
}
|
|
@@ -842,8 +863,13 @@ export class FileTaskWorkspacePreparer {
|
|
|
842
863
|
: { release: () => { }, current: heldFence.current };
|
|
843
864
|
try {
|
|
844
865
|
const lineage = executionLaneLineage(this.store, lockedTask, executionGroupId, executionLaneId, hint);
|
|
866
|
+
const item = lineage.purpose === "execution"
|
|
867
|
+
? this.store.getWorkItem(taskId, lineage.workItemId)
|
|
868
|
+
: null;
|
|
845
869
|
const source = lineage.purpose === "execution"
|
|
846
|
-
?
|
|
870
|
+
? item?.assignee === LEADER_ROLE
|
|
871
|
+
? this.store.getTaskWorkspace(taskId)
|
|
872
|
+
: this.store.getWorkItemWorkspace(taskId, lineage.workItemId)
|
|
847
873
|
: this.store.getReviewRoundWorkspace(taskId, lineage.reviewRoundId);
|
|
848
874
|
if (source === null) {
|
|
849
875
|
throw new Error(`Execution Lane source workspace is not ready: ${executionLaneId}.`);
|
|
@@ -866,6 +892,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
866
892
|
purpose: "review",
|
|
867
893
|
reviewRoundId: lineage.reviewRoundId
|
|
868
894
|
};
|
|
895
|
+
const writable = new Set(lineage.purpose === "execution"
|
|
896
|
+
? item?.writeProjectIds ?? []
|
|
897
|
+
: lockedTask.projectBindings.map(({ projectId }) => projectId));
|
|
898
|
+
const inputHeadByProject = new Map((hint?.inputHeads ?? []).map(({ projectId, headCommit }) => [projectId, headCommit]));
|
|
899
|
+
if (hint?.inputHeads !== undefined
|
|
900
|
+
&& (inputHeadByProject.size !== hint.inputHeads.length
|
|
901
|
+
|| inputHeadByProject.size !== writable.size
|
|
902
|
+
|| [...writable].some((projectId) => !inputHeadByProject.has(projectId)))) {
|
|
903
|
+
throw new Error(`Execution Lane input heads do not match writable scope: ${executionLaneId}.`);
|
|
904
|
+
}
|
|
869
905
|
const existing = this.store.getManagedWorkspace(owner);
|
|
870
906
|
if (existing !== null) {
|
|
871
907
|
if (existing.owner.type !== "execution-lane" || existing.root !== this.#executionLaneWorkspaceRoot(taskId, executionGroupId, executionLaneId)) {
|
|
@@ -874,9 +910,13 @@ export class FileTaskWorkspacePreparer {
|
|
|
874
910
|
await ensureWorkspaceView(existing.root, existing.entries);
|
|
875
911
|
this.#registerWorkspace(existing);
|
|
876
912
|
for (const entry of existing.entries.filter(({ access }) => access === "write")) {
|
|
913
|
+
const inputHead = inputHeadByProject.get(entry.projectId);
|
|
914
|
+
if (hint?.inputHeads !== undefined && inputHead !== entry.baseCommit) {
|
|
915
|
+
throw new Error(`Execution Lane input head changed: ${executionLaneId}/${entry.projectId}.`);
|
|
916
|
+
}
|
|
877
917
|
const project = requireProject(this.store, entry.projectId);
|
|
878
918
|
const physical = await this.git.ensureWorktree({
|
|
879
|
-
repositoryPath: project.
|
|
919
|
+
repositoryPath: this.#taskRepositoryPath(taskId, project.id),
|
|
880
920
|
container: this.#projectContainer(project.name),
|
|
881
921
|
taskSegment,
|
|
882
922
|
roleName: managedWorktreeName(owner),
|
|
@@ -888,12 +928,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
888
928
|
}
|
|
889
929
|
return existing;
|
|
890
930
|
}
|
|
891
|
-
const item = lineage.purpose === "execution"
|
|
892
|
-
? this.store.getWorkItem(taskId, lineage.workItemId)
|
|
893
|
-
: null;
|
|
894
|
-
const writable = new Set(lineage.purpose === "execution"
|
|
895
|
-
? item?.writeProjectIds ?? []
|
|
896
|
-
: lockedTask.projectBindings.map(({ projectId }) => projectId));
|
|
897
931
|
const prepared = [];
|
|
898
932
|
try {
|
|
899
933
|
for (const binding of lockedTask.projectBindings) {
|
|
@@ -903,13 +937,17 @@ export class FileTaskWorkspacePreparer {
|
|
|
903
937
|
prepared.push({ project, entry: { ...sourceEntry, access: "read" } });
|
|
904
938
|
continue;
|
|
905
939
|
}
|
|
940
|
+
const inputHead = inputHeadByProject.get(project.id) ?? sourceEntry.baseCommit;
|
|
906
941
|
const physical = await this.git.ensureWorktree({
|
|
907
|
-
repositoryPath: project.
|
|
942
|
+
repositoryPath: this.#taskRepositoryPath(taskId, project.id),
|
|
908
943
|
container: this.#projectContainer(project.name),
|
|
909
944
|
taskSegment,
|
|
910
945
|
roleName: managedWorktreeName(owner),
|
|
911
|
-
baseRef:
|
|
946
|
+
baseRef: inputHead
|
|
912
947
|
});
|
|
948
|
+
if (physical.baseCommit !== inputHead) {
|
|
949
|
+
throw new Error(`Execution Lane physical input head changed: ${executionLaneId}/${project.id}.`);
|
|
950
|
+
}
|
|
913
951
|
prepared.push({
|
|
914
952
|
project,
|
|
915
953
|
entry: {
|
|
@@ -917,8 +955,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
917
955
|
access: "write",
|
|
918
956
|
path: physical.path,
|
|
919
957
|
branch: physical.branch,
|
|
920
|
-
baseRef:
|
|
921
|
-
baseCommit:
|
|
958
|
+
baseRef: inputHead,
|
|
959
|
+
baseCommit: inputHead
|
|
922
960
|
}
|
|
923
961
|
});
|
|
924
962
|
}
|
|
@@ -969,168 +1007,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
969
1007
|
}
|
|
970
1008
|
}
|
|
971
1009
|
}
|
|
972
|
-
/** Merge every selected lane's committed output into the WorkItem owner. */
|
|
973
|
-
async materializeExecutionGroupCandidate(taskId, workItemId, executionGroupId, selectedLaneIds) {
|
|
974
|
-
const item = requireWorkItem(this.store, taskId, workItemId);
|
|
975
|
-
const task = requireTask(this.store, taskId);
|
|
976
|
-
const group = workItemExecutionGroupById(item, executionGroupId);
|
|
977
|
-
if (group === undefined || group.id !== executionGroupId) {
|
|
978
|
-
throw new Error(`ExecutionGroup is not attached to Work Item: ${executionGroupId}.`);
|
|
979
|
-
}
|
|
980
|
-
const workspace = this.store.getWorkItemWorkspace(taskId, workItemId);
|
|
981
|
-
const selected = (selectedLaneIds.length === 0
|
|
982
|
-
? group.lanes.filter((lane) => lane.status === "completed")
|
|
983
|
-
: group.lanes.filter((lane) => selectedLaneIds.includes(lane.id)));
|
|
984
|
-
if (selected.some((lane) => lane.status !== "completed")) {
|
|
985
|
-
throw new Error(`ExecutionGroup has a selected Lane without terminal output: ${executionGroupId}.`);
|
|
986
|
-
}
|
|
987
|
-
if (selected.length === 0)
|
|
988
|
-
throw new Error(`ExecutionGroup has no selected Lane outputs: ${executionGroupId}.`);
|
|
989
|
-
// A Gitless Task has no WorkItem workspace to materialize. Its selected
|
|
990
|
-
// Lane reports still form a valid non-Git Candidate; no fake workspace or
|
|
991
|
-
// commit snapshot is introduced.
|
|
992
|
-
if (workspace === null) {
|
|
993
|
-
if (task.projectBindings.length === 0
|
|
994
|
-
&& selected.every((lane) => lane.result?.gitSnapshot === undefined)) {
|
|
995
|
-
return { restoreTargets: [] };
|
|
996
|
-
}
|
|
997
|
-
throw new Error(`Work Item workspace is not ready for Candidate materialization: ${workItemId}.`);
|
|
998
|
-
}
|
|
999
|
-
if (workspace.owner.type !== "work-item") {
|
|
1000
|
-
throw new Error(`Work Item workspace owner is invalid for Candidate materialization: ${workItemId}.`);
|
|
1001
|
-
}
|
|
1002
|
-
const pendingMerges = new Map();
|
|
1003
|
-
const restoreTargets = [];
|
|
1004
|
-
const writableProjectIds = workspace.entries
|
|
1005
|
-
.filter(({ access }) => access === "write")
|
|
1006
|
-
.map(({ projectId }) => projectId);
|
|
1007
|
-
if (writableProjectIds.length === 0) {
|
|
1008
|
-
for (const lane of selected) {
|
|
1009
|
-
if (lane.result?.gitSnapshot !== undefined) {
|
|
1010
|
-
throw new Error(`Read-only Lane must not carry a Git snapshot: ${lane.id}.`);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
return {
|
|
1014
|
-
workspace,
|
|
1015
|
-
// A read-only WorkItem has no Lane Git output, but its frozen
|
|
1016
|
-
// WorkItem workspace is still the legitimate Candidate review
|
|
1017
|
-
// context. Keep that Candidate snapshot separate from Lane output.
|
|
1018
|
-
snapshot: await this.snapshotCandidateWorkspace(workspace),
|
|
1019
|
-
restoreTargets: []
|
|
1020
|
-
};
|
|
1021
|
-
}
|
|
1022
|
-
for (const lane of selected) {
|
|
1023
|
-
const laneSnapshot = lane.result?.gitSnapshot;
|
|
1024
|
-
if (laneSnapshot === undefined) {
|
|
1025
|
-
throw new Error(`Selected Lane has no frozen Git snapshot: ${lane.id}.`);
|
|
1026
|
-
}
|
|
1027
|
-
const snapshotProjectIds = laneSnapshot.projects.map(({ projectId }) => projectId).sort();
|
|
1028
|
-
if (!isDeepStrictEqual(snapshotProjectIds, [...writableProjectIds].sort())) {
|
|
1029
|
-
throw new Error(`Selected Lane Git snapshot Project scope changed: ${lane.id}.`);
|
|
1030
|
-
}
|
|
1031
|
-
const laneWorkspace = this.store.getManagedWorkspace({
|
|
1032
|
-
type: "execution-lane",
|
|
1033
|
-
taskId,
|
|
1034
|
-
executionGroupId,
|
|
1035
|
-
executionLaneId: lane.id,
|
|
1036
|
-
purpose: "execution",
|
|
1037
|
-
workItemId
|
|
1038
|
-
});
|
|
1039
|
-
if (laneWorkspace === null) {
|
|
1040
|
-
// fixed(1) retains the WorkItem owner as its single normal-path workspace.
|
|
1041
|
-
if (group.lanes.length === 1) {
|
|
1042
|
-
for (const target of workspace.entries.filter(({ access }) => access === "write")) {
|
|
1043
|
-
const frozen = laneSnapshot.projects.find(({ projectId }) => projectId === target.projectId);
|
|
1044
|
-
if (frozen === undefined)
|
|
1045
|
-
throw new Error(`Selected Lane snapshot omits Project: ${lane.id}/${target.projectId}.`);
|
|
1046
|
-
if (await this.git.headRef(target.path) !== frozen.branch || !await this.git.isClean(target.path)) {
|
|
1047
|
-
throw new Error(`Single-Lane workspace is not clean on its managed branch: ${lane.id}/${target.projectId}.`);
|
|
1048
|
-
}
|
|
1049
|
-
const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
|
|
1050
|
-
if (targetHead !== frozen.headCommit) {
|
|
1051
|
-
throw new Error(`Single-Lane workspace changed before resolution: ${lane.id}/${target.projectId}.`);
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
throw new Error(`Selected Execution Lane workspace is not adopted: ${lane.id}.`);
|
|
1057
|
-
}
|
|
1058
|
-
for (const target of workspace.entries.filter(({ access }) => access === "write")) {
|
|
1059
|
-
const source = laneWorkspace.entries.find(({ projectId }) => projectId === target.projectId);
|
|
1060
|
-
if (source === undefined)
|
|
1061
|
-
throw new Error(`Selected Lane omits Project: ${lane.id}/${target.projectId}.`);
|
|
1062
|
-
const frozen = laneSnapshot.projects.find(({ projectId }) => projectId === target.projectId);
|
|
1063
|
-
if (frozen === undefined)
|
|
1064
|
-
throw new Error(`Selected Lane snapshot omits Project: ${lane.id}/${target.projectId}.`);
|
|
1065
|
-
if (await this.git.headRef(source.path) !== frozen.branch || frozen.branch !== source.branch) {
|
|
1066
|
-
throw new Error(`Selected Lane left its managed branch: ${lane.id}/${target.projectId}.`);
|
|
1067
|
-
}
|
|
1068
|
-
if (!await this.git.isClean(source.path)) {
|
|
1069
|
-
throw new Error(`Selected Lane has uncommitted output: ${lane.id}/${target.projectId}.`);
|
|
1070
|
-
}
|
|
1071
|
-
const sourceHead = (await this.git.inspect(source.path, "HEAD")).baseCommit;
|
|
1072
|
-
if (sourceHead !== frozen.headCommit) {
|
|
1073
|
-
throw new Error(`Selected Lane advanced before resolution: ${lane.id}/${target.projectId}.`);
|
|
1074
|
-
}
|
|
1075
|
-
const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
|
|
1076
|
-
if (sourceHead !== targetHead && !await this.git.isAncestor(target.path, sourceHead, targetHead)) {
|
|
1077
|
-
const pending = pendingMerges.get(target.path) ?? [];
|
|
1078
|
-
pending.push(frozen.headCommit);
|
|
1079
|
-
pendingMerges.set(target.path, pending);
|
|
1080
|
-
if (!restoreTargets.some(({ path }) => path === target.path)) {
|
|
1081
|
-
restoreTargets.push({ path: target.path, head: targetHead });
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
try {
|
|
1087
|
-
for (const [targetPath, sourceRefs] of pendingMerges) {
|
|
1088
|
-
await this.git.mergeWorktree({ targetPath, sourceRefs });
|
|
1089
|
-
const target = restoreTargets.find(({ path }) => path === targetPath);
|
|
1090
|
-
if (target !== undefined) {
|
|
1091
|
-
target.mergedHead = (await this.git.inspect(targetPath, "HEAD")).baseCommit;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
return {
|
|
1095
|
-
workspace,
|
|
1096
|
-
snapshot: await this.snapshotCandidateWorkspace(workspace),
|
|
1097
|
-
restoreTargets: restoreTargets
|
|
1098
|
-
.filter((target) => (target.mergedHead !== undefined && target.mergedHead !== target.head))
|
|
1099
|
-
};
|
|
1100
|
-
}
|
|
1101
|
-
catch (error) {
|
|
1102
|
-
for (const target of [...restoreTargets].reverse()) {
|
|
1103
|
-
if (target.mergedHead === undefined)
|
|
1104
|
-
continue;
|
|
1105
|
-
try {
|
|
1106
|
-
await this.git.resetWorktree({
|
|
1107
|
-
targetPath: target.path,
|
|
1108
|
-
expectedHead: target.mergedHead,
|
|
1109
|
-
restoreHead: target.head
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
catch (restoreError) {
|
|
1113
|
-
throw new Error(`Candidate materialization failed and Git compensation could not restore ${target.path}.`, { cause: restoreError });
|
|
1114
|
-
}
|
|
1115
|
-
}
|
|
1116
|
-
throw error;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
async restoreExecutionGroupCandidateMaterialization(materialization) {
|
|
1120
|
-
for (const target of [...materialization.restoreTargets].reverse()) {
|
|
1121
|
-
const current = (await this.git.inspect(target.path, "HEAD")).baseCommit;
|
|
1122
|
-
if (current === target.head)
|
|
1123
|
-
continue;
|
|
1124
|
-
if (current !== target.mergedHead) {
|
|
1125
|
-
throw new Error(`Candidate materialization compensation found unexpected Git head: ${target.path}.`);
|
|
1126
|
-
}
|
|
1127
|
-
await this.git.resetWorktree({
|
|
1128
|
-
targetPath: target.path,
|
|
1129
|
-
expectedHead: target.mergedHead,
|
|
1130
|
-
restoreHead: target.head
|
|
1131
|
-
});
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
1010
|
async cleanupExecutionLaneWorkspace(taskId, executionGroupId, executionLaneId) {
|
|
1135
1011
|
const task = requireTask(this.store, taskId);
|
|
1136
1012
|
const lineage = executionLaneLineage(this.store, task, executionGroupId, executionLaneId);
|
|
@@ -1141,8 +1017,9 @@ export class FileTaskWorkspacePreparer {
|
|
|
1141
1017
|
? (item === null ? undefined : workItemExecutionGroupById(item, executionGroupId))
|
|
1142
1018
|
: this.store.getReviewRound(taskId, lineage.reviewRoundId)?.executionGroup;
|
|
1143
1019
|
const lane = group?.lanes.find(({ id }) => id === executionLaneId);
|
|
1020
|
+
const terminal = lane !== undefined && lane.disposition !== "open";
|
|
1144
1021
|
if (group === undefined
|
|
1145
|
-
|| lane === undefined || !
|
|
1022
|
+
|| lane === undefined || !terminal) {
|
|
1146
1023
|
throw new Error(`Execution Lane is not terminally resolved: ${taskId}/${executionLaneId}.`);
|
|
1147
1024
|
}
|
|
1148
1025
|
const workspace = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane"
|
|
@@ -1150,14 +1027,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
1150
1027
|
&& owner.executionLaneId === executionLaneId));
|
|
1151
1028
|
if (workspace === undefined)
|
|
1152
1029
|
return "missing";
|
|
1153
|
-
const state = await this.#inspectEntries(this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1030
|
+
const state = await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1154
1031
|
if (state === "dirty")
|
|
1155
1032
|
return "dirty";
|
|
1156
1033
|
let removed = false;
|
|
1157
1034
|
for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
|
|
1158
1035
|
const project = requireProject(this.store, entry.projectId);
|
|
1159
1036
|
const result = await this.git.removeWorktree({
|
|
1160
|
-
repositoryPath: project.
|
|
1037
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1161
1038
|
container: this.#projectContainer(project.name),
|
|
1162
1039
|
taskSegment: this.#taskSegment(task),
|
|
1163
1040
|
roleName: managedWorktreeName(workspace.owner),
|
|
@@ -1184,7 +1061,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1184
1061
|
const writable = workspace.entries.filter(({ access }) => access === "write");
|
|
1185
1062
|
let state;
|
|
1186
1063
|
try {
|
|
1187
|
-
state = await this.#inspectEntries(taskSegment, managedWorktreeName(workspace.owner), writable);
|
|
1064
|
+
state = await this.#inspectEntries(task.id, taskSegment, managedWorktreeName(workspace.owner), writable);
|
|
1188
1065
|
}
|
|
1189
1066
|
catch (error) {
|
|
1190
1067
|
// A `project migrate` between preparation and compensation switches the
|
|
@@ -1210,7 +1087,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1210
1087
|
for (const entry of writable) {
|
|
1211
1088
|
const project = requireProject(this.store, entry.projectId);
|
|
1212
1089
|
const result = await this.git.removeWorktree({
|
|
1213
|
-
repositoryPath: project.
|
|
1090
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1214
1091
|
container: this.#projectContainer(project.name),
|
|
1215
1092
|
taskSegment,
|
|
1216
1093
|
roleName: managedWorktreeName(workspace.owner),
|
|
@@ -1226,10 +1103,13 @@ export class FileTaskWorkspacePreparer {
|
|
|
1226
1103
|
async cleanupExecutionLaneWorkspacesForWorkItem(taskId, workItemId) {
|
|
1227
1104
|
let result = "missing";
|
|
1228
1105
|
const item = this.store.getWorkItem(taskId, workItemId);
|
|
1229
|
-
|
|
1106
|
+
const groups = item?.executionGroups ?? [];
|
|
1107
|
+
for (const group of groups)
|
|
1230
1108
|
for (const lane of group.lanes) {
|
|
1231
1109
|
const owner = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane" && owner.purpose === "execution"
|
|
1232
|
-
&& owner.workItemId === workItemId
|
|
1110
|
+
&& owner.workItemId === workItemId
|
|
1111
|
+
&& owner.executionGroupId === group.id
|
|
1112
|
+
&& owner.executionLaneId === lane.id));
|
|
1233
1113
|
if (owner !== undefined) {
|
|
1234
1114
|
if (owner.owner.type !== "execution-lane")
|
|
1235
1115
|
continue;
|
|
@@ -1245,7 +1125,13 @@ export class FileTaskWorkspacePreparer {
|
|
|
1245
1125
|
async cleanupExecutionLaneWorkspacesForReviewRound(taskId, reviewRoundId) {
|
|
1246
1126
|
let result = "missing";
|
|
1247
1127
|
const round = this.store.getReviewRound(taskId, reviewRoundId);
|
|
1248
|
-
|
|
1128
|
+
const lanes = round === null
|
|
1129
|
+
? []
|
|
1130
|
+
: [
|
|
1131
|
+
...(round.executionGroup?.lanes ?? []),
|
|
1132
|
+
...(round.executionGroup?.lanes ?? [])
|
|
1133
|
+
];
|
|
1134
|
+
for (const lane of lanes) {
|
|
1249
1135
|
const owner = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane" && owner.purpose === "review"
|
|
1250
1136
|
&& owner.reviewRoundId === reviewRoundId && owner.executionLaneId === lane.id));
|
|
1251
1137
|
if (owner !== undefined) {
|
|
@@ -1266,7 +1152,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1266
1152
|
if (workspace === null)
|
|
1267
1153
|
return "missing";
|
|
1268
1154
|
assertWorkItemOwnsWorkspace(item, workspace);
|
|
1269
|
-
return this.#inspectEntries(item.taskId, managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1155
|
+
return this.#inspectEntries(item.taskId, this.#taskSegment(requireTask(this.store, item.taskId)), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1270
1156
|
}
|
|
1271
1157
|
/** Prepare a fresh, reviewer-writable workspace from the candidate's
|
|
1272
1158
|
* frozen Develop snapshot. This record is owned by the ReviewRound; it is
|
|
@@ -1398,7 +1284,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1398
1284
|
if (physical.root !== entry.path) {
|
|
1399
1285
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace managed path mismatch for ${round.id}/${entry.projectId}.`);
|
|
1400
1286
|
}
|
|
1401
|
-
const
|
|
1287
|
+
const taskRepository = this.#taskRepositoryPath(task.id, project.id);
|
|
1288
|
+
const projectRoot = await this.git.inspect(taskRepository, "HEAD");
|
|
1402
1289
|
if (physical.gitDirectory !== projectRoot.gitDirectory) {
|
|
1403
1290
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace managed Project mismatch for ${round.id}/${entry.projectId}.`);
|
|
1404
1291
|
}
|
|
@@ -1417,7 +1304,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1417
1304
|
}
|
|
1418
1305
|
let descendsFromBase;
|
|
1419
1306
|
try {
|
|
1420
|
-
descendsFromBase = await this.git.isAncestor(
|
|
1307
|
+
descendsFromBase = await this.git.isAncestor(taskRepository, entry.baseCommit, physical.baseCommit);
|
|
1421
1308
|
}
|
|
1422
1309
|
catch (error) {
|
|
1423
1310
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace ancestry cannot be verified for `
|
|
@@ -1458,7 +1345,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1458
1345
|
for (const source of sources) {
|
|
1459
1346
|
const project = requireProject(this.store, source.projectId);
|
|
1460
1347
|
const physical = await this.git.ensureWorktree({
|
|
1461
|
-
repositoryPath: project.
|
|
1348
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1462
1349
|
container: this.#projectContainer(project.name),
|
|
1463
1350
|
taskSegment,
|
|
1464
1351
|
roleName: this.#reviewWorktreeName(round),
|
|
@@ -1480,7 +1367,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1480
1367
|
}
|
|
1481
1368
|
let descendsFromBase;
|
|
1482
1369
|
try {
|
|
1483
|
-
descendsFromBase = await this.git.isAncestor(project.
|
|
1370
|
+
descendsFromBase = await this.git.isAncestor(this.#taskRepositoryPath(task.id, project.id), source.baseCommit, physical.baseCommit);
|
|
1484
1371
|
}
|
|
1485
1372
|
catch (error) {
|
|
1486
1373
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace ancestry cannot be verified for `
|
|
@@ -1716,75 +1603,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1716
1603
|
if (round.workspace !== undefined && !isDeepStrictEqual(round.workspace, workspace)) {
|
|
1717
1604
|
throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
|
|
1718
1605
|
}
|
|
1719
|
-
return this.#inspectEntries(this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries);
|
|
1720
|
-
}
|
|
1721
|
-
async snapshotReviewRoundResult(taskId, reviewRoundId) {
|
|
1722
|
-
const round = this.store.getReviewRound(taskId, reviewRoundId);
|
|
1723
|
-
if (round === null)
|
|
1724
|
-
throw new Error(`ReviewRound not found: ${taskId}/${reviewRoundId}.`);
|
|
1725
|
-
const workspace = this.store.getReviewRoundWorkspace(taskId, reviewRoundId);
|
|
1726
|
-
if (round.status !== "running" || workspace === null) {
|
|
1727
|
-
throw new Error(`ReviewRound is not running in a managed workspace: ${reviewRoundId}.`);
|
|
1728
|
-
}
|
|
1729
|
-
assertReviewRoundOwnsWorkspace(round.id, workspace);
|
|
1730
|
-
if (round.workspace === undefined || !isDeepStrictEqual(round.workspace, workspace)) {
|
|
1731
|
-
throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
|
|
1732
|
-
}
|
|
1733
|
-
return this.#snapshotReviewWorkspaceEntries(round.id, workspace, round.reviewBaseCommit);
|
|
1734
|
-
}
|
|
1735
|
-
/**
|
|
1736
|
-
* Snapshot the exact workspace recorded on one Reviewer Turn. Panel Lanes
|
|
1737
|
-
* own separate durable workspaces; the ReviewRound's shared workspace is
|
|
1738
|
-
* not a valid substitute for a Lane's branch/head evidence.
|
|
1739
|
-
*/
|
|
1740
|
-
async snapshotReviewRunResult(taskId, run) {
|
|
1741
|
-
if (run.taskId !== taskId || run.purpose !== "review" || run.reviewRoundId === undefined) {
|
|
1742
|
-
throw new Error(`Turn is not an exact ReviewRound Turn: ${run.id}.`);
|
|
1743
|
-
}
|
|
1744
|
-
if (run.workspace === undefined) {
|
|
1745
|
-
throw new Error(`Review Turn has no managed workspace: ${run.id}.`);
|
|
1746
|
-
}
|
|
1747
|
-
const round = this.store.getReviewRound(taskId, run.reviewRoundId);
|
|
1748
|
-
if (round === null || round.status !== "running") {
|
|
1749
|
-
throw new Error(`ReviewRound is not running for Review Turn: ${run.id}.`);
|
|
1750
|
-
}
|
|
1751
|
-
const stored = this.store.getManagedWorkspace(run.workspace.owner);
|
|
1752
|
-
if (stored === null || !isDeepStrictEqual(stored, run.workspace)) {
|
|
1753
|
-
throw new Error(`Review Turn workspace is not the durable owner: ${run.id}.`);
|
|
1754
|
-
}
|
|
1755
|
-
if (run.workspace.owner.type === "review-round") {
|
|
1756
|
-
if (run.workspace.owner.taskId !== taskId
|
|
1757
|
-
|| run.workspace.owner.reviewRoundId !== round.id
|
|
1758
|
-
|| round.workspace === undefined
|
|
1759
|
-
|| !isDeepStrictEqual(round.workspace, run.workspace)) {
|
|
1760
|
-
throw new Error(`Review Turn workspace owner does not match its ReviewRound: ${run.id}.`);
|
|
1761
|
-
}
|
|
1762
|
-
return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
|
|
1763
|
-
}
|
|
1764
|
-
if (run.workspace.owner.type !== "execution-lane"
|
|
1765
|
-
|| run.workspace.owner.taskId !== taskId
|
|
1766
|
-
|| run.workspace.owner.purpose !== "review"
|
|
1767
|
-
|| run.workspace.owner.reviewRoundId !== round.id
|
|
1768
|
-
|| run.workspace.owner.executionGroupId !== run.executionGroupId
|
|
1769
|
-
|| run.workspace.owner.executionLaneId !== run.executionLaneId) {
|
|
1770
|
-
throw new Error(`Review Turn workspace owner does not match its Lane lineage: ${run.id}.`);
|
|
1771
|
-
}
|
|
1772
|
-
const lane = round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
1773
|
-
const writableProjectIds = run.workspace.entries
|
|
1774
|
-
.filter(({ access }) => access === "write")
|
|
1775
|
-
.map(({ projectId }) => projectId)
|
|
1776
|
-
.sort();
|
|
1777
|
-
if (lane === undefined
|
|
1778
|
-
|| lane.turnId !== run.id
|
|
1779
|
-
|| lane.roleName !== run.roleName
|
|
1780
|
-
|| lane.reviewRoundId !== round.id
|
|
1781
|
-
|| lane.workspace?.root !== run.workspace.root
|
|
1782
|
-
|| !isDeepStrictEqual([...lane.workspace.writableProjectIds].sort(), writableProjectIds)
|
|
1783
|
-
|| run.workspace.entries.length === 0
|
|
1784
|
-
|| run.workspace.entries.some(({ access }) => access !== "write")) {
|
|
1785
|
-
throw new Error(`Review Turn Lane workspace lineage is not exact: ${run.id}.`);
|
|
1786
|
-
}
|
|
1787
|
-
return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
|
|
1606
|
+
return this.#inspectEntries(task.id, this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries);
|
|
1788
1607
|
}
|
|
1789
1608
|
async inspectExecutionLaneWorkspace(taskId, executionGroupId, executionLaneId) {
|
|
1790
1609
|
const task = requireTask(this.store, taskId);
|
|
@@ -1793,7 +1612,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1793
1612
|
&& owner.executionLaneId === executionLaneId));
|
|
1794
1613
|
if (workspace === undefined)
|
|
1795
1614
|
return "missing";
|
|
1796
|
-
return this.#inspectEntries(this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1615
|
+
return this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1797
1616
|
}
|
|
1798
1617
|
async cleanupReviewRoundWorkspace(taskId, reviewRoundId) {
|
|
1799
1618
|
const task = requireTask(this.store, taskId);
|
|
@@ -1818,13 +1637,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
1818
1637
|
for (const lane of round.executionGroup?.lanes ?? []) {
|
|
1819
1638
|
assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
|
|
1820
1639
|
}
|
|
1821
|
-
|
|
1640
|
+
for (const lane of round.executionGroup?.lanes ?? []) {
|
|
1641
|
+
assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
|
|
1642
|
+
}
|
|
1643
|
+
if (await this.#inspectEntries(task.id, this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries) === "dirty")
|
|
1822
1644
|
return "dirty";
|
|
1823
1645
|
let removed = false;
|
|
1824
1646
|
for (const entry of workspace.entries) {
|
|
1825
1647
|
const project = requireProject(this.store, entry.projectId);
|
|
1826
1648
|
const result = await this.git.removeWorktree({
|
|
1827
|
-
repositoryPath: project.
|
|
1649
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1828
1650
|
container: this.#projectContainer(project.name),
|
|
1829
1651
|
taskSegment: this.#taskSegment(task),
|
|
1830
1652
|
roleName: this.#reviewWorktreeName(round),
|
|
@@ -1884,13 +1706,13 @@ export class FileTaskWorkspacePreparer {
|
|
|
1884
1706
|
assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
|
|
1885
1707
|
}
|
|
1886
1708
|
const writable = workspace.entries.filter(({ access }) => access === "write");
|
|
1887
|
-
if (await this.#inspectEntries(this.#taskSegment(task), managedWorktreeName(workspace.owner), writable) === "dirty")
|
|
1709
|
+
if (await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), writable) === "dirty")
|
|
1888
1710
|
return "dirty";
|
|
1889
1711
|
let removed = false;
|
|
1890
1712
|
for (const entry of writable) {
|
|
1891
1713
|
const project = requireProject(this.store, entry.projectId);
|
|
1892
1714
|
const result = await this.git.removeWorktree({
|
|
1893
|
-
repositoryPath: project.
|
|
1715
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1894
1716
|
container: this.#projectContainer(project.name),
|
|
1895
1717
|
taskSegment: this.#taskSegment(task),
|
|
1896
1718
|
roleName: managedWorktreeName(workspace.owner),
|
|
@@ -1916,6 +1738,61 @@ export class FileTaskWorkspacePreparer {
|
|
|
1916
1738
|
}
|
|
1917
1739
|
return removed ? "removed" : "missing";
|
|
1918
1740
|
}
|
|
1741
|
+
async inspectIntegrationWorkspace(taskId, integrationId) {
|
|
1742
|
+
const workspace = this.store.getIntegrationWorkspace(taskId, integrationId);
|
|
1743
|
+
if (workspace === null)
|
|
1744
|
+
return "missing";
|
|
1745
|
+
if (workspace.owner.type !== "integration-attempt"
|
|
1746
|
+
|| workspace.owner.integrationAttemptId !== integrationId) {
|
|
1747
|
+
throw new Error(`Integration workspace ownership is invalid: ${taskId}/${integrationId}.`);
|
|
1748
|
+
}
|
|
1749
|
+
let found = false;
|
|
1750
|
+
for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
|
|
1751
|
+
try {
|
|
1752
|
+
if (!await this.git.isClean(entry.path))
|
|
1753
|
+
return "dirty";
|
|
1754
|
+
found = true;
|
|
1755
|
+
}
|
|
1756
|
+
catch (error) {
|
|
1757
|
+
if (error instanceof Error && error.message.includes("No such file"))
|
|
1758
|
+
continue;
|
|
1759
|
+
throw error;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return found ? "clean" : "missing";
|
|
1763
|
+
}
|
|
1764
|
+
async cleanupIntegrationWorkspace(taskId, integrationId) {
|
|
1765
|
+
const task = requireTask(this.store, taskId);
|
|
1766
|
+
const attempt = this.store.getIntegrationAttempt(task.id, integrationId);
|
|
1767
|
+
if (attempt === null) {
|
|
1768
|
+
throw new Error(`IntegrationAttempt not found: ${task.id}/${integrationId}.`);
|
|
1769
|
+
}
|
|
1770
|
+
if (attempt.status === "running"
|
|
1771
|
+
|| attempt.status === "blocked"
|
|
1772
|
+
|| attempt.status === "validating") {
|
|
1773
|
+
throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
|
|
1774
|
+
}
|
|
1775
|
+
const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
|
|
1776
|
+
const project = requireProject(this.store, attempt.projectId);
|
|
1777
|
+
const result = await this.git.removeIntegrationWorktree({
|
|
1778
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1779
|
+
container: this.#projectContainer(project.name),
|
|
1780
|
+
taskSegment: this.#taskSegment(task),
|
|
1781
|
+
integrationId: attempt.id,
|
|
1782
|
+
discardChanges: attempt.status === "failed"
|
|
1783
|
+
});
|
|
1784
|
+
if (result !== "dirty") {
|
|
1785
|
+
if (workspace !== null)
|
|
1786
|
+
this.#resourceRegistrar().markWorkspaceDeleted(workspace);
|
|
1787
|
+
await rm(join(this.home, "artifacts", "integration-checks", task.id, attempt.id), { recursive: true, force: true });
|
|
1788
|
+
this.store.removeManagedWorkspace({
|
|
1789
|
+
type: "integration-attempt",
|
|
1790
|
+
taskId: task.id,
|
|
1791
|
+
integrationAttemptId: attempt.id
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
return result;
|
|
1795
|
+
}
|
|
1919
1796
|
async inspectTaskMainWorkspace(taskId) {
|
|
1920
1797
|
const task = requireTask(this.store, taskId);
|
|
1921
1798
|
const main = this.store.getTaskWorkspace(task.id);
|
|
@@ -1924,7 +1801,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1924
1801
|
if (main.owner.type !== "task") {
|
|
1925
1802
|
throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
|
|
1926
1803
|
}
|
|
1927
|
-
return this.#inspectEntries(this.#taskSegment(task), MAIN_WORKTREE, main.entries);
|
|
1804
|
+
return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
|
|
1928
1805
|
}
|
|
1929
1806
|
async cleanupTaskForArchive(taskId) {
|
|
1930
1807
|
const task = requireTask(this.store, taskId);
|
|
@@ -1942,7 +1819,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1942
1819
|
const main = this.store.getTaskWorkspace(task.id);
|
|
1943
1820
|
if (main !== null) {
|
|
1944
1821
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
1945
|
-
if (await this.#inspectEntries(this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
|
|
1822
|
+
if (await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
|
|
1946
1823
|
return {
|
|
1947
1824
|
taskId,
|
|
1948
1825
|
status: "retained-dirty",
|
|
@@ -1951,13 +1828,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1951
1828
|
}
|
|
1952
1829
|
for (const entry of main.entries) {
|
|
1953
1830
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
1954
|
-
const
|
|
1955
|
-
const result = await this.git.removeWorktree({
|
|
1956
|
-
repositoryPath: project.path,
|
|
1957
|
-
container: this.#projectContainer(project.name),
|
|
1958
|
-
taskSegment: this.#taskSegment(task),
|
|
1959
|
-
roleName: MAIN_WORKTREE
|
|
1960
|
-
});
|
|
1831
|
+
const result = await this.git.removeStrandedWorktree(entry.path);
|
|
1961
1832
|
if (result === "dirty") {
|
|
1962
1833
|
throw new Error(`Task workspace changed after cleanup preflight: ${task.id}.`);
|
|
1963
1834
|
}
|
|
@@ -1971,14 +1842,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
1971
1842
|
this.#clearTaskWorkspace(task, this.#fallbackWorkspace());
|
|
1972
1843
|
return { taskId, status: "removed" };
|
|
1973
1844
|
}
|
|
1974
|
-
async #inspectEntries(taskSegment, roleName, entries) {
|
|
1845
|
+
async #inspectEntries(taskId, taskSegment, roleName, entries) {
|
|
1975
1846
|
if (entries.length === 0)
|
|
1976
1847
|
return "missing";
|
|
1977
1848
|
let found = false;
|
|
1978
1849
|
for (const entry of entries) {
|
|
1979
1850
|
const project = requireProject(this.store, entry.projectId);
|
|
1980
1851
|
const state = await this.git.inspectWorktree({
|
|
1981
|
-
repositoryPath: project.
|
|
1852
|
+
repositoryPath: this.#taskRepositoryPath(taskId, project.id),
|
|
1982
1853
|
container: this.#projectContainer(project.name),
|
|
1983
1854
|
taskSegment,
|
|
1984
1855
|
roleName
|
|
@@ -1989,39 +1860,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
1989
1860
|
}
|
|
1990
1861
|
return found ? "clean" : "missing";
|
|
1991
1862
|
}
|
|
1992
|
-
async #snapshotReviewWorkspaceEntries(reviewRoundId, workspace, reviewBaseCommit) {
|
|
1993
|
-
const changed = [];
|
|
1994
|
-
let dirty = false;
|
|
1995
|
-
for (const entry of workspace.entries) {
|
|
1996
|
-
if (await this.git.headRef(entry.path) !== entry.branch) {
|
|
1997
|
-
throw new Error(`Review Project workspace left its managed branch: ${reviewRoundId}/${entry.projectId}.`);
|
|
1998
|
-
}
|
|
1999
|
-
const head = (await this.git.inspect(entry.path, "HEAD")).baseCommit;
|
|
2000
|
-
const project = requireProject(this.store, entry.projectId);
|
|
2001
|
-
if (!await this.git.isAncestor(project.path, entry.baseCommit, head)) {
|
|
2002
|
-
throw new Error(`Review workspace HEAD does not descend from its frozen base for `
|
|
2003
|
-
+ `${reviewRoundId}/${entry.projectId}: expected ancestor ${entry.baseCommit}, `
|
|
2004
|
-
+ `physical HEAD ${head}.`);
|
|
2005
|
-
}
|
|
2006
|
-
if (head !== entry.baseCommit)
|
|
2007
|
-
changed.push(head);
|
|
2008
|
-
if (!await this.git.isClean(entry.path))
|
|
2009
|
-
dirty = true;
|
|
2010
|
-
}
|
|
2011
|
-
if (changed.length > 1) {
|
|
2012
|
-
throw new Error(`Review workspace has diagnostic commits in multiple Projects; preserve it for Leader routing: ${reviewRoundId}.`);
|
|
2013
|
-
}
|
|
2014
|
-
// A dirty worktree has uncommitted diagnostics: no single commit captures
|
|
2015
|
-
// the tree the checks ran on, so no evidenceCommit can attest it and the
|
|
2016
|
-
// queue must re-run the gate. A clean worktree attests that checks ran on
|
|
2017
|
-
// the recorded tree: the frozen base when the reviewer made no commits, or
|
|
2018
|
-
// the reviewer's single diagnostic commit otherwise.
|
|
2019
|
-
if (dirty)
|
|
2020
|
-
return {};
|
|
2021
|
-
return changed.length === 0
|
|
2022
|
-
? { evidenceCommit: reviewBaseCommit }
|
|
2023
|
-
: { evidenceCommit: changed[0] };
|
|
2024
|
-
}
|
|
2025
1863
|
#projectContainer(projectName) {
|
|
2026
1864
|
return join(resolveWorktreeRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(projectName));
|
|
2027
1865
|
}
|
|
@@ -2057,8 +1895,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
2057
1895
|
async #discardUnadoptedEntries(task, taskSegment, prepared, roleName, deleteBranch = false, adoptedPaths = new Set(this.store.listManagedWorkspaces(task.id)
|
|
2058
1896
|
.flatMap((workspace) => workspace.entries.map(({ path }) => path)))) {
|
|
2059
1897
|
for (const { project, entry } of prepared.filter(({ entry }) => entry.access === "write" && !adoptedPaths.has(entry.path))) {
|
|
1898
|
+
if (roleName === MAIN_WORKTREE) {
|
|
1899
|
+
const removal = await this.git.removeStrandedWorktree(entry.path);
|
|
1900
|
+
if (removal === "dirty") {
|
|
1901
|
+
throw new Error(`Unadopted Task clone is dirty and was retained at ${entry.path}; inspect it and retry.`);
|
|
1902
|
+
}
|
|
1903
|
+
this.#resourceRegistrar().markPathsDeleted([entry.path]);
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
2060
1906
|
const removal = await this.git.removeWorktree({
|
|
2061
|
-
repositoryPath: project.
|
|
1907
|
+
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
2062
1908
|
container: this.#projectContainer(project.name),
|
|
2063
1909
|
taskSegment,
|
|
2064
1910
|
roleName,
|
|
@@ -2070,6 +1916,17 @@ export class FileTaskWorkspacePreparer {
|
|
|
2070
1916
|
this.#resourceRegistrar().markPathsDeleted([entry.path]);
|
|
2071
1917
|
}
|
|
2072
1918
|
}
|
|
1919
|
+
#taskRepositoryPath(taskId, projectId) {
|
|
1920
|
+
const workspace = this.store.getTaskWorkspace(taskId);
|
|
1921
|
+
if (workspace === null || workspace.owner.type !== "task") {
|
|
1922
|
+
throw new Error(`Task main workspace is unavailable: ${taskId}.`);
|
|
1923
|
+
}
|
|
1924
|
+
const entry = workspace.entries.find((candidate) => candidate.projectId === projectId);
|
|
1925
|
+
if (entry === undefined) {
|
|
1926
|
+
throw new Error(`Task main Project workspace is unavailable: ${taskId}/${projectId}.`);
|
|
1927
|
+
}
|
|
1928
|
+
return entry.path;
|
|
1929
|
+
}
|
|
2073
1930
|
#recordWorkspaceRemoval(task, workspace, fallback, workItem) {
|
|
2074
1931
|
this.store.transaction((tx) => {
|
|
2075
1932
|
if (workspace.owner.type === "task") {
|
|
@@ -2272,7 +2129,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
|
|
|
2272
2129
|
|| session === undefined
|
|
2273
2130
|
|| session.agentId !== role.activeAgentId
|
|
2274
2131
|
|| session.adapterId !== run.effective.adapterId
|
|
2275
|
-
|| session.
|
|
2132
|
+
|| session.runtimeGenerationId === undefined
|
|
2276
2133
|
|| session.nativeSessionId === undefined
|
|
2277
2134
|
|| !["ready", "running"].includes(session.status)
|
|
2278
2135
|
|| !isDeepStrictEqual(session.effective, run.effective)
|
|
@@ -2290,7 +2147,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
|
|
|
2290
2147
|
&& lifecycle !== undefined
|
|
2291
2148
|
&& isRuntimeLaunchReservation(lifecycle)) {
|
|
2292
2149
|
const executionRef = lifecycle.executionRef;
|
|
2293
|
-
if (!isRuntimeLaunchReservation(lifecycle, session.
|
|
2150
|
+
if (!isRuntimeLaunchReservation(lifecycle, session.runtimeGenerationId)
|
|
2294
2151
|
|| executionRef?.type !== "turn"
|
|
2295
2152
|
|| executionRef.taskId !== taskId
|
|
2296
2153
|
|| executionRef.id !== run.id)
|
|
@@ -2367,22 +2224,6 @@ function safePathSegment(value) {
|
|
|
2367
2224
|
function sameCommit(left, right) {
|
|
2368
2225
|
return left.toLowerCase() === right.toLowerCase();
|
|
2369
2226
|
}
|
|
2370
|
-
function remoteDefaultProjects(store, taskId) {
|
|
2371
|
-
const projectIds = new Set();
|
|
2372
|
-
for (const event of store.listEvents(taskId)) {
|
|
2373
|
-
if (event.type === "task.created") {
|
|
2374
|
-
for (const projectId of (event.payload.defaultProjectIds ?? "").split(",")) {
|
|
2375
|
-
const normalized = projectId.trim();
|
|
2376
|
-
if (normalized.length > 0)
|
|
2377
|
-
projectIds.add(normalized);
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
return projectIds;
|
|
2382
|
-
}
|
|
2383
|
-
function looksLikeCommit(value) {
|
|
2384
|
-
return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
|
|
2385
|
-
}
|
|
2386
2227
|
export function resolveWorktreeRoot(home, workspace) {
|
|
2387
2228
|
return join(resolveWorkspaceRoot(home, workspace), "worktree");
|
|
2388
2229
|
}
|