@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,6 +1,7 @@
|
|
|
1
1
|
import { lstat, mkdir, readlink, readdir, rm, rmdir, symlink, unlink } from "node:fs/promises";
|
|
2
|
-
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
3
|
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
|
|
4
5
|
import { retireTaskRoleSessionsForWorkspace, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
5
6
|
import { formatWorkspacePreflightError } from "../executor/workspacePreflightClassification.js";
|
|
6
7
|
import { updateRole } from "../role/role.js";
|
|
@@ -22,6 +23,7 @@ import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
|
|
|
22
23
|
import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
|
|
23
24
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
24
25
|
import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
|
|
26
|
+
import { managedTaskRoot } from "../storage/homeLayout.js";
|
|
25
27
|
const MAIN_WORKTREE = "main";
|
|
26
28
|
const LEADER_ROLE = "leader";
|
|
27
29
|
/**
|
|
@@ -537,7 +539,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
537
539
|
throw new Error(`Task Project baseline was not resolved: ${project.id}.`);
|
|
538
540
|
}
|
|
539
541
|
const identity = worktreeIdentity(taskSegment, MAIN_WORKTREE);
|
|
540
|
-
const destination = join(
|
|
542
|
+
const destination = join(root, safePathSegment(binding.directory));
|
|
541
543
|
const physical = previous === undefined
|
|
542
544
|
? await this.git.clone({
|
|
543
545
|
remoteUrl: project.remoteUrl,
|
|
@@ -966,9 +968,25 @@ export class FileTaskWorkspacePreparer {
|
|
|
966
968
|
const requestedBase = previous?.access === "write" || requestedBaseRef === undefined
|
|
967
969
|
? null
|
|
968
970
|
: await this.git.inspect(mainEntry.path, requestedBaseRef);
|
|
971
|
+
if (previous?.access === "read" && previous.directory === binding.directory) {
|
|
972
|
+
const viewPath = join(root, binding.directory);
|
|
973
|
+
const metadata = await lstat(viewPath).catch((error) => {
|
|
974
|
+
if (errorCode(error) === "ENOENT")
|
|
975
|
+
return null;
|
|
976
|
+
throw error;
|
|
977
|
+
});
|
|
978
|
+
if (metadata?.isSymbolicLink()) {
|
|
979
|
+
if (resolve(root, await readlink(viewPath)) !== resolve(previous.path)) {
|
|
980
|
+
throw new Error(`Read-only workspace view changed before scope expansion: ${viewPath}.`);
|
|
981
|
+
}
|
|
982
|
+
// Remove only the registered read view, never its target.
|
|
983
|
+
await unlink(viewPath);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
969
986
|
const physical = await this.git.ensureWorktree({
|
|
970
987
|
repositoryPath: mainEntry.path,
|
|
971
|
-
container:
|
|
988
|
+
container: root,
|
|
989
|
+
directory: binding.directory,
|
|
972
990
|
taskSegment,
|
|
973
991
|
roleName: item.id,
|
|
974
992
|
baseRef
|
|
@@ -1139,7 +1157,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1139
1157
|
const project = requireProject(this.store, entry.projectId);
|
|
1140
1158
|
const physical = await this.git.ensureWorktree({
|
|
1141
1159
|
repositoryPath: this.#taskRepositoryPath(taskId, project.id),
|
|
1142
|
-
container:
|
|
1160
|
+
container: existing.root,
|
|
1161
|
+
directory: entry.directory,
|
|
1143
1162
|
taskSegment,
|
|
1144
1163
|
roleName: managedWorktreeName(owner),
|
|
1145
1164
|
baseRef: entry.baseCommit
|
|
@@ -1151,6 +1170,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1151
1170
|
return existing;
|
|
1152
1171
|
}
|
|
1153
1172
|
const prepared = [];
|
|
1173
|
+
const root = this.#executionLaneWorkspaceRoot(taskId, executionGroupId, executionLaneId);
|
|
1154
1174
|
try {
|
|
1155
1175
|
for (const binding of lockedTask.projectBindings) {
|
|
1156
1176
|
const project = requireProject(this.store, binding.projectId);
|
|
@@ -1162,7 +1182,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1162
1182
|
const inputHead = inputHeadByProject.get(project.id) ?? sourceEntry.baseCommit;
|
|
1163
1183
|
const physical = await this.git.ensureWorktree({
|
|
1164
1184
|
repositoryPath: this.#taskRepositoryPath(taskId, project.id),
|
|
1165
|
-
container:
|
|
1185
|
+
container: root,
|
|
1186
|
+
directory: sourceEntry.directory,
|
|
1166
1187
|
taskSegment,
|
|
1167
1188
|
roleName: managedWorktreeName(owner),
|
|
1168
1189
|
baseRef: inputHead
|
|
@@ -1182,7 +1203,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
1182
1203
|
}
|
|
1183
1204
|
});
|
|
1184
1205
|
}
|
|
1185
|
-
const root = this.#executionLaneWorkspaceRoot(taskId, executionGroupId, executionLaneId);
|
|
1186
1206
|
await ensureWorkspaceView(root, prepared.map(({ entry }) => entry));
|
|
1187
1207
|
const workspace = createManagedWorkspace({
|
|
1188
1208
|
owner,
|
|
@@ -1257,13 +1277,15 @@ export class FileTaskWorkspacePreparer {
|
|
|
1257
1277
|
const project = requireProject(this.store, entry.projectId);
|
|
1258
1278
|
const result = await this.git.removeWorktree({
|
|
1259
1279
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1260
|
-
container:
|
|
1280
|
+
container: dirname(entry.path),
|
|
1281
|
+
directory: entry.directory,
|
|
1261
1282
|
taskSegment: this.#taskSegment(task),
|
|
1262
1283
|
roleName: managedWorktreeName(workspace.owner),
|
|
1263
1284
|
deleteBranch: true
|
|
1264
1285
|
});
|
|
1265
1286
|
if (result === "dirty")
|
|
1266
1287
|
return "dirty";
|
|
1288
|
+
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
1267
1289
|
removed ||= result === "removed";
|
|
1268
1290
|
}
|
|
1269
1291
|
await removeWorkspaceView(workspace.root);
|
|
@@ -1310,7 +1332,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1310
1332
|
const project = requireProject(this.store, entry.projectId);
|
|
1311
1333
|
const result = await this.git.removeWorktree({
|
|
1312
1334
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1313
|
-
container:
|
|
1335
|
+
container: dirname(entry.path),
|
|
1336
|
+
directory: entry.directory,
|
|
1314
1337
|
taskSegment,
|
|
1315
1338
|
roleName: managedWorktreeName(workspace.owner),
|
|
1316
1339
|
deleteBranch: true
|
|
@@ -1417,19 +1440,19 @@ export class FileTaskWorkspacePreparer {
|
|
|
1417
1440
|
const snapshotCommits = new Map((taskScope
|
|
1418
1441
|
? round.taskCandidate?.projects ?? []
|
|
1419
1442
|
: candidate.gitSnapshot.projects).map(({ projectId, commit }) => [projectId, commit]));
|
|
1443
|
+
const reviewRoot = this.#reviewRoundWorkspaceRoot(task.id, round);
|
|
1420
1444
|
const frozenEntries = taskScope
|
|
1421
1445
|
? task.projectBindings.map((binding) => {
|
|
1422
1446
|
const commit = snapshotCommits.get(binding.projectId);
|
|
1423
1447
|
if (commit === undefined) {
|
|
1424
1448
|
throw new Error(`Task Review candidate Project is missing: ${binding.projectId}.`);
|
|
1425
1449
|
}
|
|
1426
|
-
const project = requireProject(this.store, binding.projectId);
|
|
1427
1450
|
const identity = worktreeIdentity(taskSegment, this.#reviewWorktreeName(round));
|
|
1428
1451
|
return {
|
|
1429
1452
|
projectId: binding.projectId,
|
|
1430
1453
|
directory: binding.directory,
|
|
1431
1454
|
access: "write",
|
|
1432
|
-
path: join(
|
|
1455
|
+
path: join(reviewRoot, safePathSegment(binding.directory)),
|
|
1433
1456
|
branch: identity.branch,
|
|
1434
1457
|
baseRef: commit,
|
|
1435
1458
|
baseCommit: commit
|
|
@@ -1447,7 +1470,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
1447
1470
|
}
|
|
1448
1471
|
const expectedEntries = new Map(frozenEntries.map((entry) => [entry.projectId, entry]));
|
|
1449
1472
|
const existing = this.store.getReviewRoundWorkspace(task.id, round.id);
|
|
1450
|
-
const reviewRoot = this.#reviewRoundWorkspaceRoot(task.id, round);
|
|
1451
1473
|
if (taskScope && existing === null) {
|
|
1452
1474
|
const reassigned = await this.#reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries);
|
|
1453
1475
|
if (reassigned !== null)
|
|
@@ -1482,7 +1504,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1482
1504
|
}
|
|
1483
1505
|
const project = requireProject(this.store, entry.projectId);
|
|
1484
1506
|
const identity = worktreeIdentity(taskSegment, this.#reviewWorktreeName(round));
|
|
1485
|
-
const expectedPath = join(
|
|
1507
|
+
const expectedPath = join(reviewRoot, safePathSegment(entry.directory));
|
|
1486
1508
|
if (entry.path !== expectedPath || entry.branch !== identity.branch) {
|
|
1487
1509
|
throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace managed identity mismatch for ${round.id}/${entry.projectId}.`);
|
|
1488
1510
|
}
|
|
@@ -1568,7 +1590,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1568
1590
|
const project = requireProject(this.store, source.projectId);
|
|
1569
1591
|
const physical = await this.git.ensureWorktree({
|
|
1570
1592
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1571
|
-
container:
|
|
1593
|
+
container: reviewRoot,
|
|
1594
|
+
directory: source.directory,
|
|
1572
1595
|
taskSegment,
|
|
1573
1596
|
roleName: this.#reviewWorktreeName(round),
|
|
1574
1597
|
baseRef: source.baseCommit
|
|
@@ -1869,7 +1892,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1869
1892
|
const project = requireProject(this.store, entry.projectId);
|
|
1870
1893
|
const result = await this.git.removeWorktree({
|
|
1871
1894
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1872
|
-
container:
|
|
1895
|
+
container: dirname(entry.path),
|
|
1896
|
+
directory: entry.directory,
|
|
1873
1897
|
taskSegment: this.#taskSegment(task),
|
|
1874
1898
|
roleName: this.#reviewWorktreeName(round),
|
|
1875
1899
|
deleteBranch: true
|
|
@@ -1877,6 +1901,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1877
1901
|
if (result === "dirty") {
|
|
1878
1902
|
throw new Error(`Review workspace changed after cleanup preflight: ${round.id}.`);
|
|
1879
1903
|
}
|
|
1904
|
+
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
1880
1905
|
removed ||= result === "removed";
|
|
1881
1906
|
}
|
|
1882
1907
|
await removeWorkspaceView(workspace.root);
|
|
@@ -1935,7 +1960,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
1935
1960
|
const project = requireProject(this.store, entry.projectId);
|
|
1936
1961
|
const result = await this.git.removeWorktree({
|
|
1937
1962
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
1938
|
-
container:
|
|
1963
|
+
container: dirname(entry.path),
|
|
1964
|
+
directory: entry.directory,
|
|
1939
1965
|
taskSegment: this.#taskSegment(task),
|
|
1940
1966
|
roleName: managedWorktreeName(workspace.owner),
|
|
1941
1967
|
deleteBranch: true
|
|
@@ -1943,6 +1969,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1943
1969
|
if (result === "dirty") {
|
|
1944
1970
|
throw new Error(`WorkItem workspace changed after cleanup preflight: ${item.id}.`);
|
|
1945
1971
|
}
|
|
1972
|
+
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
1946
1973
|
removed ||= result === "removed";
|
|
1947
1974
|
}
|
|
1948
1975
|
await removeWorkspaceView(workspace.root);
|
|
@@ -1956,7 +1983,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
1956
1983
|
catch (error) {
|
|
1957
1984
|
if (!removed)
|
|
1958
1985
|
throw error;
|
|
1959
|
-
throw new Error(`WorkItem worktree was removed but durable cleanup was not recorded; retry cleanup: ${error instanceof Error ? error.message : String(error)}
|
|
1986
|
+
throw new Error(`WorkItem worktree was removed but durable cleanup was not recorded; retry cleanup: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
1960
1987
|
}
|
|
1961
1988
|
return removed ? "removed" : "missing";
|
|
1962
1989
|
}
|
|
@@ -1991,19 +2018,36 @@ export class FileTaskWorkspacePreparer {
|
|
|
1991
2018
|
}
|
|
1992
2019
|
if (attempt.status === "running"
|
|
1993
2020
|
|| attempt.status === "blocked"
|
|
2021
|
+
|| attempt.status === "conflicted"
|
|
1994
2022
|
|| attempt.status === "validating") {
|
|
1995
2023
|
throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
|
|
1996
2024
|
}
|
|
1997
2025
|
const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
|
|
1998
2026
|
const project = requireProject(this.store, attempt.projectId);
|
|
2027
|
+
// The recorded entry pins the exact on-disk worktree path; reconstruct the
|
|
2028
|
+
// owner root from it so removal targets the real Git worktree regardless of
|
|
2029
|
+
// how the layout helper derives it. Absent a recorded workspace, fall back
|
|
2030
|
+
// to the deterministic integration owner root + bound directory.
|
|
2031
|
+
const recorded = workspace?.entries[0];
|
|
2032
|
+
const directory = recorded?.directory
|
|
2033
|
+
?? task.projectBindings.find(({ projectId }) => projectId === attempt.projectId)?.directory;
|
|
2034
|
+
if (directory === undefined) {
|
|
2035
|
+
throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
|
|
2036
|
+
}
|
|
1999
2037
|
const result = await this.git.removeIntegrationWorktree({
|
|
2000
2038
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
2001
|
-
container:
|
|
2039
|
+
container: recorded !== undefined
|
|
2040
|
+
? dirname(recorded.path)
|
|
2041
|
+
: this.#integrationWorkspaceRoot(task.id, attempt.id),
|
|
2042
|
+
directory,
|
|
2002
2043
|
taskSegment: this.#taskSegment(task),
|
|
2003
2044
|
integrationId: attempt.id,
|
|
2004
|
-
discardChanges: attempt.status === "failed"
|
|
2045
|
+
discardChanges: task.status !== "archived" && attempt.status === "failed"
|
|
2005
2046
|
});
|
|
2006
2047
|
if (result !== "dirty") {
|
|
2048
|
+
for (const entry of workspace?.entries ?? []) {
|
|
2049
|
+
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
2050
|
+
}
|
|
2007
2051
|
if (workspace !== null)
|
|
2008
2052
|
this.#resourceRegistrar().markWorkspaceDeleted(workspace);
|
|
2009
2053
|
await rm(join(this.home, "artifacts", "integration-checks", task.id, attempt.id), { recursive: true, force: true });
|
|
@@ -2050,10 +2094,14 @@ export class FileTaskWorkspacePreparer {
|
|
|
2050
2094
|
}
|
|
2051
2095
|
for (const entry of main.entries) {
|
|
2052
2096
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2053
|
-
const result = await this.git.
|
|
2097
|
+
const result = await this.git.removeTaskClone({
|
|
2098
|
+
path: entry.path, container: main.root, directory: entry.directory,
|
|
2099
|
+
taskSegment: this.#taskSegment(task), branch: entry.branch
|
|
2100
|
+
});
|
|
2054
2101
|
if (result === "dirty") {
|
|
2055
2102
|
throw new Error(`Task workspace changed after cleanup preflight: ${task.id}.`);
|
|
2056
2103
|
}
|
|
2104
|
+
this.#recordArchivePathRemoval(task, entry.path, result);
|
|
2057
2105
|
}
|
|
2058
2106
|
assertTaskArchiveState(requireTask(this.store, task.id), task);
|
|
2059
2107
|
await removeWorkspaceView(main.root);
|
|
@@ -2064,6 +2112,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
2064
2112
|
this.#clearTaskWorkspace(task, this.#fallbackWorkspace());
|
|
2065
2113
|
return { taskId, status: "removed" };
|
|
2066
2114
|
}
|
|
2115
|
+
#recordArchivePathRemoval(task, path, status) {
|
|
2116
|
+
if (task.status !== "archived")
|
|
2117
|
+
return;
|
|
2118
|
+
recordArchiveCleanup(this.store, task.id, {
|
|
2119
|
+
resource: `path:${path}`, paths: [path],
|
|
2120
|
+
detail: status === "missing"
|
|
2121
|
+
? "Directory absent; exact Git cleanup completed where applicable."
|
|
2122
|
+
: "Exact owned path removed."
|
|
2123
|
+
}, status, this.now());
|
|
2124
|
+
}
|
|
2067
2125
|
async #inspectEntries(taskId, taskSegment, roleName, entries) {
|
|
2068
2126
|
if (entries.length === 0)
|
|
2069
2127
|
return "missing";
|
|
@@ -2072,7 +2130,11 @@ export class FileTaskWorkspacePreparer {
|
|
|
2072
2130
|
const project = requireProject(this.store, entry.projectId);
|
|
2073
2131
|
const state = await this.git.inspectWorktree({
|
|
2074
2132
|
repositoryPath: this.#taskRepositoryPath(taskId, project.id),
|
|
2075
|
-
|
|
2133
|
+
// A managed worktree lives at `root/<directory>`, so its parent is the
|
|
2134
|
+
// owner workspace root. Deriving it from the entry keeps this helper
|
|
2135
|
+
// agnostic to which owner (main/work-item/review/lane) it serves.
|
|
2136
|
+
container: dirname(entry.path),
|
|
2137
|
+
directory: entry.directory,
|
|
2076
2138
|
taskSegment,
|
|
2077
2139
|
roleName
|
|
2078
2140
|
});
|
|
@@ -2082,9 +2144,6 @@ export class FileTaskWorkspacePreparer {
|
|
|
2082
2144
|
}
|
|
2083
2145
|
return found ? "clean" : "missing";
|
|
2084
2146
|
}
|
|
2085
|
-
#projectContainer(projectName) {
|
|
2086
|
-
return join(resolveWorktreeRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(projectName));
|
|
2087
|
-
}
|
|
2088
2147
|
/**
|
|
2089
2148
|
* The Task workspace ref segment for Git worktree derivation: the
|
|
2090
2149
|
* token-bearing segment for a Task with a persisted identity, its bare id
|
|
@@ -2095,13 +2154,16 @@ export class FileTaskWorkspacePreparer {
|
|
|
2095
2154
|
return taskWorkspaceRefSegment(task);
|
|
2096
2155
|
}
|
|
2097
2156
|
#taskWorkspaceRoot(taskId) {
|
|
2098
|
-
return join(resolveTaskRoot(this.home
|
|
2157
|
+
return join(resolveTaskRoot(this.home), safePathSegment(taskId), "main");
|
|
2099
2158
|
}
|
|
2100
2159
|
#workItemWorkspaceRoot(taskId, workItemId) {
|
|
2101
|
-
return join(resolveTaskRoot(this.home
|
|
2160
|
+
return join(resolveTaskRoot(this.home), safePathSegment(taskId), "work-items", safePathSegment(workItemId));
|
|
2102
2161
|
}
|
|
2103
2162
|
#reviewRoundWorkspaceRoot(taskId, round) {
|
|
2104
|
-
return join(resolveTaskRoot(this.home
|
|
2163
|
+
return join(resolveTaskRoot(this.home), safePathSegment(taskId), "reviews", safePathSegment(this.#reviewWorktreeName(round)));
|
|
2164
|
+
}
|
|
2165
|
+
#integrationWorkspaceRoot(taskId, integrationId) {
|
|
2166
|
+
return integrationWorkspaceRoot(this.home, taskId, integrationId);
|
|
2105
2167
|
}
|
|
2106
2168
|
#reviewWorktreeName(round) {
|
|
2107
2169
|
return (round.scope ?? "work-item") === "task"
|
|
@@ -2109,7 +2171,7 @@ export class FileTaskWorkspacePreparer {
|
|
|
2109
2171
|
: round.id;
|
|
2110
2172
|
}
|
|
2111
2173
|
#executionLaneWorkspaceRoot(taskId, groupId, laneId) {
|
|
2112
|
-
return join(resolveTaskRoot(this.home
|
|
2174
|
+
return join(resolveTaskRoot(this.home), safePathSegment(taskId), "execution-lanes", safePathSegment(groupId), safePathSegment(laneId));
|
|
2113
2175
|
}
|
|
2114
2176
|
#fallbackWorkspace() {
|
|
2115
2177
|
return this.store.getConfig().defaultWorkspace ?? process.cwd();
|
|
@@ -2127,7 +2189,8 @@ export class FileTaskWorkspacePreparer {
|
|
|
2127
2189
|
}
|
|
2128
2190
|
const removal = await this.git.removeWorktree({
|
|
2129
2191
|
repositoryPath: this.#taskRepositoryPath(task.id, project.id),
|
|
2130
|
-
container:
|
|
2192
|
+
container: dirname(entry.path),
|
|
2193
|
+
directory: entry.directory,
|
|
2131
2194
|
taskSegment,
|
|
2132
2195
|
roleName,
|
|
2133
2196
|
...(deleteBranch ? { deleteBranch: true } : {})
|
|
@@ -2218,7 +2281,7 @@ function requireTask(store, taskId) {
|
|
|
2218
2281
|
return task;
|
|
2219
2282
|
}
|
|
2220
2283
|
function assertTaskArchiveState(current, expected) {
|
|
2221
|
-
if ((current.status !== "completed" && current.status !== "cancelled")
|
|
2284
|
+
if ((current.status !== "completed" && current.status !== "cancelled" && current.status !== "archived")
|
|
2222
2285
|
|| !isDeepStrictEqual(current, expected)) {
|
|
2223
2286
|
throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
|
|
2224
2287
|
}
|
|
@@ -2441,14 +2504,28 @@ function recordWorkspaceDisposition(store, taskId, workItem, now) {
|
|
|
2441
2504
|
throw new Error(`Work item not found: ${workItem.workItemId}.`);
|
|
2442
2505
|
store.saveWorkItem(taskId, recordWorkItemWorkspaceDisposition(item, workItem.disposition, now));
|
|
2443
2506
|
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Reconcile an owner workspace directory so it presents every bound Project at
|
|
2509
|
+
* `root/<directory>`.
|
|
2510
|
+
*
|
|
2511
|
+
* Writable Projects are real Git worktrees created in place at `root/<directory>`
|
|
2512
|
+
* by the Git port before this runs; they are left untouched here. Read-only
|
|
2513
|
+
* Projects share an upstream worktree (the Task main clone) and are surfaced as
|
|
2514
|
+
* genuine symlinks `root/<directory> -> entry.path`. Only the symlink views are
|
|
2515
|
+
* reconciled: a stale or renamed read link is repaired, an obsolete one removed.
|
|
2516
|
+
* A real directory (a write worktree) is never treated as a view entry, so this
|
|
2517
|
+
* neither creates nor removes committed Git content.
|
|
2518
|
+
*/
|
|
2444
2519
|
async function ensureWorkspaceView(root, entries) {
|
|
2445
2520
|
await mkdir(root, { recursive: true });
|
|
2446
|
-
const expected = new Map(entries.map((entry) => [entry.directory, entry.path]));
|
|
2521
|
+
const expected = new Map(entries.filter((entry) => entry.access === "read").map((entry) => [entry.directory, entry.path]));
|
|
2447
2522
|
for (const current of await readdir(root, { withFileTypes: true })) {
|
|
2523
|
+
// A write entry is a real worktree directory living in place under root; it
|
|
2524
|
+
// is owned by Git, not by this view, so it is never reconciled or removed
|
|
2525
|
+
// here. Only symlinks (read-entry views) are this function's concern.
|
|
2526
|
+
if (!current.isSymbolicLink())
|
|
2527
|
+
continue;
|
|
2448
2528
|
const target = expected.get(current.name);
|
|
2449
|
-
if (!current.isSymbolicLink()) {
|
|
2450
|
-
throw new Error(`Managed workspace contains an unexpected entry: ${join(root, current.name)}.`);
|
|
2451
|
-
}
|
|
2452
2529
|
const path = join(root, current.name);
|
|
2453
2530
|
const linked = resolve(root, await readlink(path));
|
|
2454
2531
|
if (target === undefined) {
|
|
@@ -2493,23 +2570,17 @@ function safePathSegment(value) {
|
|
|
2493
2570
|
function sameCommit(left, right) {
|
|
2494
2571
|
return left.toLowerCase() === right.toLowerCase();
|
|
2495
2572
|
}
|
|
2496
|
-
export function
|
|
2497
|
-
return
|
|
2498
|
-
}
|
|
2499
|
-
export function resolveTaskRoot(home, workspace) {
|
|
2500
|
-
return join(resolveWorkspaceRoot(home, workspace), "tasks");
|
|
2573
|
+
export function resolveTaskRoot(home) {
|
|
2574
|
+
return managedTaskRoot(home);
|
|
2501
2575
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
throw new Error("Project workspace must be outside YUI_HOME.");
|
|
2511
|
-
}
|
|
2512
|
-
return workspaceRoot;
|
|
2576
|
+
/**
|
|
2577
|
+
* The integration owner root: `tasks/<taskId>/integrations/<integrationId>`.
|
|
2578
|
+
* A single-project integration worktree lives at `<root>/<boundDirectory>`.
|
|
2579
|
+
* Shared by the preparer and the integration service so create and cleanup
|
|
2580
|
+
* derive the same on-disk path.
|
|
2581
|
+
*/
|
|
2582
|
+
export function integrationWorkspaceRoot(home, taskId, integrationId) {
|
|
2583
|
+
return join(managedTaskRoot(home), safePathSegment(taskId), "integrations", safePathSegment(integrationId));
|
|
2513
2584
|
}
|
|
2514
2585
|
function errorCode(error) {
|
|
2515
2586
|
return typeof error === "object" && error !== null && "code" in error
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { usageError } from "../errors/cliError.js";
|
|
2
|
+
/** Read the existing source owner once and freeze its exact Candidate evidence. */
|
|
3
|
+
export async function snapshotWorkItemCandidate(store, preparer, taskId, workItemId, taskFinalReviewContract) {
|
|
4
|
+
const item = store.getWorkItem(taskId, workItemId);
|
|
5
|
+
if (item === null)
|
|
6
|
+
throw usageError(`Work Item not found: ${taskId}/${workItemId}.`);
|
|
7
|
+
const workspace = store.getWorkItemWorkspace(taskId, workItemId);
|
|
8
|
+
if (workspace !== null) {
|
|
9
|
+
return { candidateGitSnapshot: await preparer.snapshotCandidateWorkspace(workspace) };
|
|
10
|
+
}
|
|
11
|
+
// Read-only results freeze their summary/artifact evidence, not Git output.
|
|
12
|
+
if (item.writeProjectIds.length === 0)
|
|
13
|
+
return {};
|
|
14
|
+
if (taskFinalReviewContract === undefined) {
|
|
15
|
+
throw usageError(`Work Item ${taskId}/${workItemId} has no managed Candidate workspace. `
|
|
16
|
+
+ `Use yui task work isolate ${taskId}/${workItemId} before implementing writable work.`);
|
|
17
|
+
}
|
|
18
|
+
// Preserve the existing exact Task-final metadata-only Candidate contract.
|
|
19
|
+
// It is not the ordinary route for a new independently accepted code result.
|
|
20
|
+
const main = store.getTaskWorkspace(taskId);
|
|
21
|
+
if (main === null)
|
|
22
|
+
return {};
|
|
23
|
+
if (main.owner.type !== "task") {
|
|
24
|
+
throw usageError(`Task has no authoritative main workspace: ${taskId}.`);
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return {
|
|
28
|
+
directTaskMainSnapshot: await preparer.snapshotDirectTaskMain(main, item.writeProjectIds)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw usageError(error instanceof Error ? error.message : String(error));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -11,6 +11,7 @@ import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from "node
|
|
|
11
11
|
import { basename, join, resolve } from "node:path";
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
13
|
import { managedProjectPath } from "../repository/project.js";
|
|
14
|
+
import { managedRuntimeRoot } from "../storage/homeLayout.js";
|
|
14
15
|
import { isResourceQuarantinePath } from "./resourceRegistry.js";
|
|
15
16
|
import { createResourceRecord, isReleaseNamespacePath, isTerminalTaskStatus } from "./resourceTypes.js";
|
|
16
17
|
const executeFile = promisify(execFile);
|
|
@@ -164,8 +165,8 @@ export async function discoverResources(input) {
|
|
|
164
165
|
function discoverRuntimeArtifacts(home, input) {
|
|
165
166
|
const discovered = [];
|
|
166
167
|
const now = input.now;
|
|
167
|
-
// 3a. Task runtime isolation roots: <home
|
|
168
|
-
const runtimeRoot =
|
|
168
|
+
// 3a. Task runtime isolation roots: <home>/runtime/task-runtimes/<taskId>/<owner>/<launch>/
|
|
169
|
+
const runtimeRoot = managedRuntimeRoot(home);
|
|
169
170
|
if (existsSync(runtimeRoot)) {
|
|
170
171
|
for (const taskEntry of safeReaddir(runtimeRoot)) {
|
|
171
172
|
if (!taskEntry.isDirectory())
|