@zq-silk/yui 0.15.11 → 0.16.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 +11 -6
- package/ARCHITECTURE.zh-CN.md +8 -4
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +92 -48
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +11 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +2 -2
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1303 -1235
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +78 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +45 -25
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +355 -732
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +20 -58
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +12 -3
- package/dist/commands/taskPublicationVerifyCommand.js +24 -40
- package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +5 -3
- package/dist/context/taskCatalog.js +185 -0
- package/dist/context/taskContext.js +152 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +20 -24
- package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +85 -43
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +79 -90
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +5 -10
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +98 -82
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +82 -144
- package/dist/repository/taskWorkspacePreparer.js +113 -47
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +42 -25
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +10 -43
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +19 -7
- package/dist/scheduler/leaderWakeupProcessor.js +24 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +49 -36
- package/dist/scheduler/taskObservabilityProjection.js +6 -45
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +173 -1
- package/dist/storage/sqliteStore.js +172 -161
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +16 -7
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +5 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +6 -5
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +12 -3
- package/dist/web/webSnapshot.js +14 -85
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +42 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
- package/docs/managed-turn-and-session-runtime.md +121 -1
- package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/plugin-sdk.md +4 -2
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +223 -17
- package/docs/task-delivery.zh-CN.md +171 -14
- package/docs/task-discovery.md +101 -0
- package/docs/task-discovery.zh-CN.md +85 -0
- package/docs/testing/verification-levels.md +161 -9
- package/docs/testing/verification-levels.zh-CN.md +116 -9
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +24 -8
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +71 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -376
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
import { lstat, mkdir,
|
|
1
|
+
import { lstat, mkdir, readdir, readlink, rm, rmdir, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
4
|
import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
5
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
6
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
5
7
|
import { retireTaskRoleSessionsForWorkspace, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
6
8
|
import { formatWorkspacePreflightError } from "../executor/workspacePreflightClassification.js";
|
|
9
|
+
import { createProjectResources } from "../resources/projectResourceService.js";
|
|
10
|
+
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
11
|
+
import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
|
|
7
12
|
import { updateRole } from "../role/role.js";
|
|
13
|
+
import { hasRuntimeCleanupObligation, RUNTIME_HOST_DETACH_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
8
14
|
import { taskRoleRuntimeIdentity } from "../runtime/managedCaller.js";
|
|
9
|
-
import {
|
|
10
|
-
import { taskLocalActor } from "../commands/taskActor.js";
|
|
11
|
-
import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
|
|
15
|
+
import { managedTaskRoot } from "../storage/homeLayout.js";
|
|
12
16
|
import { StorageConflictError } from "../storage/taskStore.js";
|
|
13
|
-
import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
|
|
14
|
-
import { admitStoredTaskActivation, adoptTaskActivationResources, recordFailedTaskActivation, recordAdoptedTaskActivation } from "../task/taskActivationService.js";
|
|
15
17
|
import { validateDraftTaskForActivation } from "../task/draftPlan.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot,
|
|
18
|
+
import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
|
|
19
|
+
import { admitStoredTaskActivation, adoptTaskActivationResources, recordAdoptedTaskActivation, recordFailedTaskActivation } from "../task/taskActivationService.js";
|
|
20
|
+
import { taskLocalActor } from "../task/taskAuthority.js";
|
|
21
|
+
import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, recordWorkItemWorkspaceDisposition, workItemExecutionGroupById } from "../workItem/workItem.js";
|
|
22
|
+
import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
|
|
20
23
|
import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
21
24
|
import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
|
|
22
|
-
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
23
|
-
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
24
|
-
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
25
|
+
import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockCancelledError, ProjectMaintenanceLockedError } from "./projectMaintenanceLock.js";
|
|
25
26
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
26
|
-
import {
|
|
27
|
+
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
28
|
+
import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
|
|
27
29
|
const MAIN_WORKTREE = "main";
|
|
28
30
|
const LEADER_ROLE = "leader";
|
|
29
31
|
/**
|
|
@@ -62,11 +64,21 @@ export class FileTaskWorkspacePreparer {
|
|
|
62
64
|
store;
|
|
63
65
|
git;
|
|
64
66
|
now;
|
|
65
|
-
|
|
67
|
+
maintenanceLockOptions;
|
|
68
|
+
constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), maintenanceLockOptions = {}) {
|
|
66
69
|
this.home = home;
|
|
67
70
|
this.store = store;
|
|
68
71
|
this.git = git;
|
|
69
72
|
this.now = now;
|
|
73
|
+
this.maintenanceLockOptions = maintenanceLockOptions;
|
|
74
|
+
}
|
|
75
|
+
inspectWorkspaceCleanup(workspace, disposition, forceArchive = false) {
|
|
76
|
+
return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive);
|
|
77
|
+
}
|
|
78
|
+
async #assertWorkspaceCleanup(workspace, disposition) {
|
|
79
|
+
const checks = await this.inspectWorkspaceCleanup(workspace, disposition);
|
|
80
|
+
if (checks.length > 0)
|
|
81
|
+
throw new CleanupInspectionError(checks);
|
|
70
82
|
}
|
|
71
83
|
#resourceRegistrarValue;
|
|
72
84
|
#resourceRegistrar() {
|
|
@@ -75,7 +87,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
75
87
|
#registerWorkspace(workspace) {
|
|
76
88
|
this.#resourceRegistrar().registerManagedWorkspace(workspace);
|
|
77
89
|
}
|
|
78
|
-
async prepareTaskWorkspace(taskId) {
|
|
90
|
+
async prepareTaskWorkspace(taskId, signal) {
|
|
79
91
|
// The per-Project maintenance fence makes prepare mutually exclusive with
|
|
80
92
|
// Project maintenance and Task archive cleanup: a concurrent migration must not switch the
|
|
81
93
|
// Project catalog to the Home-managed repo while prepare is creating
|
|
@@ -87,7 +99,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
87
99
|
for (let attempt = 0;; attempt += 1) {
|
|
88
100
|
try {
|
|
89
101
|
const task = requireTask(this.store, taskId);
|
|
90
|
-
const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
102
|
+
const { release, current } = await this.#acquireTaskProjectMaintenanceLocks(task, signal);
|
|
91
103
|
try {
|
|
92
104
|
return await this.#prepareTaskWorkspaceLocked(current.id, false);
|
|
93
105
|
}
|
|
@@ -116,11 +128,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
116
128
|
* later launch failure can never present itself as "activation never
|
|
117
129
|
* happened".
|
|
118
130
|
*/
|
|
119
|
-
async activateTaskWorkspace(taskId, environment = {}) {
|
|
131
|
+
async activateTaskWorkspace(taskId, environment = {}, signal) {
|
|
120
132
|
for (let attempt = 0;; attempt += 1) {
|
|
121
133
|
let attemptedRequest;
|
|
122
134
|
try {
|
|
123
135
|
const task = requireTask(this.store, taskId);
|
|
136
|
+
if (signal?.aborted) {
|
|
137
|
+
throw new ProjectMaintenanceLockCancelledError(task.projectBindings[0]?.projectId ?? task.id, signal.reason);
|
|
138
|
+
}
|
|
124
139
|
const caller = taskLocalActor(this.store, environment, task.id);
|
|
125
140
|
const actor = task.activationRequest?.operation.actorId === `task:${task.id}/role:leader`
|
|
126
141
|
? "leader" : caller;
|
|
@@ -139,21 +154,27 @@ export class FileTaskWorkspacePreparer {
|
|
|
139
154
|
changed: false
|
|
140
155
|
};
|
|
141
156
|
}
|
|
157
|
+
const admission = admitStoredTaskActivation(this.store, task.id);
|
|
158
|
+
if (admission.disposition === "absent") {
|
|
159
|
+
throw new Error(`Task activation request is required: ${task.id}. `
|
|
160
|
+
+ `Use yui task activation request ${task.id} --request-id <id> --environment <plan> first.`);
|
|
161
|
+
}
|
|
142
162
|
const provider = this.store.getTaskRoleSessionSet(task.id, LEADER_ROLE)?.providerBinding;
|
|
143
163
|
if (provider?.run != null
|
|
144
164
|
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
|
|
145
165
|
throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
|
|
146
166
|
}
|
|
147
|
-
const admission = admitStoredTaskActivation(this.store, task.id);
|
|
148
167
|
if (admission.disposition === "ready")
|
|
149
168
|
attemptedRequest = admission.request;
|
|
150
169
|
validateDraftTaskForActivation(this.store, task);
|
|
151
|
-
|
|
152
|
-
return adopted.workspaceFree
|
|
153
|
-
? this.#activateWorkspaceFreeTask(task.id, adopted, actor)
|
|
154
|
-
: await this.#prepareActivatedTaskWorkspace(task, adopted, actor);
|
|
170
|
+
return await this.#prepareActivatedTaskWorkspace(task, actor, environment, signal);
|
|
155
171
|
}
|
|
156
172
|
catch (error) {
|
|
173
|
+
// Waiting did not execute the workspace operation. Preserve the
|
|
174
|
+
// activation request instead of turning contention/stop into failure.
|
|
175
|
+
if (error instanceof ProjectMaintenanceLockedError
|
|
176
|
+
|| error instanceof ProjectMaintenanceLockCancelledError)
|
|
177
|
+
throw error;
|
|
157
178
|
if (error instanceof StorageConflictError
|
|
158
179
|
&& attempt < TASK_WORKSPACE_PREPARE_MAX_CONFLICT_RETRIES) {
|
|
159
180
|
continue;
|
|
@@ -170,10 +191,28 @@ export class FileTaskWorkspacePreparer {
|
|
|
170
191
|
}
|
|
171
192
|
}
|
|
172
193
|
/** Prepares the managed workspace of a Task being activated, under its fences. */
|
|
173
|
-
async #prepareActivatedTaskWorkspace(task,
|
|
174
|
-
const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
194
|
+
async #prepareActivatedTaskWorkspace(task, actor, callerEnvironment, signal) {
|
|
195
|
+
const { release, current } = await this.#acquireTaskProjectMaintenanceLocks(task, signal);
|
|
175
196
|
try {
|
|
176
|
-
|
|
197
|
+
taskLocalActor(this.store, callerEnvironment, current.id);
|
|
198
|
+
if (current.executionGate.state !== "enabled") {
|
|
199
|
+
throw new Error(`Task execution is not enabled: ${current.id}.`);
|
|
200
|
+
}
|
|
201
|
+
if (!isDeepStrictEqual(current.activationRequest, task.activationRequest)) {
|
|
202
|
+
throw new Error(`Task activation request changed while waiting for Project maintenance: ${task.id}.`);
|
|
203
|
+
}
|
|
204
|
+
const provider = this.store.getTaskRoleSessionSet(current.id, LEADER_ROLE)?.providerBinding;
|
|
205
|
+
if (provider?.run != null
|
|
206
|
+
&& ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
|
|
207
|
+
throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
|
|
208
|
+
}
|
|
209
|
+
validateDraftTaskForActivation(this.store, current);
|
|
210
|
+
// Acquire before resource adoption: a timed-out/cancelled waiter must
|
|
211
|
+
// not leave an adopted environment to be repeated on the next attempt.
|
|
212
|
+
const adopted = await this.#adoptActivationEnvironment(current);
|
|
213
|
+
return adopted.workspaceFree
|
|
214
|
+
? this.#activateWorkspaceFreeTask(current.id, adopted, actor)
|
|
215
|
+
: await this.#prepareTaskWorkspaceLocked(current.id, true, adopted, actor);
|
|
177
216
|
}
|
|
178
217
|
finally {
|
|
179
218
|
release();
|
|
@@ -183,9 +222,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
183
222
|
* Prepares and adopts the resource configuration an explicit activation
|
|
184
223
|
* request named, before any status change.
|
|
185
224
|
*
|
|
186
|
-
* A
|
|
187
|
-
*
|
|
188
|
-
* binds no Project owns nothing physical, and is reported as workspace-free
|
|
225
|
+
* A request whose plan is empty and whose Task binds no Project owns nothing
|
|
226
|
+
* physical, and is reported as workspace-free
|
|
189
227
|
* so activation creates no directory at all.
|
|
190
228
|
*
|
|
191
229
|
* The exact request that was adopted travels with the result, so the later
|
|
@@ -194,8 +232,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
194
232
|
*/
|
|
195
233
|
async #adoptActivationEnvironment(task) {
|
|
196
234
|
const admission = admitStoredTaskActivation(this.store, task.id);
|
|
197
|
-
if (admission.disposition === "absent")
|
|
198
|
-
return { workspaceFree: false };
|
|
199
235
|
if (admission.disposition !== "ready") {
|
|
200
236
|
throw new Error(`Task activation is not adoptable: ${task.id}/${admission.disposition}`
|
|
201
237
|
+ (admission.disposition === "unavailable" ? ` (${admission.reason})` : "")
|
|
@@ -330,12 +366,19 @@ export class FileTaskWorkspacePreparer {
|
|
|
330
366
|
* caller re-reads the Task and re-acquires the correct fence set. The
|
|
331
367
|
* caller owns `release` and MUST call it on every exit path.
|
|
332
368
|
*/
|
|
333
|
-
#acquireTaskProjectMaintenanceLocks(task) {
|
|
369
|
+
async #acquireTaskProjectMaintenanceLocks(task, signal) {
|
|
334
370
|
const projectIds = task.projectBindings.map(({ projectId }) => projectId);
|
|
371
|
+
const waitSignal = signal ?? this.maintenanceLockOptions.signal;
|
|
335
372
|
const release = projectIds.length === 0
|
|
336
373
|
? () => { }
|
|
337
|
-
: acquireProjectMaintenanceLocks(this.home, projectIds
|
|
374
|
+
: await acquireProjectMaintenanceLocks(this.home, projectIds, {
|
|
375
|
+
...this.maintenanceLockOptions,
|
|
376
|
+
signal: waitSignal
|
|
377
|
+
});
|
|
338
378
|
try {
|
|
379
|
+
if (waitSignal?.aborted) {
|
|
380
|
+
throw new ProjectMaintenanceLockCancelledError(projectIds[0] ?? task.id, waitSignal.reason);
|
|
381
|
+
}
|
|
339
382
|
const current = requireTask(this.store, task.id);
|
|
340
383
|
const currentIds = current.projectBindings.map(({ projectId }) => projectId).sort();
|
|
341
384
|
const lockedIds = [...projectIds].sort();
|
|
@@ -358,13 +401,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
358
401
|
* catalog in that gap. The caller owns `release` and must call it on every
|
|
359
402
|
* exit path.
|
|
360
403
|
*/
|
|
361
|
-
acquireTaskProjectMaintenanceLocks(taskId) {
|
|
362
|
-
return this.#acquireTaskProjectMaintenanceLocks(requireTask(this.store, taskId));
|
|
404
|
+
acquireTaskProjectMaintenanceLocks(taskId, signal) {
|
|
405
|
+
return this.#acquireTaskProjectMaintenanceLocks(requireTask(this.store, taskId), signal);
|
|
363
406
|
}
|
|
364
407
|
async #prepareTaskWorkspaceLocked(taskId, activate,
|
|
365
408
|
/** Environment and request identity the activation adopted, when one exists. */
|
|
366
409
|
activationEnvironment = { workspaceFree: false }, actor = "user") {
|
|
367
410
|
const task = requireTask(this.store, taskId);
|
|
411
|
+
if (task.executionGate.state !== "enabled") {
|
|
412
|
+
throw new Error(`Task execution is not enabled: ${task.id}.`);
|
|
413
|
+
}
|
|
368
414
|
if (activate && task.status !== "draft") {
|
|
369
415
|
throw new Error(`Only a Draft Task can atomically adopt a workspace: ${task.id}/${task.status}.`);
|
|
370
416
|
}
|
|
@@ -910,15 +956,17 @@ export class FileTaskWorkspacePreparer {
|
|
|
910
956
|
return projects;
|
|
911
957
|
}
|
|
912
958
|
async prepareWorkItemWorkspace(taskId, workItemId) {
|
|
913
|
-
|
|
959
|
+
let item = requireWorkItem(this.store, taskId, workItemId);
|
|
914
960
|
const task = requireTask(this.store, item.taskId);
|
|
915
961
|
assertWorkItemWorkspaceEligible(this.store, task, item);
|
|
916
962
|
// Hold the per-Project maintenance fence across the ensure-main-prepared
|
|
917
963
|
// and the WorkItem worktree creation, so concurrent Project maintenance
|
|
918
964
|
// cannot switch the Project catalog between the two. The under-lock Task
|
|
919
965
|
// snapshot drives every Project read and Git effect below.
|
|
920
|
-
const { release, current: lockedTask } = this.#acquireTaskProjectMaintenanceLocks(task);
|
|
966
|
+
const { release, current: lockedTask } = await this.#acquireTaskProjectMaintenanceLocks(task);
|
|
921
967
|
try {
|
|
968
|
+
item = requireWorkItem(this.store, taskId, workItemId);
|
|
969
|
+
assertWorkItemWorkspaceEligible(this.store, lockedTask, item);
|
|
922
970
|
const lockedProjectIds = lockedTask.projectBindings.map(({ projectId }) => projectId);
|
|
923
971
|
await this.#prepareTaskWorkspaceWithRetries(lockedTask.id, lockedProjectIds);
|
|
924
972
|
// prepareTaskWorkspace may have just minted and persisted the workspace
|
|
@@ -1099,9 +1147,12 @@ export class FileTaskWorkspacePreparer {
|
|
|
1099
1147
|
// already holds (one locked boundary across preparation and adoption);
|
|
1100
1148
|
// otherwise the fence is acquired here.
|
|
1101
1149
|
const { release, current: lockedTask } = heldFence === undefined
|
|
1102
|
-
? this.#acquireTaskProjectMaintenanceLocks(task)
|
|
1150
|
+
? await this.#acquireTaskProjectMaintenanceLocks(task)
|
|
1103
1151
|
: { release: () => { }, current: heldFence.current };
|
|
1104
1152
|
try {
|
|
1153
|
+
if (lockedTask.status !== "active" || lockedTask.executionGate.state !== "enabled") {
|
|
1154
|
+
throw new Error(`Task is not open for Execution Lane preparation: ${lockedTask.id}.`);
|
|
1155
|
+
}
|
|
1105
1156
|
const lineage = executionLaneLineage(this.store, lockedTask, executionGroupId, executionLaneId, hint);
|
|
1106
1157
|
const item = lineage.purpose === "execution"
|
|
1107
1158
|
? this.store.getWorkItem(taskId, lineage.workItemId)
|
|
@@ -1269,6 +1320,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1269
1320
|
&& owner.executionLaneId === executionLaneId));
|
|
1270
1321
|
if (workspace === undefined)
|
|
1271
1322
|
return "missing";
|
|
1323
|
+
await this.#assertWorkspaceCleanup(workspace, "abandoned");
|
|
1272
1324
|
const state = await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
|
|
1273
1325
|
if (state === "dirty")
|
|
1274
1326
|
return "dirty";
|
|
@@ -1404,8 +1456,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
1404
1456
|
* never used as a ChangeSet capture source. */
|
|
1405
1457
|
async prepareReviewRoundWorkspace(taskId, reviewRoundId) {
|
|
1406
1458
|
const taskSnapshot = requireTask(this.store, taskId);
|
|
1407
|
-
const { release, current: task } = this.#acquireTaskProjectMaintenanceLocks(taskSnapshot);
|
|
1459
|
+
const { release, current: task } = await this.#acquireTaskProjectMaintenanceLocks(taskSnapshot);
|
|
1408
1460
|
try {
|
|
1461
|
+
if (task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1462
|
+
throw new Error(`Task is not open for ReviewRound preparation: ${task.id}.`);
|
|
1463
|
+
}
|
|
1409
1464
|
const taskSegment = this.#taskSegment(task);
|
|
1410
1465
|
const round = this.store.getReviewRound(task.id, reviewRoundId);
|
|
1411
1466
|
if (round === null)
|
|
@@ -1413,7 +1468,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1413
1468
|
if (round.status !== "pending") {
|
|
1414
1469
|
throw new Error(`ReviewRound workspace can only prepare while pending: ${round.id}.`);
|
|
1415
1470
|
}
|
|
1416
|
-
const taskScope =
|
|
1471
|
+
const taskScope = round.scope === "task";
|
|
1417
1472
|
const item = taskScope || round.workItemId === undefined
|
|
1418
1473
|
? undefined
|
|
1419
1474
|
: requireWorkItem(this.store, task.id, round.workItemId);
|
|
@@ -1716,7 +1771,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1716
1771
|
async #reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries) {
|
|
1717
1772
|
const previous = this.store.listReviewRounds(task.id)
|
|
1718
1773
|
.filter((candidate) => (candidate.id !== round.id
|
|
1719
|
-
&&
|
|
1774
|
+
&& candidate.scope === "task"
|
|
1720
1775
|
&& candidate.reviewerRoleName === round.reviewerRoleName
|
|
1721
1776
|
&& (candidate.status === "completed" || candidate.status === "failed")
|
|
1722
1777
|
&& candidate.workspaceDisposition?.kind !== "removed"
|
|
@@ -1878,6 +1933,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1878
1933
|
if (!isDeepStrictEqual(round.workspace, workspace)) {
|
|
1879
1934
|
throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
|
|
1880
1935
|
}
|
|
1936
|
+
await this.#assertWorkspaceCleanup(workspace, "abandoned");
|
|
1881
1937
|
assertWorkspaceSessionsRetirable(this.store, task.id, round.reviewerRoleName, this.now());
|
|
1882
1938
|
for (const lane of round.executionGroup?.lanes ?? []) {
|
|
1883
1939
|
assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
|
|
@@ -1945,6 +2001,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1945
2001
|
throw new Error(`WorkItem has no managed isolated worktree workspace: ${item.id}.`);
|
|
1946
2002
|
}
|
|
1947
2003
|
assertWorkItemOwnsWorkspace(item, workspace);
|
|
2004
|
+
await this.#assertWorkspaceCleanup(workspace, disposition);
|
|
1948
2005
|
if (item.assignee !== undefined) {
|
|
1949
2006
|
assertWorkspaceSessionsRetirable(this.store, task.id, item.assignee, this.now());
|
|
1950
2007
|
}
|
|
@@ -1998,7 +2055,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1998
2055
|
let found = false;
|
|
1999
2056
|
for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
|
|
2000
2057
|
try {
|
|
2001
|
-
if (!await this.git.
|
|
2058
|
+
if (!await this.git.inspectClean(entry.path))
|
|
2002
2059
|
return "dirty";
|
|
2003
2060
|
found = true;
|
|
2004
2061
|
}
|
|
@@ -2010,7 +2067,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2010
2067
|
}
|
|
2011
2068
|
return found ? "clean" : "missing";
|
|
2012
2069
|
}
|
|
2013
|
-
async cleanupIntegrationWorkspace(taskId, integrationId) {
|
|
2070
|
+
async cleanupIntegrationWorkspace(taskId, integrationId, options = {}) {
|
|
2014
2071
|
const task = requireTask(this.store, taskId);
|
|
2015
2072
|
const attempt = this.store.getIntegrationAttempt(task.id, integrationId);
|
|
2016
2073
|
if (attempt === null) {
|
|
@@ -2023,6 +2080,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
2023
2080
|
throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
|
|
2024
2081
|
}
|
|
2025
2082
|
const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
|
|
2083
|
+
// A failed Integration's explicitly disposable conflict tree retains its
|
|
2084
|
+
// existing cleanup contract outside archive. Archive never discards dirt.
|
|
2085
|
+
if (workspace !== null && (options.preserveChanges || task.status === "archived" || attempt.status !== "failed")) {
|
|
2086
|
+
await this.#assertWorkspaceCleanup(workspace, "abandoned");
|
|
2087
|
+
}
|
|
2026
2088
|
const project = requireProject(this.store, attempt.projectId);
|
|
2027
2089
|
// The recorded entry pins the exact on-disk worktree path; reconstruct the
|
|
2028
2090
|
// owner root from it so removal targets the real Git worktree regardless of
|
|
@@ -2042,7 +2104,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2042
2104
|
directory,
|
|
2043
2105
|
taskSegment: this.#taskSegment(task),
|
|
2044
2106
|
integrationId: attempt.id,
|
|
2045
|
-
discardChanges: task.status !== "archived" && attempt.status === "failed"
|
|
2107
|
+
discardChanges: options.preserveChanges !== true && task.status !== "archived" && attempt.status === "failed"
|
|
2046
2108
|
});
|
|
2047
2109
|
if (result !== "dirty") {
|
|
2048
2110
|
for (const entry of workspace?.entries ?? []) {
|
|
@@ -2067,9 +2129,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
2067
2129
|
if (main.owner.type !== "task") {
|
|
2068
2130
|
throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
|
|
2069
2131
|
}
|
|
2132
|
+
if (main.entries.length === 0)
|
|
2133
|
+
return "missing";
|
|
2070
2134
|
return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
|
|
2071
2135
|
}
|
|
2072
|
-
async cleanupTaskForArchive(taskId) {
|
|
2136
|
+
async cleanupTaskForArchive(taskId, disposition = "abandoned") {
|
|
2073
2137
|
const task = requireTask(this.store, taskId);
|
|
2074
2138
|
assertTaskArchiveState(task, task);
|
|
2075
2139
|
const isolated = this.store.listManagedWorkspaces(task.id)
|
|
@@ -2084,8 +2148,10 @@ export class FileTaskWorkspacePreparer {
|
|
|
2084
2148
|
}
|
|
2085
2149
|
const main = this.store.getTaskWorkspace(task.id);
|
|
2086
2150
|
if (main !== null) {
|
|
2151
|
+
await this.#assertWorkspaceCleanup(main, disposition);
|
|
2087
2152
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2088
|
-
|
|
2153
|
+
// Scratch-only workspaces have no Git entries or Git workspace identity.
|
|
2154
|
+
if (main.entries.length > 0 && await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
|
|
2089
2155
|
return {
|
|
2090
2156
|
taskId,
|
|
2091
2157
|
status: "retained-dirty",
|
|
@@ -2166,7 +2232,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2166
2232
|
return integrationWorkspaceRoot(this.home, taskId, integrationId);
|
|
2167
2233
|
}
|
|
2168
2234
|
#reviewWorktreeName(round) {
|
|
2169
|
-
return
|
|
2235
|
+
return round.scope === "task"
|
|
2170
2236
|
? `reviewer-${round.reviewerRoleName}`
|
|
2171
2237
|
: round.id;
|
|
2172
2238
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
|
|
4
|
+
import { cleanupCheckFromError } from "../workspace/cleanupInspection.js";
|
|
5
|
+
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
6
|
+
import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
|
|
7
|
+
import { integrationWorktreeIdentity, worktreeIdentity } from "./gitWorkspace.js";
|
|
8
|
+
import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
|
|
9
|
+
/** Current owner facts + the same Git inspection used immediately before removal.
|
|
10
|
+
* No preparation, locks, Git refresh, runtime stop, DB writes or repair.
|
|
11
|
+
*/
|
|
12
|
+
export async function inspectWorkspaceCleanup(store, git, workspace, disposition, forceArchive = false) {
|
|
13
|
+
const owner = workspace.owner;
|
|
14
|
+
const task = store.getTask(owner.taskId);
|
|
15
|
+
if (task === null)
|
|
16
|
+
throw new Error(`Task not found: ${owner.taskId}.`);
|
|
17
|
+
const resource = managedWorkspaceKey(owner);
|
|
18
|
+
const sources = [resource];
|
|
19
|
+
const actions = workspaceCleanupActions(workspace);
|
|
20
|
+
const checks = [];
|
|
21
|
+
const add = (reason, detail, expected, observed) => checks.push({ resource, reason, status: "blocked", detail, expected, observed, sources, actions });
|
|
22
|
+
if (!isDeepStrictEqual(store.getManagedWorkspace(owner), workspace)) {
|
|
23
|
+
add("workspace-identity-mismatch", "Current workspace ownership differs from the inspected record.", "same managed workspace", "changed or missing");
|
|
24
|
+
return checks;
|
|
25
|
+
}
|
|
26
|
+
if (task.workspaceIdentity === undefined && workspace.entries.some(e => e.access === "write")) {
|
|
27
|
+
add("workspace-metadata-missing", "Task has no durable workspace identity; do not infer one from paths.", "recorded Task workspace identity", null);
|
|
28
|
+
return checks;
|
|
29
|
+
}
|
|
30
|
+
let roleName = managedWorktreeName(owner);
|
|
31
|
+
switch (owner.type) {
|
|
32
|
+
case "work-item": {
|
|
33
|
+
const item = store.getWorkItem(task.id, owner.workItemId);
|
|
34
|
+
if (item === null || !["accepted", "retired"].includes(item.status)) {
|
|
35
|
+
add("owner-unsettled", "WorkItem must be terminal before cleanup.", ["accepted", "retired"], item?.status ?? null);
|
|
36
|
+
}
|
|
37
|
+
if (item?.workspaceDisposition !== undefined && item.workspaceDisposition !== disposition) {
|
|
38
|
+
add("workspace-disposition-mismatch", "WorkItem cleanup disposition differs from the recorded decision.", item.workspaceDisposition, disposition);
|
|
39
|
+
}
|
|
40
|
+
if (item?.status === "accepted" && disposition === "integrated") {
|
|
41
|
+
const inspection = await new WorkItemChangeSetManager(store).inspectIntegrated(task.id, item.id);
|
|
42
|
+
checks.push(...inspection.checks);
|
|
43
|
+
}
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case "review-round": {
|
|
47
|
+
const round = store.getReviewRound(task.id, owner.reviewRoundId);
|
|
48
|
+
if (round === null || !["completed", "failed"].includes(round.status)) {
|
|
49
|
+
add("owner-unsettled", "ReviewRound must be terminal before cleanup.", ["completed", "failed"], round?.status ?? null);
|
|
50
|
+
}
|
|
51
|
+
if (round?.workspace === undefined || !isDeepStrictEqual(round.workspace, workspace)) {
|
|
52
|
+
add("workspace-metadata-mismatch", "ReviewRound and managed workspace records diverge.", "same workspace in ReviewRound and registry", "different or missing mirror");
|
|
53
|
+
}
|
|
54
|
+
if (round?.scope === "task")
|
|
55
|
+
roleName = `reviewer-${round.reviewerRoleName}`;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "execution-lane": {
|
|
59
|
+
const group = owner.purpose === "execution"
|
|
60
|
+
? store.getWorkItem(task.id, owner.workItemId)?.executionGroups.find(g => g.id === owner.executionGroupId)
|
|
61
|
+
: store.getReviewRound(task.id, owner.reviewRoundId)?.executionGroup;
|
|
62
|
+
const lane = group?.id === owner.executionGroupId
|
|
63
|
+
? group.lanes.find(l => l.id === owner.executionLaneId) : undefined;
|
|
64
|
+
if (lane === undefined || lane.disposition === "open") {
|
|
65
|
+
add("owner-unsettled", "Execution Lane is not terminally resolved.", "settled lane", lane?.disposition ?? null);
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "integration-attempt": {
|
|
70
|
+
const attempt = store.getIntegrationAttempt(task.id, owner.integrationAttemptId);
|
|
71
|
+
if (attempt === null || ["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
|
|
72
|
+
add("owner-unsettled", "IntegrationAttempt must be terminal before cleanup.", "terminal attempt", attempt?.status ?? null);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "task":
|
|
77
|
+
if (!["completed", "cancelled", "archived"].includes(task.status)) {
|
|
78
|
+
add("task-not-terminal", "Task must be completed or retired before archive cleanup.", ["completed", "cancelled", "archived"], task.status);
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
const delivery = owner.type === "task" && (disposition === "integrated" || task.status === "archived" || forceArchive)
|
|
83
|
+
? await readArchiveDelivery(store, git, task, forceArchive) : undefined;
|
|
84
|
+
if (delivery !== undefined && (!delivery.allMerged || !delivery.allVerified)) {
|
|
85
|
+
add("delivery-coverage", "Task main has commits without verified remote coverage; cleanup does not establish delivery.", { allMerged: true, allVerified: true }, { allMerged: delivery.allMerged, allVerified: delivery.allVerified });
|
|
86
|
+
}
|
|
87
|
+
for (const entry of workspace.entries.filter(e => e.access === "write")) {
|
|
88
|
+
const entryResource = `${resource}/${entry.projectId}`;
|
|
89
|
+
const entryChecks = [];
|
|
90
|
+
const check = (reason, detail, expected, observed) => entryChecks.push({ resource: entryResource, reason, status: "blocked", detail, expected, observed, sources, actions });
|
|
91
|
+
// Integration's durable root is its single Project worktree (not the
|
|
92
|
+
// multi-Project owner container used by the other workspace owners).
|
|
93
|
+
const container = owner.type === "integration-attempt" ? dirname(workspace.root) : workspace.root;
|
|
94
|
+
const expectedPath = join(container, entry.directory);
|
|
95
|
+
if (entry.path !== expectedPath) {
|
|
96
|
+
check("workspace-path-mismatch", "Recorded path is not the current owner-root/Project path; relocation is not proven.", { ownerRelativePath: entry.directory }, { ownerRelativePath: safeRelativePath(container, entry.path) });
|
|
97
|
+
checks.push(...entryChecks);
|
|
98
|
+
continue; // Never inspect a foreign/mismatched path.
|
|
99
|
+
}
|
|
100
|
+
const taskSegment = taskWorkspaceRefSegment(task);
|
|
101
|
+
const expectedBranch = owner.type === "integration-attempt"
|
|
102
|
+
? integrationWorktreeIdentity(taskSegment, owner.integrationAttemptId).branch
|
|
103
|
+
: worktreeIdentity(taskSegment, roleName).branch;
|
|
104
|
+
if (entry.branch !== expectedBranch) {
|
|
105
|
+
check("workspace-identity-mismatch", "Recorded branch does not match the exact owner.", expectedBranch, entry.branch);
|
|
106
|
+
checks.push(...entryChecks);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const main = store.getTaskWorkspace(task.id)?.entries.find(e => e.projectId === entry.projectId);
|
|
111
|
+
if (main === undefined && owner.type !== "task") {
|
|
112
|
+
check("workspace-metadata-missing", "Task main Git repository reference is unavailable.", "Task main Project entry", null);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const state = owner.type === "task"
|
|
116
|
+
? await git.inspectTaskClone({ path: entry.path, container: workspace.root, directory: entry.directory,
|
|
117
|
+
taskSegment, branch: entry.branch })
|
|
118
|
+
: await git.inspectWorktree({ repositoryPath: main.path, container, directory: entry.directory,
|
|
119
|
+
taskSegment, roleName, expectedBranch });
|
|
120
|
+
if (state === "dirty")
|
|
121
|
+
check("dirty-worktree", "Uncommitted or untracked changes must be preserved.", "clean", "dirty");
|
|
122
|
+
if (owner.type === "task" && state !== "missing" && delivery !== undefined) {
|
|
123
|
+
const covered = delivery.projects.find(p => p.projectId === entry.projectId);
|
|
124
|
+
const expected = covered?.expectedLocalCommit;
|
|
125
|
+
const observed = (await git.inspect(entry.path, "HEAD")).baseCommit;
|
|
126
|
+
if (expected != null && expected !== observed && covered?.deliveryLocalCommit !== observed) {
|
|
127
|
+
check("head-mismatch", "Task main HEAD differs from the accepted head and its covered publication candidate.", { acceptedCommit: expected, deliveryLocalCommit: covered?.deliveryLocalCommit ?? null }, observed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
entryChecks.push(...cleanupCheckFromError(error, entryResource, sources, actions));
|
|
134
|
+
}
|
|
135
|
+
checks.push(...entryChecks);
|
|
136
|
+
}
|
|
137
|
+
return checks;
|
|
138
|
+
}
|
|
139
|
+
/** Reuse Publication's projection, supplying live heads only where its existing
|
|
140
|
+
* contract asks for them (active/cancelled). Completed/archived heads stay frozen.
|
|
141
|
+
* Never prepare a workspace or substitute an old binding's commit for a failed read.
|
|
142
|
+
*/
|
|
143
|
+
export async function readArchiveDelivery(store, git, task, forceArchive = false) {
|
|
144
|
+
const projects = [];
|
|
145
|
+
// Force admission preserves its existing store-only evidence; it never
|
|
146
|
+
// freezes new delivery heads merely because an inspection observed them.
|
|
147
|
+
if (!forceArchive && (task.status === "active" || task.status === "cancelled") && task.workspaceIdentity !== undefined) {
|
|
148
|
+
const main = store.getTaskWorkspace(task.id);
|
|
149
|
+
if (main?.owner.type === "task" && main.owner.taskId === task.id) {
|
|
150
|
+
const branch = worktreeIdentity(taskWorkspaceRefSegment(task), "main").branch;
|
|
151
|
+
for (const entry of main.entries.filter(e => e.access === "write")) {
|
|
152
|
+
if (entry.path !== join(main.root, entry.directory) || entry.branch !== branch)
|
|
153
|
+
continue;
|
|
154
|
+
try {
|
|
155
|
+
const head = await git.inspect(entry.path, "HEAD");
|
|
156
|
+
if (head.root === entry.path && await git.headRef(entry.path) === branch) {
|
|
157
|
+
projects.push({ projectId: entry.projectId, commit: head.baseCommit });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch { /* Failed reads remain unavailable in the existing projection. */ }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return projectTaskRemoteDeliveryFromStore(store, task, { projects });
|
|
165
|
+
}
|
|
166
|
+
export function workspaceCleanupActions(workspace) {
|
|
167
|
+
const owner = workspace.owner;
|
|
168
|
+
switch (owner.type) {
|
|
169
|
+
case "task": return [`yui task remote-delivery ${owner.taskId}`, `yui task show ${owner.taskId}`];
|
|
170
|
+
case "work-item": return [`yui task work show ${owner.taskId}/${owner.workItemId}`,
|
|
171
|
+
`yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`];
|
|
172
|
+
case "review-round": return [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
|
|
173
|
+
`yui task work review cleanup ${owner.taskId}/${owner.reviewRoundId}`];
|
|
174
|
+
case "integration-attempt": return [`yui task integration show ${owner.taskId}/${owner.integrationAttemptId}`,
|
|
175
|
+
`yui task integration cleanup ${owner.taskId}/${owner.integrationAttemptId}`];
|
|
176
|
+
case "execution-lane": return owner.purpose === "execution"
|
|
177
|
+
? [`yui task work show ${owner.taskId}/${owner.workItemId}`,
|
|
178
|
+
`yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`]
|
|
179
|
+
: [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
|
|
180
|
+
`yui task work review cleanup ${owner.taskId}/${owner.reviewRoundId}`];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Only expose paths inside this already-authorized owner, never foreign paths. */
|
|
184
|
+
export function safeRelativePath(root, path) {
|
|
185
|
+
const value = relative(root, path);
|
|
186
|
+
return value === ".." || value.startsWith("../") || isAbsolute(value) ? "[outside owner root]" : value || ".";
|
|
187
|
+
}
|