@zq-silk/yui 0.13.9 → 0.14.0
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 +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -12,10 +12,9 @@ import { createTaskEvent } from "../event/taskEvent.js";
|
|
|
12
12
|
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
13
13
|
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
|
|
14
14
|
import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
15
|
-
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
16
15
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
17
16
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
18
|
-
import { generateTaskWorkspaceIdentity,
|
|
17
|
+
import { generateTaskWorkspaceIdentity, taskMainBranch, taskWorkspaceRefSegment, taskWorkspaceRefSegmentFromIdentity, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
19
18
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
20
19
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
21
20
|
const MAIN_WORKTREE = "main";
|
|
@@ -71,7 +70,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
71
70
|
}
|
|
72
71
|
async prepareTaskWorkspace(taskId) {
|
|
73
72
|
// The per-Project maintenance fence makes prepare mutually exclusive with
|
|
74
|
-
//
|
|
73
|
+
// Project maintenance and Task archive cleanup: a concurrent migration must not switch the
|
|
75
74
|
// Project catalog to the Home-managed repo while prepare is creating
|
|
76
75
|
// worktrees from the old external checkout. The fence is acquired on
|
|
77
76
|
// every conflict retry so the locked set always covers the Task's
|
|
@@ -271,10 +270,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
271
270
|
if (existing !== null && existing.owner.type !== "task") {
|
|
272
271
|
throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
|
|
273
272
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
273
|
+
if (task.workspaceIdentity === undefined
|
|
274
|
+
&& existing !== null
|
|
275
|
+
&& existing.entries.length > 0) {
|
|
276
|
+
throw new Error(`Task workspace does not use the current identity contract: ${task.id}. `
|
|
277
|
+
+ "Preserve the old Home for diagnosis and let the Operator create a new Task.");
|
|
278
|
+
}
|
|
279
|
+
// The durable workspace identity is minted once before the first managed
|
|
280
|
+
// Git workspace is adopted; every later prepare reuses it.
|
|
278
281
|
const workspaceIdentity = task.workspaceIdentity === undefined
|
|
279
282
|
&& (existing === null || existing.entries.length === 0)
|
|
280
283
|
? await this.#mintTaskWorkspaceIdentity(task)
|
|
@@ -414,9 +417,9 @@ export class FileTaskWorkspacePreparer {
|
|
|
414
417
|
if (current !== null && current.owner.type !== "task") {
|
|
415
418
|
throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
|
|
416
419
|
}
|
|
417
|
-
// Single-writer CAS
|
|
420
|
+
// Single-writer CAS: this attempt only
|
|
418
421
|
// commits while the Task is still unbound (or carries the identity it
|
|
419
|
-
// started with). A concurrent prepare
|
|
422
|
+
// started with). A concurrent prepare that bound a
|
|
420
423
|
// different identity wins; this attempt discards its refs and retries
|
|
421
424
|
// through the StorageConflictError channel.
|
|
422
425
|
if (workspaceIdentity !== undefined
|
|
@@ -462,18 +465,18 @@ export class FileTaskWorkspacePreparer {
|
|
|
462
465
|
// durable WorkItem or ReviewRound workspace owns that cwd; Task-main
|
|
463
466
|
// preparation must not move a Reviewer Session out from under an
|
|
464
467
|
// active or retained ReviewRound. Other Roles use Task main.
|
|
465
|
-
// Prefer the active
|
|
466
|
-
// to the first queued WorkItem only when no active
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
&&
|
|
470
|
-
&&
|
|
471
|
-
? tx.getWorkItem(task.id,
|
|
468
|
+
// Prefer the active Turn's exact WorkItem for this Role; fall back
|
|
469
|
+
// to the first queued WorkItem only when no active Turn owns the Role.
|
|
470
|
+
const activeRoleTurn = tx.getActiveTurn(task.id, role.name);
|
|
471
|
+
const activeTurnItem = activeRoleTurn !== null
|
|
472
|
+
&& activeRoleTurn.purpose === "execution"
|
|
473
|
+
&& activeRoleTurn.workItemId !== undefined
|
|
474
|
+
? tx.getWorkItem(task.id, activeRoleTurn.workItemId)
|
|
472
475
|
: null;
|
|
473
|
-
const assignedItem =
|
|
474
|
-
&&
|
|
475
|
-
&& !["completed", "failed", "retired"].includes(
|
|
476
|
-
?
|
|
476
|
+
const assignedItem = activeTurnItem !== null
|
|
477
|
+
&& activeTurnItem.assignee === role.name
|
|
478
|
+
&& !["completed", "failed", "retired"].includes(activeTurnItem.status)
|
|
479
|
+
? activeTurnItem
|
|
477
480
|
: tx.listWorkItems(task.id).find((candidate) => (candidate.assignee === role.name
|
|
478
481
|
&& !["completed", "failed", "retired"]
|
|
479
482
|
.includes(candidate.status)));
|
|
@@ -666,7 +669,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
666
669
|
const task = requireTask(this.store, item.taskId);
|
|
667
670
|
assertWorkItemWorkspaceEligible(this.store, task, item);
|
|
668
671
|
// Hold the per-Project maintenance fence across the ensure-main-prepared
|
|
669
|
-
// and the WorkItem worktree creation, so
|
|
672
|
+
// and the WorkItem worktree creation, so concurrent Project maintenance
|
|
670
673
|
// cannot switch the Project catalog between the two. The under-lock Task
|
|
671
674
|
// snapshot drives every Project read and Git effect below.
|
|
672
675
|
const { release, current: lockedTask } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
@@ -785,14 +788,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
785
788
|
|| !isDeepStrictEqual(latestItem.writeProjectIds, item.writeProjectIds)) {
|
|
786
789
|
throw new Error(`Work item changed while preparing its workspace: ${item.id}.`);
|
|
787
790
|
}
|
|
788
|
-
const
|
|
791
|
+
const activeDevelopTurn = tx.listTurns(lockedTask.id)
|
|
789
792
|
.find((run) => run.status === "active" && run.workItemId === item.id);
|
|
790
|
-
if (
|
|
791
|
-
throw new Error(`Work Item already has an active Develop
|
|
793
|
+
if (activeDevelopTurn !== undefined) {
|
|
794
|
+
throw new Error(`Work Item already has an active Develop Turn: ${activeDevelopTurn.id}.`);
|
|
792
795
|
}
|
|
793
796
|
if (latestItem.assignee !== undefined
|
|
794
|
-
&& tx.
|
|
795
|
-
throw new Error(`Role has an active
|
|
797
|
+
&& tx.getActiveTurn(lockedTask.id, latestItem.assignee) !== null) {
|
|
798
|
+
throw new Error(`Role has an active Turn: ${lockedTask.id}/${latestItem.assignee}.`);
|
|
796
799
|
}
|
|
797
800
|
const existingWorkspace = tx.getWorkItemWorkspace(lockedTask.id, item.id);
|
|
798
801
|
if (existingWorkspace !== null && (existingWorkspace.owner.type !== "work-item"
|
|
@@ -830,7 +833,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
830
833
|
const task = requireTask(this.store, taskId);
|
|
831
834
|
// Hold the per-Project maintenance fence across Lane worktree creation
|
|
832
835
|
// (both the reuse and the fresh-mint paths), so a concurrent
|
|
833
|
-
//
|
|
836
|
+
// Project maintenance cannot switch the Project catalog mid-prepare. A
|
|
834
837
|
// dispatch preflight that prepares a whole new Group passes the fence it
|
|
835
838
|
// already holds (one locked boundary across preparation and adoption);
|
|
836
839
|
// otherwise the fence is acquired here.
|
|
@@ -976,9 +979,9 @@ export class FileTaskWorkspacePreparer {
|
|
|
976
979
|
}
|
|
977
980
|
const workspace = this.store.getWorkItemWorkspace(taskId, workItemId);
|
|
978
981
|
const selected = (selectedLaneIds.length === 0
|
|
979
|
-
? group.lanes.filter((lane) => lane.status === "
|
|
982
|
+
? group.lanes.filter((lane) => lane.status === "completed")
|
|
980
983
|
: group.lanes.filter((lane) => selectedLaneIds.includes(lane.id)));
|
|
981
|
-
if (selected.some((lane) =>
|
|
984
|
+
if (selected.some((lane) => lane.status !== "completed")) {
|
|
982
985
|
throw new Error(`ExecutionGroup has a selected Lane without terminal output: ${executionGroupId}.`);
|
|
983
986
|
}
|
|
984
987
|
if (selected.length === 0)
|
|
@@ -1045,7 +1048,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1045
1048
|
}
|
|
1046
1049
|
const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
|
|
1047
1050
|
if (targetHead !== frozen.headCommit) {
|
|
1048
|
-
throw new Error(`Single-Lane workspace changed
|
|
1051
|
+
throw new Error(`Single-Lane workspace changed before resolution: ${lane.id}/${target.projectId}.`);
|
|
1049
1052
|
}
|
|
1050
1053
|
}
|
|
1051
1054
|
continue;
|
|
@@ -1067,7 +1070,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1067
1070
|
}
|
|
1068
1071
|
const sourceHead = (await this.git.inspect(source.path, "HEAD")).baseCommit;
|
|
1069
1072
|
if (sourceHead !== frozen.headCommit) {
|
|
1070
|
-
throw new Error(`Selected Lane advanced
|
|
1073
|
+
throw new Error(`Selected Lane advanced before resolution: ${lane.id}/${target.projectId}.`);
|
|
1071
1074
|
}
|
|
1072
1075
|
const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
|
|
1073
1076
|
if (sourceHead !== targetHead && !await this.git.isAncestor(target.path, sourceHead, targetHead)) {
|
|
@@ -1139,7 +1142,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1139
1142
|
: this.store.getReviewRound(taskId, lineage.reviewRoundId)?.executionGroup;
|
|
1140
1143
|
const lane = group?.lanes.find(({ id }) => id === executionLaneId);
|
|
1141
1144
|
if (group === undefined
|
|
1142
|
-
|| lane === undefined || !["completed", "failed", "
|
|
1145
|
+
|| lane === undefined || !["completed", "failed", "skipped"].includes(lane.status)) {
|
|
1143
1146
|
throw new Error(`Execution Lane is not terminally resolved: ${taskId}/${executionLaneId}.`);
|
|
1144
1147
|
}
|
|
1145
1148
|
const workspace = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane"
|
|
@@ -1533,8 +1536,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1533
1536
|
|| candidateChanged) {
|
|
1534
1537
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound changed while preparing its workspace: ${round.id}.`);
|
|
1535
1538
|
}
|
|
1536
|
-
if (tx.
|
|
1537
|
-
throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active
|
|
1539
|
+
if (tx.getActiveTurn(task.id, reviewer.name) !== null) {
|
|
1540
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Turn: ${task.id}/${reviewer.name}.`);
|
|
1538
1541
|
}
|
|
1539
1542
|
const currentWorkspace = tx.getReviewRoundWorkspace(task.id, round.id);
|
|
1540
1543
|
if (existing !== null
|
|
@@ -1599,8 +1602,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1599
1602
|
|| previousWorkspace.root !== reviewRoot) {
|
|
1600
1603
|
throw new ReviewRoundWorkspaceEvidenceError(`Previous Task-final Review workspace cannot be continued: ${previous.id}.`);
|
|
1601
1604
|
}
|
|
1602
|
-
if (this.store.
|
|
1603
|
-
throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active
|
|
1605
|
+
if (this.store.getActiveTurn(task.id, reviewer.name) !== null) {
|
|
1606
|
+
throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Turn: ${task.id}/${reviewer.name}.`);
|
|
1604
1607
|
}
|
|
1605
1608
|
const expected = new Map(frozenEntries.map((entry) => [entry.projectId, entry]));
|
|
1606
1609
|
if (previousWorkspace.entries.length !== expected.size) {
|
|
@@ -1667,7 +1670,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1667
1670
|
|| currentRound.workspace !== undefined
|
|
1668
1671
|
|| !isDeepStrictEqual(currentRound.taskCandidate, round.taskCandidate)
|
|
1669
1672
|
|| tx.getReviewRoundWorkspace(task.id, round.id) !== null
|
|
1670
|
-
|| tx.
|
|
1673
|
+
|| tx.getActiveTurn(task.id, reviewer.name) !== null) {
|
|
1671
1674
|
throw new ReviewRoundWorkspaceEvidenceError(`Task-final Review workspace changed before reassignment: ${previous.id}/${round.id}.`);
|
|
1672
1675
|
}
|
|
1673
1676
|
const latestReviewer = tx.getRole(task.id, reviewer.name);
|
|
@@ -1730,31 +1733,31 @@ export class FileTaskWorkspacePreparer {
|
|
|
1730
1733
|
return this.#snapshotReviewWorkspaceEntries(round.id, workspace, round.reviewBaseCommit);
|
|
1731
1734
|
}
|
|
1732
1735
|
/**
|
|
1733
|
-
* Snapshot the exact workspace recorded on one Reviewer
|
|
1736
|
+
* Snapshot the exact workspace recorded on one Reviewer Turn. Panel Lanes
|
|
1734
1737
|
* own separate durable workspaces; the ReviewRound's shared workspace is
|
|
1735
1738
|
* not a valid substitute for a Lane's branch/head evidence.
|
|
1736
1739
|
*/
|
|
1737
1740
|
async snapshotReviewRunResult(taskId, run) {
|
|
1738
1741
|
if (run.taskId !== taskId || run.purpose !== "review" || run.reviewRoundId === undefined) {
|
|
1739
|
-
throw new Error(`
|
|
1742
|
+
throw new Error(`Turn is not an exact ReviewRound Turn: ${run.id}.`);
|
|
1740
1743
|
}
|
|
1741
1744
|
if (run.workspace === undefined) {
|
|
1742
|
-
throw new Error(`Review
|
|
1745
|
+
throw new Error(`Review Turn has no managed workspace: ${run.id}.`);
|
|
1743
1746
|
}
|
|
1744
1747
|
const round = this.store.getReviewRound(taskId, run.reviewRoundId);
|
|
1745
1748
|
if (round === null || round.status !== "running") {
|
|
1746
|
-
throw new Error(`ReviewRound is not running for Review
|
|
1749
|
+
throw new Error(`ReviewRound is not running for Review Turn: ${run.id}.`);
|
|
1747
1750
|
}
|
|
1748
1751
|
const stored = this.store.getManagedWorkspace(run.workspace.owner);
|
|
1749
1752
|
if (stored === null || !isDeepStrictEqual(stored, run.workspace)) {
|
|
1750
|
-
throw new Error(`Review
|
|
1753
|
+
throw new Error(`Review Turn workspace is not the durable owner: ${run.id}.`);
|
|
1751
1754
|
}
|
|
1752
1755
|
if (run.workspace.owner.type === "review-round") {
|
|
1753
1756
|
if (run.workspace.owner.taskId !== taskId
|
|
1754
1757
|
|| run.workspace.owner.reviewRoundId !== round.id
|
|
1755
1758
|
|| round.workspace === undefined
|
|
1756
1759
|
|| !isDeepStrictEqual(round.workspace, run.workspace)) {
|
|
1757
|
-
throw new Error(`Review
|
|
1760
|
+
throw new Error(`Review Turn workspace owner does not match its ReviewRound: ${run.id}.`);
|
|
1758
1761
|
}
|
|
1759
1762
|
return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
|
|
1760
1763
|
}
|
|
@@ -1764,7 +1767,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1764
1767
|
|| run.workspace.owner.reviewRoundId !== round.id
|
|
1765
1768
|
|| run.workspace.owner.executionGroupId !== run.executionGroupId
|
|
1766
1769
|
|| run.workspace.owner.executionLaneId !== run.executionLaneId) {
|
|
1767
|
-
throw new Error(`Review
|
|
1770
|
+
throw new Error(`Review Turn workspace owner does not match its Lane lineage: ${run.id}.`);
|
|
1768
1771
|
}
|
|
1769
1772
|
const lane = round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
|
|
1770
1773
|
const writableProjectIds = run.workspace.entries
|
|
@@ -1772,14 +1775,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
1772
1775
|
.map(({ projectId }) => projectId)
|
|
1773
1776
|
.sort();
|
|
1774
1777
|
if (lane === undefined
|
|
1775
|
-
|| lane.
|
|
1778
|
+
|| lane.turnId !== run.id
|
|
1776
1779
|
|| lane.roleName !== run.roleName
|
|
1777
1780
|
|| lane.reviewRoundId !== round.id
|
|
1778
1781
|
|| lane.workspace?.root !== run.workspace.root
|
|
1779
1782
|
|| !isDeepStrictEqual([...lane.workspace.writableProjectIds].sort(), writableProjectIds)
|
|
1780
1783
|
|| run.workspace.entries.length === 0
|
|
1781
1784
|
|| run.workspace.entries.some(({ access }) => access !== "write")) {
|
|
1782
|
-
throw new Error(`Review
|
|
1785
|
+
throw new Error(`Review Turn Lane workspace lineage is not exact: ${run.id}.`);
|
|
1783
1786
|
}
|
|
1784
1787
|
return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
|
|
1785
1788
|
}
|
|
@@ -1968,302 +1971,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
1968
1971
|
this.#clearTaskWorkspace(task, this.#fallbackWorkspace());
|
|
1969
1972
|
return { taskId, status: "removed" };
|
|
1970
1973
|
}
|
|
1971
|
-
/**
|
|
1972
|
-
* Rebuild an eligible legacy Task's managed Git workspace under a fresh
|
|
1973
|
-
* workspace identity. Legacy refs are archived (never deleted outright),
|
|
1974
|
-
* the new main worktrees start from verified remote SHAs, and the Task
|
|
1975
|
-
* record switches only after every Git side effect succeeded, so a crash
|
|
1976
|
-
* at any point leaves the old layout usable and the command resumable.
|
|
1977
|
-
*
|
|
1978
|
-
* A Task that already carries an identity takes the resume path: only the
|
|
1979
|
-
* pending legacy archive and old-worktree removal run.
|
|
1980
|
-
*/
|
|
1981
|
-
async rebuildTaskWorkspace(taskId, options = {}) {
|
|
1982
|
-
const task = requireTask(this.store, taskId);
|
|
1983
|
-
if (task.status !== "active") {
|
|
1984
|
-
throw new Error(`Only an active Task can rebuild its adopted workspace: ${task.id}/${task.status}.`);
|
|
1985
|
-
}
|
|
1986
|
-
// The rebuild holds every touched Project's maintenance fence for its
|
|
1987
|
-
// whole duration, so the Controller defers preparation and no other
|
|
1988
|
-
// maintenance interleaves. The resume path may clean up legacy
|
|
1989
|
-
// worktrees/refs in any Project; the fresh path touches its bound
|
|
1990
|
-
// Projects plus any still holding the Task's legacy refs.
|
|
1991
|
-
const projectIds = task.workspaceIdentity !== undefined
|
|
1992
|
-
? this.store.listProjects().map(({ id }) => id)
|
|
1993
|
-
: [
|
|
1994
|
-
...new Set([
|
|
1995
|
-
...task.projectBindings.map(({ projectId }) => projectId),
|
|
1996
|
-
...(await this.listLegacyTaskRefs(task.id)).map(({ projectId }) => projectId)
|
|
1997
|
-
])
|
|
1998
|
-
];
|
|
1999
|
-
const releaseMaintenance = acquireProjectMaintenanceLocks(this.home, projectIds);
|
|
2000
|
-
try {
|
|
2001
|
-
return await this.#rebuildTaskWorkspaceLocked(task, options);
|
|
2002
|
-
}
|
|
2003
|
-
finally {
|
|
2004
|
-
releaseMaintenance();
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
async #rebuildTaskWorkspaceLocked(task, options) {
|
|
2008
|
-
if (task.workspaceIdentity !== undefined) {
|
|
2009
|
-
// Quick Win (EXE-08): an identity-bearing Task cannot silently "resume"
|
|
2010
|
-
// when --latest is requested. The resume branch only cleans legacy
|
|
2011
|
-
// refs and reclaims orphans; it never re-pins the baseline. Reporting
|
|
2012
|
-
// success here would manufacture a split-brain state where the physical
|
|
2013
|
-
// Task branch moves but the binding, ManagedWorkspace, and Run snapshot
|
|
2014
|
-
// stay on the old commit. Refuse explicitly and point at the correct
|
|
2015
|
-
// tools instead.
|
|
2016
|
-
if (options.latestRemote === true) {
|
|
2017
|
-
throw new Error(`Task ${task.id} already owns a workspace identity; `
|
|
2018
|
-
+ "`task rebuild --latest` cannot re-pin its baseline in place. "
|
|
2019
|
-
+ "Use `yui task base status " + task.id + "` to inspect the current "
|
|
2020
|
-
+ "binding, ManagedWorkspace, physical HEAD, and Run snapshot. "
|
|
2021
|
-
+ "For an Active Task, integrate upstream changes through the "
|
|
2022
|
-
+ "normal Leader-driven integration path; for a Draft Task with no "
|
|
2023
|
-
+ "execution evidence, recreate the Task workspace.");
|
|
2024
|
-
}
|
|
2025
|
-
const current = this.store.getTaskWorkspace(task.id);
|
|
2026
|
-
if (current !== null && current.owner.type === "task") {
|
|
2027
|
-
await ensureWorkspaceView(current.root, current.entries);
|
|
2028
|
-
}
|
|
2029
|
-
// Remove the legacy worktrees before archiving their refs: a worktree
|
|
2030
|
-
// whose checked-out branch was just deleted reports an unborn-branch
|
|
2031
|
-
// ("dirty") status and can no longer be removed cleanly.
|
|
2032
|
-
await this.#removeLegacyWorktrees(task, this.store.listProjects().map((project) => project.id));
|
|
2033
|
-
const archived = await this.#archiveLegacyRefs(task);
|
|
2034
|
-
// Reclaim token worktrees/branches a crashed prepare or rebuild left
|
|
2035
|
-
// behind without a catalog record, so they do not accumulate.
|
|
2036
|
-
await this.#reclaimOrphanedTaskWorktrees(task);
|
|
2037
|
-
return { task: requireTask(this.store, task.id), archived, resumed: true };
|
|
2038
|
-
}
|
|
2039
|
-
assertTaskHasNoEvidence(this.store, task.id);
|
|
2040
|
-
const existing = this.store.getTaskWorkspace(task.id);
|
|
2041
|
-
if (existing !== null && existing.owner.type !== "task") {
|
|
2042
|
-
throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
|
|
2043
|
-
}
|
|
2044
|
-
if (existing !== null
|
|
2045
|
-
&& await this.#inspectLegacyTaskEntries(task, existing.entries) === "dirty") {
|
|
2046
|
-
throw new Error(`Task workspace is dirty and blocks the rebuild: ${task.id}.`);
|
|
2047
|
-
}
|
|
2048
|
-
// Reclaim token worktrees/branches a crashed prepare or rebuild left
|
|
2049
|
-
// behind without a catalog record, before minting a new identity. A hard
|
|
2050
|
-
// crash after `ensureWorktree` but before the Task/catalog transaction
|
|
2051
|
-
// leaves the Task unbound; without this reaping the fresh path would
|
|
2052
|
-
// report success and leave the orphaned token worktree/branch behind.
|
|
2053
|
-
await this.#reclaimOrphanedTaskWorktrees(task);
|
|
2054
|
-
// Resolve verified remote SHAs before any Git side effect, mirroring the
|
|
2055
|
-
// first-prepare pinning: a remote default Project is fetched and its
|
|
2056
|
-
// exact advertised SHA is pinned; a local or explicit ref is validated.
|
|
2057
|
-
const defaultProjects = remoteDefaultProjects(this.store, task.id);
|
|
2058
|
-
const pins = new Map();
|
|
2059
|
-
for (const binding of task.projectBindings) {
|
|
2060
|
-
const project = requireProject(this.store, binding.projectId);
|
|
2061
|
-
// `--latest` explicitly re-resolves every remote-backed Project. Without
|
|
2062
|
-
// it, only a still-symbolic creation default is refreshed; an explicit
|
|
2063
|
-
// or previously pinned commit retains the established rebuild behavior.
|
|
2064
|
-
const useRemoteDefault = (options.latestRemote === true
|
|
2065
|
-
|| (defaultProjects.has(project.id) && !looksLikeCommit(binding.baseRef)))
|
|
2066
|
-
&& project.remoteUrl !== undefined;
|
|
2067
|
-
if (useRemoteDefault) {
|
|
2068
|
-
const resolver = this.git.resolveRemoteBaseline;
|
|
2069
|
-
if (typeof resolver !== "function") {
|
|
2070
|
-
throw new Error(`Git workspace cannot resolve the remote baseline for Project: ${project.id}.`);
|
|
2071
|
-
}
|
|
2072
|
-
const remote = await resolver.call(this.git, {
|
|
2073
|
-
repositoryPath: project.path,
|
|
2074
|
-
remoteUrl: project.remoteUrl,
|
|
2075
|
-
developmentRef: options.latestRemote === true
|
|
2076
|
-
? project.developmentBranch
|
|
2077
|
-
: binding.baseRef
|
|
2078
|
-
});
|
|
2079
|
-
pins.set(project.id, remote.commit);
|
|
2080
|
-
}
|
|
2081
|
-
else {
|
|
2082
|
-
await this.git.inspect(project.path, binding.baseRef);
|
|
2083
|
-
}
|
|
2084
|
-
}
|
|
2085
|
-
const identity = await this.#mintTaskWorkspaceIdentity(task);
|
|
2086
|
-
const taskSegment = taskWorkspaceRefSegmentFromIdentity(identity);
|
|
2087
|
-
const root = this.#taskWorkspaceRoot(task.id);
|
|
2088
|
-
const prepared = [];
|
|
2089
|
-
try {
|
|
2090
|
-
for (const binding of task.projectBindings) {
|
|
2091
|
-
const project = requireProject(this.store, binding.projectId);
|
|
2092
|
-
const baseRef = pins.get(project.id) ?? binding.baseRef;
|
|
2093
|
-
const physical = await this.git.ensureWorktree({
|
|
2094
|
-
repositoryPath: project.path,
|
|
2095
|
-
container: this.#projectContainer(project.name),
|
|
2096
|
-
taskSegment,
|
|
2097
|
-
roleName: MAIN_WORKTREE,
|
|
2098
|
-
baseRef
|
|
2099
|
-
});
|
|
2100
|
-
if (pins.has(project.id) && physical.baseCommit !== pins.get(project.id)) {
|
|
2101
|
-
throw new Error(`Task Project workspace did not start at the fetched remote baseline: ${project.id}.`);
|
|
2102
|
-
}
|
|
2103
|
-
prepared.push({
|
|
2104
|
-
project,
|
|
2105
|
-
entry: {
|
|
2106
|
-
projectId: project.id,
|
|
2107
|
-
directory: binding.directory,
|
|
2108
|
-
access: "write",
|
|
2109
|
-
path: physical.path,
|
|
2110
|
-
branch: physical.branch,
|
|
2111
|
-
baseRef,
|
|
2112
|
-
baseCommit: physical.baseCommit
|
|
2113
|
-
}
|
|
2114
|
-
});
|
|
2115
|
-
}
|
|
2116
|
-
await ensureWorkspaceView(root, prepared.map(({ entry }) => entry));
|
|
2117
|
-
const workspace = createManagedWorkspace({
|
|
2118
|
-
owner: { type: "task", taskId: task.id },
|
|
2119
|
-
root,
|
|
2120
|
-
entries: prepared.map(({ entry }) => entry)
|
|
2121
|
-
}, this.now());
|
|
2122
|
-
this.#registerWorkspace(workspace);
|
|
2123
|
-
// Switch the durable record only now that every new ref/worktree exists.
|
|
2124
|
-
this.store.transaction((tx) => {
|
|
2125
|
-
const latest = requireTask(tx, task.id);
|
|
2126
|
-
if (!["draft", "active"].includes(latest.status)
|
|
2127
|
-
|| latest.workspaceIdentity !== undefined
|
|
2128
|
-
|| !isDeepStrictEqual(latest.projectBindings, task.projectBindings)) {
|
|
2129
|
-
throw new Error(`Task changed while rebuilding its workspace: ${task.id}.`);
|
|
2130
|
-
}
|
|
2131
|
-
assertTaskHasNoEvidence(tx, task.id);
|
|
2132
|
-
const current = tx.getTaskWorkspace(task.id);
|
|
2133
|
-
if (current === null
|
|
2134
|
-
? existing !== null
|
|
2135
|
-
: existing === null || !sameManagedWorkspace(current, existing)) {
|
|
2136
|
-
throw new Error(`Task workspace changed while rebuilding: ${task.id}.`);
|
|
2137
|
-
}
|
|
2138
|
-
const pinnedBindings = latest.projectBindings.map((binding) => {
|
|
2139
|
-
const pinned = pins.get(binding.projectId);
|
|
2140
|
-
return pinned !== undefined ? { ...binding, baseRef: pinned } : binding;
|
|
2141
|
-
});
|
|
2142
|
-
const timestamp = this.now();
|
|
2143
|
-
let persistedTask = isDeepStrictEqual(pinnedBindings, latest.projectBindings)
|
|
2144
|
-
? latest
|
|
2145
|
-
: { ...latest, projectBindings: pinnedBindings, updatedAt: timestamp.toISOString() };
|
|
2146
|
-
persistedTask = bindTaskWorkspaceIdentity(persistedTask, identity, timestamp);
|
|
2147
|
-
if (persistedTask.cwd !== root) {
|
|
2148
|
-
persistedTask = { ...persistedTask, cwd: root, updatedAt: timestamp.toISOString() };
|
|
2149
|
-
}
|
|
2150
|
-
if (!isDeepStrictEqual(persistedTask, latest)) {
|
|
2151
|
-
tx.saveTask(persistedTask);
|
|
2152
|
-
}
|
|
2153
|
-
if (existing !== null)
|
|
2154
|
-
tx.removeManagedWorkspace(existing.owner);
|
|
2155
|
-
tx.saveManagedWorkspace(workspace);
|
|
2156
|
-
});
|
|
2157
|
-
// Record switched: retire the legacy layout. The worktrees leave first
|
|
2158
|
-
// (their branches are retained), then the refs are archived and
|
|
2159
|
-
// deleted; each step is resumable.
|
|
2160
|
-
await this.#removeLegacyWorktrees(task, existing === null
|
|
2161
|
-
? this.store.listProjects().map((project) => project.id)
|
|
2162
|
-
: existing.entries.map(({ projectId }) => projectId));
|
|
2163
|
-
const archived = await this.#archiveLegacyRefs(task, existing === null ? undefined : [...new Set(existing.entries.map(({ projectId }) => projectId))]);
|
|
2164
|
-
return { task: requireTask(this.store, task.id), archived, resumed: false };
|
|
2165
|
-
}
|
|
2166
|
-
catch (error) {
|
|
2167
|
-
await this.#discardUnadoptedEntries(task, taskSegment, prepared, MAIN_WORKTREE, true);
|
|
2168
|
-
throw error;
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
/**
|
|
2172
|
-
* List legacy (pre-identity) Task refs across the Home's Project
|
|
2173
|
-
* repositories, optionally restricted to one Task. Identity-bearing
|
|
2174
|
-
* branches (`yui/task-N-<8hex>/...`) are never legacy.
|
|
2175
|
-
*/
|
|
2176
|
-
async listLegacyTaskRefs(taskId) {
|
|
2177
|
-
const found = [];
|
|
2178
|
-
for (const project of this.store.listProjects()) {
|
|
2179
|
-
let refs;
|
|
2180
|
-
try {
|
|
2181
|
-
refs = await this.git.listRefs(project.path, "refs/heads/yui/");
|
|
2182
|
-
}
|
|
2183
|
-
catch (error) {
|
|
2184
|
-
// A deleted external checkout has no refs to scan; skip it.
|
|
2185
|
-
if (isMissingPath(error))
|
|
2186
|
-
continue;
|
|
2187
|
-
throw error;
|
|
2188
|
-
}
|
|
2189
|
-
for (const ref of refs) {
|
|
2190
|
-
if (!isLegacyTaskRef(ref))
|
|
2191
|
-
continue;
|
|
2192
|
-
const ownerTaskId = ref.slice("refs/heads/yui/".length).split("/")[0];
|
|
2193
|
-
if (taskId !== undefined && ownerTaskId !== taskId)
|
|
2194
|
-
continue;
|
|
2195
|
-
found.push({ projectId: project.id, taskId: ownerTaskId, ref });
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
return found;
|
|
2199
|
-
}
|
|
2200
|
-
/**
|
|
2201
|
-
* Archive legacy Task refs into the Home-scoped archive namespace
|
|
2202
|
-
* (`refs/yui/archive/<homeId>/heads/...`). Refs owned by a draft/active
|
|
2203
|
-
* Task are refused: their worktrees may still be live. Terminal and
|
|
2204
|
-
* unknown-owner refs are archived; an already-archived or missing ref is
|
|
2205
|
-
* simply absent on retry.
|
|
2206
|
-
*
|
|
2207
|
-
* Every registered worktree on a to-be-archived ref is preflighted before
|
|
2208
|
-
* the first side effect: a dirty or unidentifiable one fails the whole
|
|
2209
|
-
* archive with nothing created, deleted, or removed. A same-repo worktree
|
|
2210
|
-
* this Home does not manage that still has the ref checked out fails the
|
|
2211
|
-
* archive closed as well, so deleting the ref cannot strand it.
|
|
2212
|
-
*/
|
|
2213
|
-
async archiveLegacyTaskRefs(taskId) {
|
|
2214
|
-
const home = this.store.getHomeIdentity();
|
|
2215
|
-
const refused = [];
|
|
2216
|
-
const archived = [];
|
|
2217
|
-
// The pre-lock scan discovers ONLY the sorted Project lock set. The pending
|
|
2218
|
-
// set is rebuilt under the fence (below) by re-listing refs and
|
|
2219
|
-
// re-classifying their owners, so a Task that transitions terminal ->
|
|
2220
|
-
// active between the scan and the lock is refused rather than archived.
|
|
2221
|
-
const scanned = await this.listLegacyTaskRefs(taskId);
|
|
2222
|
-
const projectIds = [...new Set(scanned.map(({ projectId }) => projectId))].sort();
|
|
2223
|
-
// The fence covers preflight and archive alike: a concurrent rebuild
|
|
2224
|
-
// must not remove a worktree between its inspection and its ref's
|
|
2225
|
-
// archival.
|
|
2226
|
-
const releaseMaintenance = projectIds.length === 0
|
|
2227
|
-
? () => { }
|
|
2228
|
-
: acquireProjectMaintenanceLocks(this.home, projectIds);
|
|
2229
|
-
try {
|
|
2230
|
-
const locked = new Set(projectIds);
|
|
2231
|
-
const pending = [];
|
|
2232
|
-
for (const entry of await this.listLegacyTaskRefs(taskId)) {
|
|
2233
|
-
if (!locked.has(entry.projectId))
|
|
2234
|
-
continue; // added after the pre-lock scan; not fenced.
|
|
2235
|
-
const owner = this.store.getTask(entry.taskId);
|
|
2236
|
-
if (owner !== null && ["draft", "active"].includes(owner.status)) {
|
|
2237
|
-
refused.push(`${entry.projectId}:${entry.ref}`);
|
|
2238
|
-
continue;
|
|
2239
|
-
}
|
|
2240
|
-
pending.push({
|
|
2241
|
-
project: requireProject(this.store, entry.projectId),
|
|
2242
|
-
taskId: entry.taskId,
|
|
2243
|
-
ref: entry.ref,
|
|
2244
|
-
archiveRef: taskArchiveRef(home.homeId, entry.ref)
|
|
2245
|
-
});
|
|
2246
|
-
}
|
|
2247
|
-
for (const target of pending) {
|
|
2248
|
-
await this.#assertLegacyRefWorktreeArchivable(target);
|
|
2249
|
-
}
|
|
2250
|
-
for (const target of pending) {
|
|
2251
|
-
// Re-validate under the fence: a Task reopened after classification
|
|
2252
|
-
// but before this point must not have its ref deleted.
|
|
2253
|
-
const current = this.store.getTask(target.taskId);
|
|
2254
|
-
if (current !== null && ["draft", "active"].includes(current.status)) {
|
|
2255
|
-
refused.push(`${target.project.id}:${target.ref}`);
|
|
2256
|
-
continue;
|
|
2257
|
-
}
|
|
2258
|
-
await this.#archiveLegacyRef(target);
|
|
2259
|
-
archived.push(`${target.project.id}:${target.ref}`);
|
|
2260
|
-
}
|
|
2261
|
-
return { archived, refused };
|
|
2262
|
-
}
|
|
2263
|
-
finally {
|
|
2264
|
-
releaseMaintenance();
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
1974
|
async #inspectEntries(taskSegment, roleName, entries) {
|
|
2268
1975
|
if (entries.length === 0)
|
|
2269
1976
|
return "missing";
|
|
@@ -2363,225 +2070,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
2363
2070
|
this.#resourceRegistrar().markPathsDeleted([entry.path]);
|
|
2364
2071
|
}
|
|
2365
2072
|
}
|
|
2366
|
-
/**
|
|
2367
|
-
* Archive one Task's legacy refs (`refs/heads/yui/<taskId>/...`) into the
|
|
2368
|
-
* Home-scoped archive namespace across the given Project repositories. When
|
|
2369
|
-
* no Project list is given, every Project in the Home is scanned, so the
|
|
2370
|
-
* resume path also finds refs in Projects that left the Task binding.
|
|
2371
|
-
*/
|
|
2372
|
-
async #archiveLegacyRefs(task, projectIds) {
|
|
2373
|
-
const home = this.store.getHomeIdentity();
|
|
2374
|
-
const projects = projectIds === undefined
|
|
2375
|
-
? this.store.listProjects()
|
|
2376
|
-
: projectIds.map((id) => requireProject(this.store, id));
|
|
2377
|
-
const seen = new Set();
|
|
2378
|
-
const archived = [];
|
|
2379
|
-
for (const project of projects) {
|
|
2380
|
-
let refs;
|
|
2381
|
-
try {
|
|
2382
|
-
refs = await this.git.listRefs(project.path, `refs/heads/yui/${task.id}/`);
|
|
2383
|
-
}
|
|
2384
|
-
catch (error) {
|
|
2385
|
-
// A deleted external checkout has no refs to archive; skip it.
|
|
2386
|
-
if (isMissingPath(error))
|
|
2387
|
-
continue;
|
|
2388
|
-
throw error;
|
|
2389
|
-
}
|
|
2390
|
-
for (const ref of refs) {
|
|
2391
|
-
const ownerTaskId = ref.slice("refs/heads/yui/".length).split("/")[0];
|
|
2392
|
-
if (ownerTaskId !== task.id || !isLegacyTaskRef(ref))
|
|
2393
|
-
continue;
|
|
2394
|
-
const key = `${project.id}:${ref}`;
|
|
2395
|
-
if (seen.has(key))
|
|
2396
|
-
continue;
|
|
2397
|
-
seen.add(key);
|
|
2398
|
-
await this.#archiveLegacyRef({
|
|
2399
|
-
project,
|
|
2400
|
-
taskId: task.id,
|
|
2401
|
-
ref,
|
|
2402
|
-
archiveRef: taskArchiveRef(home.homeId, ref)
|
|
2403
|
-
});
|
|
2404
|
-
archived.push(key);
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
return archived;
|
|
2408
|
-
}
|
|
2409
|
-
/**
|
|
2410
|
-
* The recorded worktree of a legacy Task ref, when the ref is the Task's
|
|
2411
|
-
* main branch. The legacy layout registers exactly one worktree per Task
|
|
2412
|
-
* and Project (`<taskId>/main` on `yui/<taskId>/main`); every other legacy
|
|
2413
|
-
* ref has no registered worktree and is archived directly.
|
|
2414
|
-
*/
|
|
2415
|
-
#legacyRefWorktree(target) {
|
|
2416
|
-
const identity = worktreeIdentity(target.taskId, MAIN_WORKTREE);
|
|
2417
|
-
if (target.ref !== `refs/heads/${identity.branch}`)
|
|
2418
|
-
return undefined;
|
|
2419
|
-
const container = this.#projectContainer(target.project.name);
|
|
2420
|
-
return {
|
|
2421
|
-
repositoryPath: target.project.path,
|
|
2422
|
-
container,
|
|
2423
|
-
path: join(container, target.taskId, MAIN_WORKTREE),
|
|
2424
|
-
branch: identity.branch,
|
|
2425
|
-
taskSegment: target.taskId,
|
|
2426
|
-
roleName: MAIN_WORKTREE
|
|
2427
|
-
};
|
|
2428
|
-
}
|
|
2429
|
-
/**
|
|
2430
|
-
* Preflight the worktree of a to-be-archived ref: a registered worktree
|
|
2431
|
-
* must be clean (or absent) before the archive may touch any ref. An
|
|
2432
|
-
* unidentifiable worktree makes `inspectRecordedWorktree` throw, which
|
|
2433
|
-
* likewise fails the archive closed.
|
|
2434
|
-
*/
|
|
2435
|
-
async #assertLegacyRefWorktreeArchivable(target) {
|
|
2436
|
-
const worktree = this.#legacyRefWorktree(target);
|
|
2437
|
-
if (worktree === undefined)
|
|
2438
|
-
return;
|
|
2439
|
-
const state = await this.git.inspectRecordedWorktree(worktree);
|
|
2440
|
-
if (state === "dirty") {
|
|
2441
|
-
throw new Error(`Legacy Task worktree is dirty and blocks the archive: ${target.taskId}/${target.project.id}.`);
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
/**
|
|
2445
|
-
* Archive one legacy ref after removing its registered worktree. The
|
|
2446
|
-
* worktree leaves first (its commit retained in the archive ref), then the
|
|
2447
|
-
* active ref is deleted; a dirty worktree fails the ref closed. Each step
|
|
2448
|
-
* is resumable: a same-commit archive ref resumes, a missing worktree and
|
|
2449
|
-
* an already-deleted source are no-ops.
|
|
2450
|
-
*
|
|
2451
|
-
* Before any mutation, a same-repo worktree this Home does not manage that
|
|
2452
|
-
* still has the ref checked out fails the archive closed: `archiveRef`
|
|
2453
|
-
* deletes the ref with `update-ref -d`, which bypasses git's
|
|
2454
|
-
* worktree-occupancy check and would strand that worktree with an unborn
|
|
2455
|
-
* HEAD. The recorded worktree removed below is excluded from the check.
|
|
2456
|
-
*/
|
|
2457
|
-
async #archiveLegacyRef(target) {
|
|
2458
|
-
const worktree = this.#legacyRefWorktree(target);
|
|
2459
|
-
await this.git.assertNoForeignWorktreeOnRef({
|
|
2460
|
-
repositoryPath: target.project.path,
|
|
2461
|
-
ref: target.ref,
|
|
2462
|
-
excludeWorktreePath: worktree?.path
|
|
2463
|
-
});
|
|
2464
|
-
if (worktree !== undefined) {
|
|
2465
|
-
const removal = await this.git.removeRecordedWorktree({
|
|
2466
|
-
...worktree,
|
|
2467
|
-
retainedRef: target.archiveRef
|
|
2468
|
-
});
|
|
2469
|
-
if (removal === "dirty") {
|
|
2470
|
-
throw new Error(`Legacy Task worktree is dirty and blocks the archive: ${target.taskId}/${target.project.id}.`);
|
|
2471
|
-
}
|
|
2472
|
-
this.#resourceRegistrar().markPathsDeleted([worktree.path]);
|
|
2473
|
-
}
|
|
2474
|
-
await this.git.archiveRef({
|
|
2475
|
-
repositoryPath: target.project.path,
|
|
2476
|
-
sourceRef: target.ref,
|
|
2477
|
-
archiveRef: target.archiveRef
|
|
2478
|
-
});
|
|
2479
|
-
}
|
|
2480
|
-
/**
|
|
2481
|
-
* Remove the legacy worktrees of a Task (the bare `<taskId>/main` layout).
|
|
2482
|
-
* A missing worktree is expected on retry and ignored; a dirty one blocks.
|
|
2483
|
-
*/
|
|
2484
|
-
async #removeLegacyWorktrees(task, projectIds) {
|
|
2485
|
-
for (const projectId of projectIds) {
|
|
2486
|
-
const project = requireProject(this.store, projectId);
|
|
2487
|
-
const container = this.#projectContainer(project.name);
|
|
2488
|
-
const branch = worktreeIdentity(task.id, MAIN_WORKTREE).branch;
|
|
2489
|
-
const removal = await this.git.removeRecordedWorktree({
|
|
2490
|
-
repositoryPath: project.path,
|
|
2491
|
-
container,
|
|
2492
|
-
path: join(container, task.id, MAIN_WORKTREE),
|
|
2493
|
-
branch,
|
|
2494
|
-
retainedRef: taskArchiveRef(this.store.getHomeIdentity().homeId, `refs/heads/${branch}`),
|
|
2495
|
-
taskSegment: task.id,
|
|
2496
|
-
roleName: MAIN_WORKTREE
|
|
2497
|
-
});
|
|
2498
|
-
if (removal === "dirty") {
|
|
2499
|
-
throw new Error(`Legacy Task worktree is dirty and blocks the rebuild: ${task.id}/${project.id}.`);
|
|
2500
|
-
}
|
|
2501
|
-
this.#resourceRegistrar().markPathsDeleted([join(container, task.id, MAIN_WORKTREE)]);
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
/**
|
|
2505
|
-
* Reclaim token worktrees of this Task that no catalog record owns. A
|
|
2506
|
-
* crashed prepare or rebuild (SIGKILL before the catalog transaction) leaves
|
|
2507
|
-
* a token-bearing worktree and branch behind; a retry mints a fresh token,
|
|
2508
|
-
* so the old one is orphaned. The resume path scans this Home's project
|
|
2509
|
-
* containers for `task-N-<token>` directories and removes any the catalog
|
|
2510
|
-
* no longer owns, reusing the recorded-worktree mechanism: prove the exact
|
|
2511
|
-
* identity, retain the commit in the Home archive, remove the worktree, then
|
|
2512
|
-
* archive and delete its now-unchecked-out branch.
|
|
2513
|
-
*
|
|
2514
|
-
* Only this Home's project containers are scanned, and every candidate must
|
|
2515
|
-
* pass the exact recorded-worktree proof (its branch retained in this
|
|
2516
|
-
* Home's Project repository) before it is removed. A deleted external
|
|
2517
|
-
* checkout orphans the worktree directory, which `removeRecordedWorktree`
|
|
2518
|
-
* removes outright; its branch is gone with the repository and needs no
|
|
2519
|
-
* further action.
|
|
2520
|
-
*/
|
|
2521
|
-
async #reclaimOrphanedTaskWorktrees(task) {
|
|
2522
|
-
const cataloged = new Set(this.store.listManagedWorkspaces(task.id)
|
|
2523
|
-
.flatMap((workspace) => workspace.entries.map(({ path }) => path)));
|
|
2524
|
-
const homeId = this.store.getHomeIdentity().homeId;
|
|
2525
|
-
for (const project of this.store.listProjects()) {
|
|
2526
|
-
const container = this.#projectContainer(project.name);
|
|
2527
|
-
for (const segment of await listTaskTokenSegments(container, task.id)) {
|
|
2528
|
-
const worktreePath = join(container, segment, MAIN_WORKTREE);
|
|
2529
|
-
if (cataloged.has(worktreePath))
|
|
2530
|
-
continue;
|
|
2531
|
-
const branch = worktreeIdentity(segment, MAIN_WORKTREE).branch;
|
|
2532
|
-
const archiveRef = taskArchiveRef(homeId, `refs/heads/${branch}`);
|
|
2533
|
-
const removal = await this.git.removeRecordedWorktree({
|
|
2534
|
-
repositoryPath: project.path,
|
|
2535
|
-
container,
|
|
2536
|
-
path: worktreePath,
|
|
2537
|
-
branch,
|
|
2538
|
-
retainedRef: archiveRef,
|
|
2539
|
-
taskSegment: segment,
|
|
2540
|
-
roleName: MAIN_WORKTREE
|
|
2541
|
-
});
|
|
2542
|
-
if (removal === "dirty") {
|
|
2543
|
-
throw new Error(`Orphaned Task worktree is dirty and blocks the rebuild: ${task.id}/${project.id}.`);
|
|
2544
|
-
}
|
|
2545
|
-
this.#resourceRegistrar().markPathsDeleted([worktreePath]);
|
|
2546
|
-
// The worktree is gone; archive+delete its now-unchecked-out branch.
|
|
2547
|
-
// A deleted external checkout takes the branch with it, so a missing
|
|
2548
|
-
// repository or an already-absent branch is a no-op.
|
|
2549
|
-
let branchExists = false;
|
|
2550
|
-
try {
|
|
2551
|
-
branchExists = await this.git.refExists(project.path, `refs/heads/${branch}`);
|
|
2552
|
-
}
|
|
2553
|
-
catch (error) {
|
|
2554
|
-
if (!isMissingPath(error))
|
|
2555
|
-
throw error;
|
|
2556
|
-
}
|
|
2557
|
-
if (branchExists) {
|
|
2558
|
-
await this.git.archiveRef({
|
|
2559
|
-
repositoryPath: project.path,
|
|
2560
|
-
sourceRef: `refs/heads/${branch}`,
|
|
2561
|
-
archiveRef
|
|
2562
|
-
});
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
async #inspectLegacyTaskEntries(task, entries) {
|
|
2568
|
-
let found = false;
|
|
2569
|
-
for (const entry of entries) {
|
|
2570
|
-
const project = requireProject(this.store, entry.projectId);
|
|
2571
|
-
const state = await this.git.inspectRecordedWorktree({
|
|
2572
|
-
repositoryPath: project.path,
|
|
2573
|
-
container: this.#projectContainer(project.name),
|
|
2574
|
-
path: entry.path,
|
|
2575
|
-
branch: entry.branch,
|
|
2576
|
-
taskSegment: task.id,
|
|
2577
|
-
roleName: MAIN_WORKTREE
|
|
2578
|
-
});
|
|
2579
|
-
if (state === "dirty")
|
|
2580
|
-
return state;
|
|
2581
|
-
found ||= state === "clean";
|
|
2582
|
-
}
|
|
2583
|
-
return found ? "clean" : "missing";
|
|
2584
|
-
}
|
|
2585
2073
|
#recordWorkspaceRemoval(task, workspace, fallback, workItem) {
|
|
2586
2074
|
this.store.transaction((tx) => {
|
|
2587
2075
|
if (workspace.owner.type === "task") {
|
|
@@ -2649,19 +2137,6 @@ function assertTaskArchiveState(current, expected) {
|
|
|
2649
2137
|
throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
|
|
2650
2138
|
}
|
|
2651
2139
|
}
|
|
2652
|
-
/**
|
|
2653
|
-
* A controlled rebuild is only safe for a Task that owns no delivery
|
|
2654
|
-
* evidence: a Run, WorkItem, ChangeSet, or Integration would pin the old
|
|
2655
|
-
* refs and worktrees as historical proof.
|
|
2656
|
-
*/
|
|
2657
|
-
function assertTaskHasNoEvidence(store, taskId) {
|
|
2658
|
-
if (store.listAgentRuns(taskId).length > 0
|
|
2659
|
-
|| store.listWorkItems(taskId).length > 0
|
|
2660
|
-
|| store.listChangeSets(taskId).length > 0
|
|
2661
|
-
|| store.listIntegrationAttempts(taskId).length > 0) {
|
|
2662
|
-
throw new Error(`Task has Run, Work item, Change set, or Integration evidence and cannot be rebuilt in place: ${taskId}.`);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
2140
|
function requireProject(store, projectId) {
|
|
2666
2141
|
const project = store.getProject(projectId);
|
|
2667
2142
|
if (project === null)
|
|
@@ -2690,9 +2165,9 @@ function executionLaneLineage(store, task, executionGroupId, executionLaneId, hi
|
|
|
2690
2165
|
throw new Error(`ReviewRound not found: ${hint.reviewRoundId}.`);
|
|
2691
2166
|
return { purpose: "review", reviewRoundId: round.id };
|
|
2692
2167
|
}
|
|
2693
|
-
// Prefer the active
|
|
2694
|
-
// first queued WorkItem only when no active
|
|
2695
|
-
const activeLaneRun = store.
|
|
2168
|
+
// Prefer the active Turn's exact WorkItem for this Lane; fall back to the
|
|
2169
|
+
// first queued WorkItem only when no active Turn owns the Lane.
|
|
2170
|
+
const activeLaneRun = store.listTurns(task.id)
|
|
2696
2171
|
.find((run) => run.status === "active"
|
|
2697
2172
|
&& run.purpose === "execution"
|
|
2698
2173
|
&& run.executionGroupId === executionGroupId
|
|
@@ -2720,13 +2195,13 @@ function assertWorkItemWorkspaceEligible(store, task, item) {
|
|
|
2720
2195
|
if (["completed", "failed", "retired"].includes(item.status)) {
|
|
2721
2196
|
throw new Error(`Work item is already terminal: ${item.id}.`);
|
|
2722
2197
|
}
|
|
2723
|
-
const
|
|
2198
|
+
const activeDevelopTurn = store.listTurns(task.id)
|
|
2724
2199
|
.find((run) => run.status === "active" && run.workItemId === item.id);
|
|
2725
|
-
if (
|
|
2726
|
-
throw new Error(`Work Item already has an active Develop
|
|
2200
|
+
if (activeDevelopTurn !== undefined) {
|
|
2201
|
+
throw new Error(`Work Item already has an active Develop Turn: ${activeDevelopTurn.id}.`);
|
|
2727
2202
|
}
|
|
2728
|
-
if (item.assignee !== undefined && store.
|
|
2729
|
-
throw new Error(`Role has an active
|
|
2203
|
+
if (item.assignee !== undefined && store.getActiveTurn(task.id, item.assignee) !== null) {
|
|
2204
|
+
throw new Error(`Role has an active Turn: ${task.id}/${item.assignee}.`);
|
|
2730
2205
|
}
|
|
2731
2206
|
}
|
|
2732
2207
|
function assertWorkItemOwnsWorkspace(item, workspace) {
|
|
@@ -2746,16 +2221,16 @@ function preserveWorkspaceCreatedAt(workspace, previous) {
|
|
|
2746
2221
|
: { ...workspace, createdAt: previous.createdAt };
|
|
2747
2222
|
}
|
|
2748
2223
|
function assertWorkspaceSessionsRetirable(store, taskId, roleName, now) {
|
|
2749
|
-
if (store.
|
|
2750
|
-
throw new Error(`Role has an active
|
|
2224
|
+
if (store.getActiveTurn(taskId, roleName) !== null) {
|
|
2225
|
+
throw new Error(`Role has an active Turn: ${taskId}/${roleName}.`);
|
|
2751
2226
|
}
|
|
2752
2227
|
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
2753
2228
|
if (sessions !== null)
|
|
2754
2229
|
retireTaskRoleSessionsForWorkspace(sessions, now);
|
|
2755
2230
|
}
|
|
2756
2231
|
function retireWorkspaceBoundSession(store, taskId, roleName, now) {
|
|
2757
|
-
if (store.
|
|
2758
|
-
throw new Error(`Role has an active
|
|
2232
|
+
if (store.getActiveTurn(taskId, roleName) !== null) {
|
|
2233
|
+
throw new Error(`Role has an active Turn: ${taskId}/${roleName}.`);
|
|
2759
2234
|
}
|
|
2760
2235
|
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
2761
2236
|
if (sessions !== null) {
|
|
@@ -2777,7 +2252,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
|
|
|
2777
2252
|
.sort();
|
|
2778
2253
|
if (!isDeepStrictEqual(writableProjects, [...item.writeProjectIds].sort()))
|
|
2779
2254
|
return false;
|
|
2780
|
-
const run = store.
|
|
2255
|
+
const run = store.getActiveTurn(taskId, role.name);
|
|
2781
2256
|
if (run === null
|
|
2782
2257
|
|| run.status !== "active"
|
|
2783
2258
|
|| run.purpose !== "execution"
|
|
@@ -2794,10 +2269,6 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
|
|
|
2794
2269
|
|| sessions.owner.taskId !== taskId
|
|
2795
2270
|
|| sessions.owner.roleName !== role.name
|
|
2796
2271
|
|| sessions.activeAgentId !== role.activeAgentId
|
|
2797
|
-
|| sessions.inFlight === null
|
|
2798
|
-
|| sessions.inFlight.agentId !== role.activeAgentId
|
|
2799
|
-
|| sessions.inFlight.runId !== run.id
|
|
2800
|
-
|| sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)
|
|
2801
2272
|
|| session === undefined
|
|
2802
2273
|
|| session.agentId !== role.activeAgentId
|
|
2803
2274
|
|| session.adapterId !== run.effective.adapterId
|
|
@@ -2820,7 +2291,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
|
|
|
2820
2291
|
&& isRuntimeLaunchReservation(lifecycle)) {
|
|
2821
2292
|
const executionRef = lifecycle.executionRef;
|
|
2822
2293
|
if (!isRuntimeLaunchReservation(lifecycle, session.launchId)
|
|
2823
|
-
|| executionRef?.type !== "
|
|
2294
|
+
|| executionRef?.type !== "turn"
|
|
2824
2295
|
|| executionRef.taskId !== taskId
|
|
2825
2296
|
|| executionRef.id !== run.id)
|
|
2826
2297
|
return false;
|
|
@@ -2838,34 +2309,6 @@ function isMissingPath(error) {
|
|
|
2838
2309
|
return typeof error === "object" && error !== null && "code" in error
|
|
2839
2310
|
&& error.code === "ENOENT";
|
|
2840
2311
|
}
|
|
2841
|
-
/**
|
|
2842
|
-
* The token-bearing ref segments (`task-N-<8hex>`) under a Project's worktree
|
|
2843
|
-
* container for one Task. A missing container yields no segments; legacy
|
|
2844
|
-
* (`task-N`) and foreign-Task directories are ignored.
|
|
2845
|
-
*/
|
|
2846
|
-
async function listTaskTokenSegments(container, taskId) {
|
|
2847
|
-
let entries;
|
|
2848
|
-
try {
|
|
2849
|
-
entries = await readdir(container, { withFileTypes: true });
|
|
2850
|
-
}
|
|
2851
|
-
catch (error) {
|
|
2852
|
-
if (isMissingPath(error))
|
|
2853
|
-
return [];
|
|
2854
|
-
throw error;
|
|
2855
|
-
}
|
|
2856
|
-
const prefix = `${taskId}-`;
|
|
2857
|
-
const segments = [];
|
|
2858
|
-
for (const entry of entries) {
|
|
2859
|
-
if (!entry.isDirectory())
|
|
2860
|
-
continue;
|
|
2861
|
-
if (!entry.name.startsWith(prefix))
|
|
2862
|
-
continue;
|
|
2863
|
-
if (!TASK_WORKSPACE_TOKEN_PATTERN.test(entry.name.slice(prefix.length)))
|
|
2864
|
-
continue;
|
|
2865
|
-
segments.push(entry.name);
|
|
2866
|
-
}
|
|
2867
|
-
return segments.sort();
|
|
2868
|
-
}
|
|
2869
2312
|
function recordWorkspaceDisposition(store, taskId, workItem, now) {
|
|
2870
2313
|
const item = store.getWorkItem(taskId, workItem.workItemId);
|
|
2871
2314
|
if (item === null)
|