@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.
Files changed (100) hide show
  1. package/dist/agent/launchEnvironment.js +7 -0
  2. package/dist/cli/commandCatalog.js +26 -6
  3. package/dist/cli/interactionPolicy.js +3 -3
  4. package/dist/cli/updateOrchestrator.js +24 -1
  5. package/dist/cli/updatePorts.js +7 -3
  6. package/dist/cli/upgradeCommand.js +42 -2
  7. package/dist/cli.js +336 -93
  8. package/dist/commands/globalRoleCommands.js +314 -4
  9. package/dist/commands/projectCommands.js +6 -7
  10. package/dist/commands/releaseCommands.js +18 -0
  11. package/dist/commands/taskActor.js +25 -0
  12. package/dist/commands/taskCommands.js +511 -83
  13. package/dist/commands/taskIntegrationCommands.js +16 -38
  14. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  15. package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
  16. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  17. package/dist/context/taskContext.js +38 -2
  18. package/dist/controller/agentHostObservation.js +155 -0
  19. package/dist/controller/clientRuntime.js +17 -2
  20. package/dist/controller/controller.js +3 -0
  21. package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
  22. package/dist/controller/globalInputDelivery.js +119 -0
  23. package/dist/controller/jobControl.js +6 -2
  24. package/dist/controller/resourceInventory.js +14 -4
  25. package/dist/controller/resourceInventoryLinux.js +2 -6
  26. package/dist/controller/runtime.js +81 -6
  27. package/dist/controller/runtimeEventInbox.js +32 -3
  28. package/dist/controller/runtimeEventProcessor.js +26 -6
  29. package/dist/controller/runtimeHookRunFence.js +75 -19
  30. package/dist/controller/structuredProviderObservation.js +133 -70
  31. package/dist/coordination/workMailboxQueue.js +5 -0
  32. package/dist/execution/workItemExecutionProjection.js +1 -1
  33. package/dist/executor/agentExecutor.js +64 -4
  34. package/dist/executor/executorRegistry.js +3 -0
  35. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  36. package/dist/integration/deliveryObligation.js +2 -1
  37. package/dist/integration/gitIntegrationService.js +312 -382
  38. package/dist/integration/integrationAttempt.js +30 -4
  39. package/dist/integration/integrationQueueService.js +7 -7
  40. package/dist/integration/integrationSourceApplication.js +323 -0
  41. package/dist/message/globalInterrupt.js +33 -0
  42. package/dist/message/inputControlResolution.js +106 -0
  43. package/dist/message/message.js +367 -0
  44. package/dist/message/messageContinuation.js +126 -3
  45. package/dist/message/taskInterrupt.js +34 -0
  46. package/dist/observability/orchestrationMetrics.js +1 -1
  47. package/dist/release/releaseHandover.js +22 -0
  48. package/dist/release/releaseWorkflowPorts.js +15 -7
  49. package/dist/repository/gitWorkspace.js +72 -15
  50. package/dist/repository/taskWorkspaceCoordinator.js +134 -0
  51. package/dist/repository/taskWorkspacePreparer.js +120 -49
  52. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  53. package/dist/resources/resourceDiscovery.js +3 -2
  54. package/dist/runtime/agentHost.js +152 -72
  55. package/dist/runtime/agentHostCompatibility.js +127 -0
  56. package/dist/runtime/agentHostProtocol.js +53 -0
  57. package/dist/runtime/executionEnvironment.js +0 -19
  58. package/dist/runtime/launchBroker.js +6 -0
  59. package/dist/runtime/sessionReconciliation.js +4 -4
  60. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  61. package/dist/runtime/tmuxAdapters.js +5 -3
  62. package/dist/scheduler/operatorEvent.js +4 -0
  63. package/dist/scheduler/taskExecutionProjection.js +12 -1
  64. package/dist/scheduler/wakeReason.js +7 -1
  65. package/dist/scheduler/wakeupQueue.js +2 -0
  66. package/dist/setup/setupCommand.js +26 -8
  67. package/dist/storage/homeLayout.js +130 -0
  68. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  69. package/dist/storage/migrations/integrationContinuation.js +104 -0
  70. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  71. package/dist/storage/sqliteSchema.js +136 -4
  72. package/dist/storage/sqliteStore.js +40 -1
  73. package/dist/storage/storageVersions.js +1 -1
  74. package/dist/storage/storeRpc.js +1 -0
  75. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  76. package/dist/task/archiveDiagnostics.js +128 -0
  77. package/dist/task/nextAction.js +44 -11
  78. package/dist/web/assets/client/app.js +55 -0
  79. package/dist/web/assets/client/components.js +1 -0
  80. package/dist/web/assets/client/i18n.js +6 -0
  81. package/dist/web/assets/client/taskSurface.js +106 -0
  82. package/dist/web/assets/client/view.js +7 -4
  83. package/dist/web/assets/shell.js +23 -0
  84. package/dist/web/assets/styles/layout.js +1 -1
  85. package/dist/web/assets/styles/widgets.js +12 -0
  86. package/dist/web/webServer.js +117 -1
  87. package/dist/web/webSnapshot.js +4 -3
  88. package/dist/web/webTaskSurface.js +222 -5
  89. package/dist/workspace/workItemChangeSetManager.js +18 -2
  90. package/docs/release-workflow.md +39 -0
  91. package/docs/release-workflow.zh-CN.md +29 -0
  92. package/docs/sqlite-control-plane-design.md +223 -1
  93. package/docs/testing/verification-levels.md +24 -0
  94. package/docs/testing/verification-levels.zh-CN.md +11 -0
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +151 -49
  97. package/skills/yui-leader/references/integration.md +52 -2
  98. package/skills/yui-operator/SKILL.md +6 -1
  99. package/skills/yui-runtime/SKILL.md +27 -0
  100. 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(this.#projectContainer(project.name), identity.directory);
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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(this.#projectContainer(project.name), identity.directory),
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(this.#projectContainer(project.name), identity.directory);
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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: this.#projectContainer(project.name),
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.removeStrandedWorktree(entry.path);
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
- container: this.#projectContainer(project.name),
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, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "main");
2157
+ return join(resolveTaskRoot(this.home), safePathSegment(taskId), "main");
2099
2158
  }
2100
2159
  #workItemWorkspaceRoot(taskId, workItemId) {
2101
- return join(resolveTaskRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "work-items", safePathSegment(workItemId));
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, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "reviews", safePathSegment(this.#reviewWorktreeName(round)));
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, this.store.getConfig().defaultWorkspace), safePathSegment(taskId), "execution-lanes", safePathSegment(groupId), safePathSegment(laneId));
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: this.#projectContainer(project.name),
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 resolveWorktreeRoot(home, workspace) {
2497
- return join(resolveWorkspaceRoot(home, workspace), "worktree");
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
- function resolveWorkspaceRoot(home, workspace) {
2503
- if (workspace === undefined) {
2504
- throw new Error("Project workspace is not configured; run yui setup.");
2505
- }
2506
- const homeRoot = resolve(home);
2507
- const workspaceRoot = resolve(workspace);
2508
- const fromHome = relative(homeRoot, workspaceRoot);
2509
- if (fromHome === "" || (!fromHome.startsWith("..") && !isAbsolute(fromHome))) {
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>.task-runtimes/<taskId>/<owner>/<launch>/
168
- const runtimeRoot = `${resolve(home)}.task-runtimes`;
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())