@zq-silk/yui 0.15.9 → 0.15.11
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/dist/agent/launchEnvironment.js +7 -0
- package/dist/cli/commandCatalog.js +26 -6
- package/dist/cli/interactionPolicy.js +3 -3
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +336 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +511 -83
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/taskContext.js +38 -2
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +3 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +136 -4
- package/dist/storage/sqliteStore.js +40 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/web/assets/client/app.js +55 -0
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +106 -0
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +117 -1
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/testing/verification-levels.md +24 -0
- package/docs/testing/verification-levels.zh-CN.md +11 -0
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +151 -49
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +6 -1
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { releaseWorkflowScratchRoot } from "../storage/homeLayout.js";
|
|
5
5
|
import { runUpdate } from "../cli/updateOrchestrator.js";
|
|
6
6
|
import { activatedControllerEntrypoint } from "../cli/updatePorts.js";
|
|
7
7
|
import { restartFileTaskController, stopFileTaskController } from "../controller/clientRuntime.js";
|
|
@@ -581,7 +581,7 @@ export function createReleaseWorkflowPorts(deps) {
|
|
|
581
581
|
// publish` read THIS snapshot, so a TOCTOU replacement of the
|
|
582
582
|
// original path after the integrity check cannot change what is
|
|
583
583
|
// published. The snapshot is removed once publish completes.
|
|
584
|
-
const snapshot = await writeVerifiedTarballSnapshot(bytes);
|
|
584
|
+
const snapshot = await writeVerifiedTarballSnapshot(deps.home, bytes);
|
|
585
585
|
try {
|
|
586
586
|
// Inspect the tarball manifest before publishing: the actual
|
|
587
587
|
// package name/version must match the frozen declaration, not
|
|
@@ -727,8 +727,12 @@ export function createReleaseWorkflowPorts(deps) {
|
|
|
727
727
|
// P2-2 (rr19): Install in a fresh temp directory with an isolated
|
|
728
728
|
// npm cache, so npx cannot reuse a local node_modules binary. Then
|
|
729
729
|
// run the installed binary directly and verify the output matches
|
|
730
|
-
// the pinned version.
|
|
731
|
-
|
|
730
|
+
// the pinned version. The smoke directory is a Yui-authored release
|
|
731
|
+
// artifact, so it lands under the Home release-workflow scratch root,
|
|
732
|
+
// never a shared system temp root.
|
|
733
|
+
const scratch = releaseWorkflowScratchRoot(deps.home);
|
|
734
|
+
await mkdir(scratch, { recursive: true, mode: 0o700 });
|
|
735
|
+
const smokeDir = await mkdtemp(join(scratch, "yui-smoke-"));
|
|
732
736
|
const smokeCache = join(smokeDir, "npm-cache");
|
|
733
737
|
try {
|
|
734
738
|
const install = await run("npm", [
|
|
@@ -1642,10 +1646,14 @@ async function queryControllerLifecycle(run, globalYui, home, expectedVersion) {
|
|
|
1642
1646
|
* random name and read-only permissions (P1-3, rr22). The manifest
|
|
1643
1647
|
* inspection and `npm publish` both read this snapshot, so replacing the
|
|
1644
1648
|
* original path after the integrity check cannot change what is published.
|
|
1645
|
-
* The caller removes the snapshot once publish completes.
|
|
1649
|
+
* The caller removes the snapshot once publish completes. The snapshot is a
|
|
1650
|
+
* Yui-authored release artifact, so it lands under the Home release-workflow
|
|
1651
|
+
* scratch root, never a shared system temp root.
|
|
1646
1652
|
*/
|
|
1647
|
-
async function writeVerifiedTarballSnapshot(bytes) {
|
|
1648
|
-
const
|
|
1653
|
+
async function writeVerifiedTarballSnapshot(home, bytes) {
|
|
1654
|
+
const scratch = releaseWorkflowScratchRoot(home);
|
|
1655
|
+
await mkdir(scratch, { recursive: true, mode: 0o700 });
|
|
1656
|
+
const snapshot = join(scratch, `yui-release-snapshot-${randomBytes(12).toString("hex")}.tgz`);
|
|
1649
1657
|
// "wx" fails if the random name already exists, so a pre-existing file
|
|
1650
1658
|
// (or symlink) can never be overwritten or followed.
|
|
1651
1659
|
await writeFile(snapshot, bytes, { flag: "wx" });
|
|
@@ -679,7 +679,10 @@ export class NodeGitWorkspace {
|
|
|
679
679
|
return this.#ensureManagedWorktree({
|
|
680
680
|
repositoryPath: input.repositoryPath,
|
|
681
681
|
container: input.container,
|
|
682
|
-
identity:
|
|
682
|
+
identity: {
|
|
683
|
+
directory: input.directory,
|
|
684
|
+
branch: worktreeIdentity(input.taskSegment, input.roleName).branch
|
|
685
|
+
},
|
|
683
686
|
baseRef: input.baseRef
|
|
684
687
|
});
|
|
685
688
|
}
|
|
@@ -687,7 +690,10 @@ export class NodeGitWorkspace {
|
|
|
687
690
|
return this.#ensureManagedWorktree({
|
|
688
691
|
repositoryPath: input.repositoryPath,
|
|
689
692
|
container: input.container,
|
|
690
|
-
identity:
|
|
693
|
+
identity: {
|
|
694
|
+
directory: input.directory,
|
|
695
|
+
branch: integrationWorktreeIdentity(input.taskSegment, input.integrationId).branch
|
|
696
|
+
},
|
|
691
697
|
baseRef: input.baseRef,
|
|
692
698
|
allowRebase: true
|
|
693
699
|
});
|
|
@@ -734,19 +740,19 @@ export class NodeGitWorkspace {
|
|
|
734
740
|
if (state === "dirty")
|
|
735
741
|
return state;
|
|
736
742
|
const container = resolve(input.container);
|
|
737
|
-
const
|
|
738
|
-
const path = managedPath(container,
|
|
743
|
+
const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
744
|
+
const path = managedPath(container, input.directory);
|
|
739
745
|
const project = await this.inspect(input.repositoryPath);
|
|
740
746
|
if (state === "missing") {
|
|
741
747
|
if (input.deleteBranch === true) {
|
|
742
|
-
await
|
|
743
|
-
await deleteBranchIfPresent(project.root,
|
|
748
|
+
await removeMissingWorktreeRegistration(project.root, path, branch);
|
|
749
|
+
await deleteBranchIfPresent(project.root, branch);
|
|
744
750
|
}
|
|
745
751
|
return state;
|
|
746
752
|
}
|
|
747
753
|
await git(["-C", project.root, "worktree", "remove", "--", path]);
|
|
748
754
|
if (input.deleteBranch === true) {
|
|
749
|
-
await deleteBranchIfPresent(project.root,
|
|
755
|
+
await deleteBranchIfPresent(project.root, branch);
|
|
750
756
|
}
|
|
751
757
|
return "removed";
|
|
752
758
|
}
|
|
@@ -779,6 +785,36 @@ export class NodeGitWorkspace {
|
|
|
779
785
|
}
|
|
780
786
|
return "removed";
|
|
781
787
|
}
|
|
788
|
+
async removeTaskClone(input) {
|
|
789
|
+
const identity = worktreeIdentity(input.taskSegment, "main");
|
|
790
|
+
const expected = managedPath(resolve(input.container), input.directory);
|
|
791
|
+
if (resolve(input.path) !== expected || input.branch !== identity.branch) {
|
|
792
|
+
throw new Error("Task clone does not match its recorded managed identity.");
|
|
793
|
+
}
|
|
794
|
+
const kind = await pathKind(expected);
|
|
795
|
+
if (kind === undefined)
|
|
796
|
+
return "missing";
|
|
797
|
+
if (kind === "symlink")
|
|
798
|
+
throw new Error("Task clone must not be a symbolic link.");
|
|
799
|
+
await canonicalContainer(input.container, false);
|
|
800
|
+
if (await realpath(expected) !== expected || await pathKind(join(expected, ".git")) !== "directory") {
|
|
801
|
+
throw new Error("Task clone is not a standalone owned Git directory.");
|
|
802
|
+
}
|
|
803
|
+
const repository = await this.inspect(expected);
|
|
804
|
+
if (repository.root !== expected || repository.gitDirectory !== join(expected, ".git")) {
|
|
805
|
+
throw new Error("Task clone Git ownership cannot be established.");
|
|
806
|
+
}
|
|
807
|
+
await assertExpectedBranch(expected, input.branch);
|
|
808
|
+
if (!await this.isClean(expected))
|
|
809
|
+
return "dirty";
|
|
810
|
+
const worktrees = (await git(["-C", expected, "worktree", "list", "--porcelain", "-z"]))
|
|
811
|
+
.split("\0").filter(line => line.startsWith("worktree "));
|
|
812
|
+
if (worktrees.length !== 1 || worktrees[0] !== `worktree ${expected}`) {
|
|
813
|
+
throw new Error("Task clone still owns Git worktree registrations; retained.");
|
|
814
|
+
}
|
|
815
|
+
await rm(expected, { recursive: true });
|
|
816
|
+
return "removed";
|
|
817
|
+
}
|
|
782
818
|
async inspectRecordedWorktree(input) {
|
|
783
819
|
const inspected = await inspectExactRecordedWorktree(input);
|
|
784
820
|
return inspected?.state ?? "missing";
|
|
@@ -824,7 +860,7 @@ export class NodeGitWorkspace {
|
|
|
824
860
|
}
|
|
825
861
|
async inspectWorktree(input) {
|
|
826
862
|
const container = resolve(input.container);
|
|
827
|
-
const path = managedPath(container,
|
|
863
|
+
const path = managedPath(container, input.directory);
|
|
828
864
|
const kind = await pathKind(path);
|
|
829
865
|
if (kind === undefined)
|
|
830
866
|
return "missing";
|
|
@@ -840,7 +876,10 @@ export class NodeGitWorkspace {
|
|
|
840
876
|
return this.#removeManagedWorktree({
|
|
841
877
|
repositoryPath: input.repositoryPath,
|
|
842
878
|
container: input.container,
|
|
843
|
-
identity:
|
|
879
|
+
identity: {
|
|
880
|
+
directory: input.directory,
|
|
881
|
+
branch: integrationWorktreeIdentity(input.taskSegment, input.integrationId).branch
|
|
882
|
+
},
|
|
844
883
|
discardChanges: input.discardChanges
|
|
845
884
|
});
|
|
846
885
|
}
|
|
@@ -851,6 +890,9 @@ export class NodeGitWorkspace {
|
|
|
851
890
|
if (kind === "symlink")
|
|
852
891
|
throw new Error("Managed worktree path must not be a symbolic link.");
|
|
853
892
|
const repository = await this.inspect(input.repositoryPath);
|
|
893
|
+
if (kind === undefined) {
|
|
894
|
+
await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch);
|
|
895
|
+
}
|
|
854
896
|
if (kind === "directory") {
|
|
855
897
|
const canonicalContainerPath = await canonicalContainer(container, false);
|
|
856
898
|
await assertOwnedWorktree(repository, canonicalContainerPath, path);
|
|
@@ -884,6 +926,21 @@ export class NodeGitWorkspace {
|
|
|
884
926
|
return kind === "directory" ? "removed" : "missing";
|
|
885
927
|
}
|
|
886
928
|
}
|
|
929
|
+
/** Missing paths do not imply missing Git metadata. Never prune another
|
|
930
|
+
* workspace's registration as a side effect of cleaning this exact owner.
|
|
931
|
+
*/
|
|
932
|
+
async function removeMissingWorktreeRegistration(repositoryRoot, path, branch) {
|
|
933
|
+
const records = (await git(["-C", repositoryRoot, "worktree", "list", "--porcelain", "-z"])).split("\0\0");
|
|
934
|
+
const fields = records.map(record => record.split("\0")).find(record => record.includes(`worktree ${path}`));
|
|
935
|
+
if (fields === undefined)
|
|
936
|
+
return;
|
|
937
|
+
if (!fields.includes(`branch refs/heads/${branch}`) || fields.some(field => field.startsWith("locked"))) {
|
|
938
|
+
throw new Error(`Missing worktree has uncertain or locked Git ownership: ${path}; metadata retained.`);
|
|
939
|
+
}
|
|
940
|
+
if (await pathKind(path) !== undefined)
|
|
941
|
+
throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
|
|
942
|
+
await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
|
|
943
|
+
}
|
|
887
944
|
async function deleteBranchIfPresent(repositoryRoot, branch) {
|
|
888
945
|
const branchRef = `refs/heads/${branch}`;
|
|
889
946
|
if (!await gitSucceeds([
|
|
@@ -960,9 +1017,9 @@ async function assertOwnedWorktree(project, container, path) {
|
|
|
960
1017
|
}
|
|
961
1018
|
async function inspectExactRecordedWorktree(input) {
|
|
962
1019
|
const container = resolve(input.container);
|
|
963
|
-
const
|
|
964
|
-
const expectedPath = managedPath(container,
|
|
965
|
-
if (resolve(input.path) !== expectedPath || input.branch !==
|
|
1020
|
+
const expectedBranch = worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
1021
|
+
const expectedPath = managedPath(container, input.directory);
|
|
1022
|
+
if (resolve(input.path) !== expectedPath || input.branch !== expectedBranch) {
|
|
966
1023
|
throw new Error("Recorded managed worktree identity is invalid.");
|
|
967
1024
|
}
|
|
968
1025
|
const kind = await pathKind(expectedPath);
|
|
@@ -1023,9 +1080,9 @@ async function resolveRefCommit(repositoryPath, ref) {
|
|
|
1023
1080
|
async function recordedWorktreeWithoutRepository(input, cause) {
|
|
1024
1081
|
if (await pathKind(input.repositoryPath) !== undefined)
|
|
1025
1082
|
throw cause;
|
|
1026
|
-
const
|
|
1027
|
-
const expectedPath = managedPath(resolve(input.container),
|
|
1028
|
-
if (resolve(input.path) !== expectedPath || input.branch !==
|
|
1083
|
+
const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
|
|
1084
|
+
const expectedPath = managedPath(resolve(input.container), input.directory);
|
|
1085
|
+
if (resolve(input.path) !== expectedPath || input.branch !== branch) {
|
|
1029
1086
|
throw cause;
|
|
1030
1087
|
}
|
|
1031
1088
|
if (await pathKind(expectedPath) === undefined)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { isArchivePersistenceFailure, recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
4
|
+
import { projectTaskRemoteDeliveryFromStore } from "../commands/taskRemoteDeliveryCommand.js";
|
|
5
|
+
import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
|
|
2
6
|
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
7
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
4
8
|
import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
@@ -247,6 +251,7 @@ export class TaskWorkspaceCoordinator {
|
|
|
247
251
|
if (attempt === null
|
|
248
252
|
|| attempt.status === "running"
|
|
249
253
|
|| attempt.status === "blocked"
|
|
254
|
+
|| attempt.status === "conflicted"
|
|
250
255
|
|| attempt.status === "validating") {
|
|
251
256
|
throw new Error(`IntegrationAttempt must be terminal before archive cleanup: ${workspace.owner.integrationAttemptId}.`);
|
|
252
257
|
}
|
|
@@ -330,6 +335,135 @@ export class TaskWorkspaceCoordinator {
|
|
|
330
335
|
releaseMaintenance?.();
|
|
331
336
|
}
|
|
332
337
|
}
|
|
338
|
+
/** Best-effort cleanup only after explicit force admission has committed.
|
|
339
|
+
* This is one foreground attempt, not a retry worker. Each independent result
|
|
340
|
+
* is durable before continuing, and a failed audit write really fails.
|
|
341
|
+
*/
|
|
342
|
+
async cleanupArchivedTask(taskId, disposition) {
|
|
343
|
+
const task = this.store.getTask(taskId);
|
|
344
|
+
if (task?.status !== "archived"
|
|
345
|
+
|| !this.store.listEvents(taskId).some(e => e.type === "task.archived" && e.payload.force === "true")) {
|
|
346
|
+
throw new Error(`Force archive must commit before cleanup: ${taskId}.`);
|
|
347
|
+
}
|
|
348
|
+
const attempt = async (diagnostic, action) => {
|
|
349
|
+
recordArchiveCleanup(this.store, taskId, diagnostic, "started");
|
|
350
|
+
let status;
|
|
351
|
+
let detail = diagnostic.detail;
|
|
352
|
+
try {
|
|
353
|
+
const result = await action();
|
|
354
|
+
status = result === "dirty" ? "retained" : result;
|
|
355
|
+
if (result === "dirty")
|
|
356
|
+
detail = "Dirty workspace retained; force archive does not discard local changes.";
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
if (isArchivePersistenceFailure(error))
|
|
360
|
+
throw error;
|
|
361
|
+
status = "retained";
|
|
362
|
+
detail = error instanceof Error ? error.message : String(error);
|
|
363
|
+
}
|
|
364
|
+
recordArchiveCleanup(this.store, taskId, { ...diagnostic, detail }, status);
|
|
365
|
+
return status !== "retained";
|
|
366
|
+
};
|
|
367
|
+
let runtimeReleased = true;
|
|
368
|
+
for (const role of this.store.listRoles(taskId)) {
|
|
369
|
+
const released = await attempt({ resource: `role:${taskId}/${role.name}`,
|
|
370
|
+
detail: "Exact Role runtime stop." }, async () => {
|
|
371
|
+
const provider = this.store.getTaskRoleSessionSet(taskId, role.name)?.providerBinding;
|
|
372
|
+
const mailbox = this.store.getWorkMailbox({ kind: "role", taskId, roleName: role.name });
|
|
373
|
+
if (mailbox?.processing != null && (provider?.run == null
|
|
374
|
+
|| !["completed", "failed", "cancelled"].includes(provider.run.status))) {
|
|
375
|
+
throw new Error(`Original mailbox claim ${mailbox.processing.batchId} retained; execution outcome is not known.`);
|
|
376
|
+
}
|
|
377
|
+
if (provider?.run != null && !["completed", "failed", "cancelled"].includes(provider.run.status)) {
|
|
378
|
+
throw new Error(`Provider input ${provider.run.attemptId}/${provider.run.status} retained; establish exact quiescence before resolving it.`);
|
|
379
|
+
}
|
|
380
|
+
await this.#stopLiveRoles(taskId, [role.name]);
|
|
381
|
+
return "released";
|
|
382
|
+
});
|
|
383
|
+
runtimeReleased &&= released;
|
|
384
|
+
}
|
|
385
|
+
const physicalReleased = await attempt({ resource: `runtime:${taskId}`,
|
|
386
|
+
detail: "Verify exact physical resource release before workspace deletion." }, async () => {
|
|
387
|
+
const activeRun = this.store.listRuns(taskId).find(r => r.status === "active");
|
|
388
|
+
const job = this.store.listDurableJobs(taskId).find(j => ["queued", "running", "unknown-needs-attention"].includes(j.status));
|
|
389
|
+
if (activeRun !== undefined || job !== undefined) {
|
|
390
|
+
throw new Error(`Execution resources retained: ${activeRun?.id ?? job?.id}; archive is not proof of quiescence.`);
|
|
391
|
+
}
|
|
392
|
+
if (!runtimeReleased)
|
|
393
|
+
throw new Error("One or more Role runtimes could not be safely released.");
|
|
394
|
+
if (this.runtime.assertTaskPhysicalResourcesReleased === undefined) {
|
|
395
|
+
throw new Error("Physical resource release inspection is unavailable; workspace references retained.");
|
|
396
|
+
}
|
|
397
|
+
await this.runtime.assertTaskPhysicalResourcesReleased(taskId);
|
|
398
|
+
return "released";
|
|
399
|
+
});
|
|
400
|
+
if (physicalReleased) {
|
|
401
|
+
// Task main owns the Git objects of its dependent worktrees, so it is
|
|
402
|
+
// always last and remains when any dependent cleanup is unresolved.
|
|
403
|
+
const workspaces = this.store.listManagedWorkspaces(taskId).sort((a, b) => Number(a.owner.type === "task") - Number(b.owner.type === "task"));
|
|
404
|
+
for (const workspace of workspaces) {
|
|
405
|
+
await attempt({ resource: managedWorkspaceKey(workspace.owner),
|
|
406
|
+
detail: "Remove only clean, exactly owned workspace resources.",
|
|
407
|
+
paths: [workspace.root, ...workspace.entries.filter(e => e.access === "write").map(e => e.path)]
|
|
408
|
+
}, async () => {
|
|
409
|
+
const release = acquireProjectMaintenanceLocks(this.preparer.home, workspace.entries.map(e => e.projectId));
|
|
410
|
+
try {
|
|
411
|
+
if (!isDeepStrictEqual(this.store.getManagedWorkspace(workspace.owner), workspace)) {
|
|
412
|
+
throw new Error("Workspace ownership changed; retained.");
|
|
413
|
+
}
|
|
414
|
+
const owner = workspace.owner;
|
|
415
|
+
switch (owner.type) {
|
|
416
|
+
case "work-item": {
|
|
417
|
+
const item = this.store.getWorkItem(taskId, owner.workItemId);
|
|
418
|
+
if (item?.status === "accepted" && disposition === "integrated") {
|
|
419
|
+
await new WorkItemChangeSetManager(this.store).assertIntegrated(taskId, item.id);
|
|
420
|
+
}
|
|
421
|
+
return this.preparer.cleanupWorkItemWorkspace(taskId, owner.workItemId, item?.status === "retired" ? "abandoned" : disposition);
|
|
422
|
+
}
|
|
423
|
+
case "review-round":
|
|
424
|
+
return this.preparer.cleanupReviewRoundWorkspace(taskId, owner.reviewRoundId);
|
|
425
|
+
case "execution-lane":
|
|
426
|
+
return this.preparer.cleanupExecutionLaneWorkspace(taskId, owner.executionGroupId, owner.executionLaneId);
|
|
427
|
+
case "integration-attempt":
|
|
428
|
+
if (await this.preparer.inspectIntegrationWorkspace(taskId, owner.integrationAttemptId) === "dirty")
|
|
429
|
+
return "dirty";
|
|
430
|
+
return this.preparer.cleanupIntegrationWorkspace(taskId, owner.integrationAttemptId);
|
|
431
|
+
case "task": {
|
|
432
|
+
const current = this.store.getTask(taskId);
|
|
433
|
+
const delivery = projectTaskRemoteDeliveryFromStore(this.store, current);
|
|
434
|
+
if (!delivery.allMerged || !delivery.allVerified) {
|
|
435
|
+
throw new Error("Task main retained: local commits are not covered by verified remote delivery. Force does not discard them.");
|
|
436
|
+
}
|
|
437
|
+
for (const entry of workspace.entries.filter(e => e.access === "write")) {
|
|
438
|
+
const expected = delivery.projects.find(p => p.projectId === entry.projectId)?.expectedLocalCommit;
|
|
439
|
+
const path = await lstat(entry.path).catch(error => {
|
|
440
|
+
if (error.code === "ENOENT")
|
|
441
|
+
return null;
|
|
442
|
+
throw error;
|
|
443
|
+
});
|
|
444
|
+
if (expected !== null && expected !== undefined
|
|
445
|
+
&& path !== null) {
|
|
446
|
+
const actual = await this.preparer.git.inspect(entry.path, "HEAD");
|
|
447
|
+
if (actual.baseCommit !== expected)
|
|
448
|
+
throw new Error(`Task main HEAD changed: ${entry.path}; retained.`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const result = await this.preparer.cleanupTaskForArchive(taskId);
|
|
452
|
+
if (result.status !== "removed")
|
|
453
|
+
throw new Error(result.error ?? "Task main or dependent workspace retained.");
|
|
454
|
+
return "removed";
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
release();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
recordArchiveCleanup(this.store, taskId, { resource: `task:${taskId}`,
|
|
465
|
+
detail: "Foreground cleanup attempt finished; inspect retained resource references and warnings." }, "finished");
|
|
466
|
+
}
|
|
333
467
|
#assertTaskArchiveSnapshot(snapshot) {
|
|
334
468
|
this.#assertTaskArchiveLifecycle(snapshot.task);
|
|
335
469
|
const managedWorkspaces = [...this.store.listManagedWorkspaces(snapshot.task.id)]
|