@zq-silk/yui 2.2.0 → 2.4.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.
Files changed (206) hide show
  1. package/README.md +19 -3
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +25 -11
  7. package/dist/cli/interactionPolicy.js +4 -0
  8. package/dist/cli/invocationAuthority.js +1 -1
  9. package/dist/commands/executionAuditCommands.js +5 -0
  10. package/dist/commands/globalRoleCommands.js +41 -3
  11. package/dist/commands/projectCommands.js +89 -94
  12. package/dist/commands/roleSkillValidation.js +4 -8
  13. package/dist/commands/taskCommands.js +30 -9
  14. package/dist/commands/taskIntegrationCommands.js +5 -16
  15. package/dist/commands/taskUsageCommand.js +38 -0
  16. package/dist/context/roleSessionContext.js +18 -2
  17. package/dist/context/runContextPack.js +5 -1
  18. package/dist/context/sessionBootstrapManifest.js +10 -1
  19. package/dist/context/skillCatalog.js +108 -0
  20. package/dist/context/skillPackage.js +161 -0
  21. package/dist/context/taskContext.js +3 -2
  22. package/dist/context/taskSearch.js +127 -0
  23. package/dist/controlPlaneCli.js +51 -39
  24. package/dist/controller/clientRuntime.js +3 -2
  25. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  26. package/dist/controller/jobControl.js +2 -0
  27. package/dist/controller/nativeHostInput.js +37 -0
  28. package/dist/controller/runtime.js +93 -2
  29. package/dist/controller/runtimeEventInbox.js +2 -2
  30. package/dist/controller/runtimeEventProcessor.js +3 -3
  31. package/dist/doctor/doctor.js +10 -0
  32. package/dist/execution/reviewMainRun.js +1 -0
  33. package/dist/execution/workItemMainRun.js +1 -0
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  37. package/dist/integration/gitIntegrationService.js +15 -79
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/job/httpPreview.js +61 -0
  40. package/dist/job/jobCleanupInspection.js +67 -0
  41. package/dist/job/jobRunner.js +12 -2
  42. package/dist/kernel/builtinCapabilities.js +3 -1
  43. package/dist/message/message.js +15 -1
  44. package/dist/output/rolePresentation.js +4 -1
  45. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  46. package/dist/repository/gitCompatibility.js +62 -0
  47. package/dist/repository/gitWorkspace.js +86 -80
  48. package/dist/repository/gitWorktreeInventory.js +121 -0
  49. package/dist/repository/project.js +74 -25
  50. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  51. package/dist/repository/taskWorkspacePreparer.js +39 -43
  52. package/dist/repository/workspaceCleanupInspection.js +88 -7
  53. package/dist/resources/resourceDiscovery.js +2 -2
  54. package/dist/resources/resourceGc.js +7 -4
  55. package/dist/resources/resourceRegistrar.js +18 -11
  56. package/dist/runtime/agentEndpoint.js +8 -0
  57. package/dist/runtime/agentHost.js +170 -3
  58. package/dist/runtime/codexAppServerRuntime.js +1 -1
  59. package/dist/runtime/codexNativeAccess.js +175 -0
  60. package/dist/runtime/controlledCodexTui.js +116 -0
  61. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  62. package/dist/runtime/publicReply.js +23 -0
  63. package/dist/runtime/runtimeObservation.js +48 -0
  64. package/dist/runtime/structuredProviderHost.js +16 -7
  65. package/dist/runtime/taskCostMetrics.js +133 -0
  66. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  67. package/dist/runtime/taskUsageMetrics.js +11 -2
  68. package/dist/runtime/taskUsageQuery.js +38 -0
  69. package/dist/scheduler/taskExecutionProjection.js +2 -0
  70. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  71. package/dist/storage/homeLayout.js +13 -0
  72. package/dist/storage/sqliteSchema.js +44 -0
  73. package/dist/storage/sqliteStore.js +53 -0
  74. package/dist/storage/storageVersions.js +1 -1
  75. package/dist/storage/storeRpc.js +2 -0
  76. package/dist/storage/taskSearch.js +35 -0
  77. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  78. package/dist/task/archiveDiagnostics.js +26 -8
  79. package/dist/task/archivePreflight.js +26 -9
  80. package/dist/task/leaderArchive.js +9 -15
  81. package/dist/task/ordinaryArchive.js +42 -0
  82. package/dist/tmux/tmuxManager.js +8 -8
  83. package/dist/web/assets/assetManifest.js +11 -51
  84. package/dist/web/assets/client/app/catalog.js +215 -0
  85. package/dist/web/assets/client/app/main.js +148 -0
  86. package/dist/web/assets/client/app/selection.js +139 -0
  87. package/dist/web/assets/client/app/settings.js +326 -0
  88. package/dist/web/assets/client/app/taskView.js +280 -0
  89. package/dist/web/assets/client/domain/context.js +96 -0
  90. package/dist/web/assets/client/domain/records.js +126 -0
  91. package/dist/web/assets/client/domain/runs.js +119 -0
  92. package/dist/web/assets/client/domain/taskForms.js +197 -0
  93. package/dist/web/assets/client/domain/vocab.js +69 -0
  94. package/dist/web/assets/client/domain/work.js +116 -0
  95. package/dist/web/assets/client/i18n/en.js +900 -0
  96. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  97. package/dist/web/assets/client/index.js +114 -0
  98. package/dist/web/assets/client/layout/resizer.js +37 -0
  99. package/dist/web/assets/client/layout/router.js +35 -0
  100. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  101. package/dist/web/assets/client/layout/sizing.js +95 -0
  102. package/dist/web/assets/client/layout/workspace.js +247 -0
  103. package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
  104. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  105. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  106. package/dist/web/assets/client/lib/i18n.js +54 -0
  107. package/dist/web/assets/client/lib/markdown.js +167 -0
  108. package/dist/web/assets/client/lib/prefs.js +28 -0
  109. package/dist/web/assets/client/lib/theme.js +68 -0
  110. package/dist/web/assets/client/ui/containers.js +101 -0
  111. package/dist/web/assets/client/ui/controls.js +51 -0
  112. package/dist/web/assets/client/ui/forms.js +48 -0
  113. package/dist/web/assets/client/ui/metrics.js +137 -0
  114. package/dist/web/assets/client/ui/primitives.js +92 -0
  115. package/dist/web/assets/client/ui/text.js +33 -0
  116. package/dist/web/assets/client/ui/toast.js +12 -0
  117. package/dist/web/assets/client/views/bodySearch.js +172 -0
  118. package/dist/web/assets/client/views/createTask.js +85 -0
  119. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  120. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  121. package/dist/web/assets/client/views/dock/preview.js +129 -0
  122. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  123. package/dist/web/assets/client/views/globalInput.js +67 -0
  124. package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
  125. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  126. package/dist/web/assets/client/views/skills.js +152 -0
  127. package/dist/web/assets/client/views/task/actions.js +36 -0
  128. package/dist/web/assets/client/views/task/delivery.js +185 -0
  129. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  130. package/dist/web/assets/client/views/task/files.js +272 -0
  131. package/dist/web/assets/client/views/task/observation.js +153 -0
  132. package/dist/web/assets/client/views/task/overview.js +161 -0
  133. package/dist/web/assets/client/views/task/page.js +131 -0
  134. package/dist/web/assets/client/views/task/records.js +163 -0
  135. package/dist/web/assets/client/views/task/runtime.js +130 -0
  136. package/dist/web/assets/client/views/task/timeline.js +190 -0
  137. package/dist/web/assets/shared/geometry.js +19 -0
  138. package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
  139. package/dist/web/assets/shared/taskTabs.js +8 -0
  140. package/dist/web/assets/shared/themes.js +93 -0
  141. package/dist/web/assets/shell/dialogs.js +63 -0
  142. package/dist/web/assets/shell/dock.js +40 -0
  143. package/dist/web/assets/shell/index.js +40 -0
  144. package/dist/web/assets/shell/settings.js +34 -0
  145. package/dist/web/assets/shell/sidebar.js +45 -0
  146. package/dist/web/assets/styles/index.js +41 -0
  147. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
  148. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  149. package/dist/web/assets/styles/markdown.js +22 -3
  150. package/dist/web/assets/styles/tokens.js +10 -63
  151. package/dist/web/assets/styles/ui/controls.js +76 -0
  152. package/dist/web/assets/styles/ui/forms.js +46 -0
  153. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  154. package/dist/web/assets/styles/views/delivery.js +29 -0
  155. package/dist/web/assets/styles/views/dock.js +71 -0
  156. package/dist/web/assets/styles/views/page.js +32 -0
  157. package/dist/web/assets/styles/views/records.js +70 -0
  158. package/dist/web/assets/styles/views/settings.js +26 -0
  159. package/dist/web/assets/styles/views/sidebar.js +74 -0
  160. package/dist/web/assets/styles/views/task.js +80 -0
  161. package/dist/web/assets/styles/views/terminal.js +38 -0
  162. package/dist/web/taskPreviews.js +205 -0
  163. package/dist/web/tmuxWebTerminal.js +16 -9
  164. package/dist/web/webConversation.js +373 -0
  165. package/dist/web/webMaterials.js +155 -0
  166. package/dist/web/webServer.js +306 -18
  167. package/dist/web/webSettings.js +331 -0
  168. package/dist/web/webSkills.js +86 -0
  169. package/dist/web/webSnapshot.js +40 -8
  170. package/dist/web/webTaskSurface.js +114 -9
  171. package/dist/workspace/gitChangeSetCapture.js +3 -1
  172. package/docs/managed-turn-and-session-runtime.md +9 -0
  173. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  174. package/docs/observability/README.md +13 -3
  175. package/docs/observability/README.zh-CN.md +10 -3
  176. package/docs/release-workflow.md +53 -0
  177. package/docs/release-workflow.zh-CN.md +40 -0
  178. package/docs/storage-baseline.md +38 -1
  179. package/docs/storage-baseline.zh-CN.md +22 -0
  180. package/docs/task-delivery.md +36 -8
  181. package/docs/task-delivery.zh-CN.md +24 -7
  182. package/docs/task-discovery.md +28 -0
  183. package/docs/testing/verification-levels.md +2 -2
  184. package/i18n/README.zh-CN.md +4 -3
  185. package/package.json +1 -1
  186. package/skills/yui-leader/references/execution.md +2 -1
  187. package/skills/yui-leader/references/integration.md +20 -0
  188. package/skills/yui-operator/SKILL.md +4 -0
  189. package/skills/yui-reviewer/SKILL.md +3 -0
  190. package/skills/yui-runtime/SKILL.md +4 -0
  191. package/skills/yui-runtime/references/publication.md +20 -4
  192. package/skills/yui-worker/SKILL.md +3 -0
  193. package/dist/web/assets/client/app.js +0 -804
  194. package/dist/web/assets/client/components.js +0 -418
  195. package/dist/web/assets/client/detail.js +0 -333
  196. package/dist/web/assets/client/dock.js +0 -225
  197. package/dist/web/assets/client/evidence.js +0 -172
  198. package/dist/web/assets/client/forms.js +0 -230
  199. package/dist/web/assets/client/i18n.js +0 -1161
  200. package/dist/web/assets/client/markdown.js +0 -114
  201. package/dist/web/assets/client/records.js +0 -63
  202. package/dist/web/assets/client/sections.js +0 -313
  203. package/dist/web/assets/client/theme.js +0 -71
  204. package/dist/web/assets/shell.js +0 -161
  205. package/dist/web/assets/styles/components.js +0 -169
  206. package/dist/web/assets/styles/views.js +0 -292
@@ -1,4 +1,4 @@
1
- import { lstat, mkdir, readdir, readlink, rm, rmdir, symlink, unlink } from "node:fs/promises";
1
+ import { lstat, mkdir, readdir, readlink, 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";
@@ -25,7 +25,8 @@ import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
25
25
  import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockCancelledError, ProjectMaintenanceLockedError } from "./projectMaintenanceLock.js";
26
26
  import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
27
27
  import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
28
- import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
28
+ import { inspectWorkspaceCleanup, inspectWorkspaceContainer } from "./workspaceCleanupInspection.js";
29
+ import { defaultIntegrationRuntimeIsolation } from "../runtime/integrationRuntimeIsolation.js";
29
30
  const MAIN_WORKTREE = "main";
30
31
  const LEADER_ROLE = "leader";
31
32
  /**
@@ -73,7 +74,7 @@ export class FileTaskWorkspacePreparer {
73
74
  this.maintenanceLockOptions = maintenanceLockOptions;
74
75
  }
75
76
  inspectWorkspaceCleanup(workspace, disposition, forceArchive = false) {
76
- return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive);
77
+ return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive, this.home);
77
78
  }
78
79
  async #assertWorkspaceCleanup(workspace, disposition) {
79
80
  const checks = await this.inspectWorkspaceCleanup(workspace, disposition);
@@ -1296,7 +1297,7 @@ export class FileTaskWorkspacePreparer {
1296
1297
  this.#recordArchivePathRemoval(task, entry.path, result);
1297
1298
  removed ||= result === "removed";
1298
1299
  }
1299
- await removeWorkspaceView(workspace.root);
1300
+ await removeWorkspaceView(workspace.root, workspace);
1300
1301
  this.#resourceRegistrar().markWorkspaceDeleted(workspace);
1301
1302
  this.store.removeManagedWorkspace(workspace.owner);
1302
1303
  return removed ? "removed" : "missing";
@@ -1907,7 +1908,7 @@ export class FileTaskWorkspacePreparer {
1907
1908
  this.#recordArchivePathRemoval(task, entry.path, result);
1908
1909
  removed ||= result === "removed";
1909
1910
  }
1910
- await removeWorkspaceView(workspace.root);
1911
+ await removeWorkspaceView(workspace.root, workspace);
1911
1912
  this.#resourceRegistrar().markWorkspaceDeleted(workspace);
1912
1913
  this.store.transaction((tx) => {
1913
1914
  const currentRound = tx.getReviewRound(task.id, round.id);
@@ -1976,7 +1977,7 @@ export class FileTaskWorkspacePreparer {
1976
1977
  this.#recordArchivePathRemoval(task, entry.path, result);
1977
1978
  removed ||= result === "removed";
1978
1979
  }
1979
- await removeWorkspaceView(workspace.root);
1980
+ await removeWorkspaceView(workspace.root, workspace);
1980
1981
  this.#resourceRegistrar().markWorkspaceDeleted(workspace);
1981
1982
  try {
1982
1983
  this.#recordWorkspaceRemoval(task, workspace, this.#fallbackWorkspace(), {
@@ -2027,39 +2028,36 @@ export class FileTaskWorkspacePreparer {
2027
2028
  throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
2028
2029
  }
2029
2030
  const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
2030
- // A failed Integration's explicitly disposable conflict tree retains its
2031
- // existing cleanup contract outside archive. Archive never discards dirt.
2032
- if (workspace !== null && (options.preserveChanges || task.status === "archived" || attempt.status !== "failed")) {
2033
- await this.#assertWorkspaceCleanup(workspace, "abandoned");
2034
- }
2031
+ if (workspace === null)
2032
+ return "missing"; // no current resource ownership to delete
2033
+ // Terminal history does not authorize discarding conflict resolutions or
2034
+ // diagnostics. All entry points use the same exact-owner inspection.
2035
+ const runtime = options.runtimeIsolation ?? defaultIntegrationRuntimeIsolation(this.home, this.store.getHomeIdentity().homeId);
2036
+ const checks = await inspectWorkspaceCleanup(this.store, this.git, workspace, "abandoned", false, this.home, runtime);
2037
+ if (checks.length > 0)
2038
+ throw new CleanupInspectionError(checks);
2039
+ runtime.cleanup(runtime.preflight({ workspace, allowExactActive: true }), attempt.status === "committed" ? "completion" : "failure");
2035
2040
  const project = requireProject(this.store, attempt.projectId);
2036
- // The recorded entry pins the exact on-disk worktree path; reconstruct the
2037
- // owner root from it so removal targets the real Git worktree regardless of
2038
- // how the layout helper derives it. Absent a recorded workspace, fall back
2039
- // to the deterministic integration owner root + bound directory.
2040
- const recorded = workspace?.entries[0];
2041
- const directory = recorded?.directory
2042
- ?? task.projectBindings.find(({ projectId }) => projectId === attempt.projectId)?.directory;
2043
- if (directory === undefined) {
2044
- throw new Error(`Integration Project binding is unavailable: ${task.id}/${project.id}.`);
2041
+ // Only the recorded entry grants resource ownership; no history-derived
2042
+ // path reconstruction after the current resource record has been removed.
2043
+ const recorded = workspace.entries[0];
2044
+ if (recorded === undefined || recorded.projectId !== project.id || workspace.entries.length !== 1) {
2045
+ throw new Error(`Integration workspace Project identity is unavailable: ${task.id}/${project.id}.`);
2045
2046
  }
2046
2047
  const result = await this.git.removeIntegrationWorktree({
2047
2048
  repositoryPath: this.#taskRepositoryPath(task.id, project.id),
2048
- container: recorded !== undefined
2049
- ? dirname(recorded.path)
2050
- : this.#integrationWorkspaceRoot(task.id, attempt.id),
2051
- directory,
2049
+ container: dirname(recorded.path),
2050
+ directory: recorded.directory,
2052
2051
  taskSegment: this.#taskSegment(task),
2053
2052
  integrationId: attempt.id,
2054
- discardChanges: options.preserveChanges !== true && task.status !== "archived" && attempt.status === "failed"
2053
+ discardChanges: false
2055
2054
  });
2056
2055
  if (result !== "dirty") {
2057
- for (const entry of workspace?.entries ?? []) {
2056
+ for (const entry of workspace.entries) {
2058
2057
  this.#recordArchivePathRemoval(task, entry.path, result);
2059
2058
  }
2060
- if (workspace !== null)
2061
- this.#resourceRegistrar().markWorkspaceDeleted(workspace);
2062
- await rm(join(this.home, "artifacts", "integration-checks", task.id, attempt.id), { recursive: true, force: true });
2059
+ this.#resourceRegistrar().markWorkspaceDeleted(workspace);
2060
+ // Check logs are stable historical evidence, not worktree scratch.
2063
2061
  this.store.removeManagedWorkspace({
2064
2062
  type: "integration-attempt",
2065
2063
  taskId: task.id,
@@ -2097,15 +2095,9 @@ export class FileTaskWorkspacePreparer {
2097
2095
  if (main !== null) {
2098
2096
  await this.#assertWorkspaceCleanup(main, disposition);
2099
2097
  assertTaskArchiveState(requireTask(this.store, task.id), task);
2100
- // Scratch-only workspaces have no Git entries or Git workspace identity.
2101
- if (main.entries.length > 0 && await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
2102
- return {
2103
- taskId,
2104
- status: "retained-dirty",
2105
- ...(task.cwd === undefined ? {} : { path: task.cwd })
2106
- };
2107
- }
2108
- for (const entry of main.entries) {
2098
+ // The shared preflight and removeTaskClone both inspect clone identity.
2099
+ // Do not re-enter linked-worktree inspection against a missing clone.
2100
+ for (const entry of main.entries.filter(e => e.access === "write")) {
2109
2101
  assertTaskArchiveState(requireTask(this.store, task.id), task);
2110
2102
  const result = await this.git.removeTaskClone({
2111
2103
  path: entry.path, container: main.root, directory: entry.directory,
@@ -2117,7 +2109,7 @@ export class FileTaskWorkspacePreparer {
2117
2109
  this.#recordArchivePathRemoval(task, entry.path, result);
2118
2110
  }
2119
2111
  assertTaskArchiveState(requireTask(this.store, task.id), task);
2120
- await removeWorkspaceView(main.root);
2112
+ await removeWorkspaceView(main.root, main);
2121
2113
  this.#resourceRegistrar().markWorkspaceDeleted(main);
2122
2114
  assertTaskArchiveState(requireTask(this.store, task.id), task);
2123
2115
  this.#recordWorkspaceRemoval(task, main, this.#fallbackWorkspace());
@@ -2175,9 +2167,6 @@ export class FileTaskWorkspacePreparer {
2175
2167
  #reviewRoundWorkspaceRoot(taskId, round) {
2176
2168
  return join(resolveTaskRoot(this.home), safePathSegment(taskId), "reviews", safePathSegment(this.#reviewWorktreeName(round)));
2177
2169
  }
2178
- #integrationWorkspaceRoot(taskId, integrationId) {
2179
- return integrationWorkspaceRoot(this.home, taskId, integrationId);
2180
- }
2181
2170
  #reviewWorktreeName(round) {
2182
2171
  return round.scope === "task"
2183
2172
  ? `reviewer-${round.reviewerRoleName}`
@@ -2570,7 +2559,12 @@ async function ensureWorkspaceView(root, entries) {
2570
2559
  await symlink(target, join(root, directory), "dir");
2571
2560
  }
2572
2561
  }
2573
- async function removeWorkspaceView(root) {
2562
+ async function removeWorkspaceView(root, workspace) {
2563
+ if (workspace !== undefined) {
2564
+ const checks = await inspectWorkspaceContainer(workspace);
2565
+ if (checks.length > 0)
2566
+ throw new CleanupInspectionError(checks);
2567
+ }
2574
2568
  let entries;
2575
2569
  try {
2576
2570
  entries = await readdir(root, { withFileTypes: true });
@@ -2584,6 +2578,8 @@ async function removeWorkspaceView(root) {
2584
2578
  if (!entry.isSymbolicLink()) {
2585
2579
  throw new Error(`Managed workspace contains an unexpected entry: ${join(root, entry.name)}.`);
2586
2580
  }
2581
+ }
2582
+ for (const entry of entries) {
2587
2583
  await unlink(join(root, entry.name));
2588
2584
  }
2589
2585
  await rmdir(root);
@@ -1,15 +1,19 @@
1
- import { dirname, isAbsolute, join, relative } from "node:path";
1
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { lstat, readdir, readlink } from "node:fs/promises";
3
4
  import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
4
5
  import { cleanupCheckFromError } from "../workspace/cleanupInspection.js";
5
6
  import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
6
7
  import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
7
8
  import { integrationWorktreeIdentity, worktreeIdentity } from "./gitWorkspace.js";
8
9
  import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
10
+ import { jobCleanupBlocker } from "../job/jobCleanupInspection.js";
11
+ import { defaultIntegrationRuntimeIsolation } from "../runtime/integrationRuntimeIsolation.js";
12
+ import { inspectTaskRuntimeCleanup } from "../runtime/taskRuntimeIsolation.js";
9
13
  /** Current owner facts + the same Git inspection used immediately before removal.
10
14
  * No preparation, locks, Git refresh, runtime stop, DB writes or repair.
11
15
  */
12
- export async function inspectWorkspaceCleanup(store, git, workspace, disposition, forceArchive = false) {
16
+ export async function inspectWorkspaceCleanup(store, git, workspace, disposition, forceArchive = false, home, integrationRuntime) {
13
17
  const owner = workspace.owner;
14
18
  const task = store.getTask(owner.taskId);
15
19
  if (task === null)
@@ -23,6 +27,18 @@ export async function inspectWorkspaceCleanup(store, git, workspace, disposition
23
27
  add("workspace-identity-mismatch", "Current workspace ownership differs from the inspected record.", "same managed workspace", "changed or missing");
24
28
  return checks;
25
29
  }
30
+ // Job admission supports these three exact managed owners. Apply the same
31
+ // physical check before every supported owner's cleanup, not only Integration.
32
+ for (const job of store.listDurableJobs(task.id).filter(job => (owner.type === "task" && job.owner.kind === "task")
33
+ || (owner.type === "work-item" && job.owner.kind === "work-item"
34
+ && job.owner.workItemId === owner.workItemId)
35
+ || (owner.type === "integration-attempt" && job.owner.kind === "integration-attempt"
36
+ && job.owner.integrationAttemptId === owner.integrationAttemptId))) {
37
+ const blocker = jobCleanupBlocker(job, home);
38
+ if (blocker !== undefined)
39
+ add("unresolved-execution", blocker, "settled execution", { jobId: job.id, status: job.status });
40
+ }
41
+ checks.push(...await inspectWorkspaceContainer(workspace));
26
42
  if (task.workspaceIdentity === undefined && workspace.entries.some(e => e.access === "write")) {
27
43
  add("workspace-metadata-missing", "Task has no durable workspace identity; do not infer one from paths.", "recorded Task workspace identity", null);
28
44
  return checks;
@@ -68,9 +84,24 @@ export async function inspectWorkspaceCleanup(store, git, workspace, disposition
68
84
  }
69
85
  case "integration-attempt": {
70
86
  const attempt = store.getIntegrationAttempt(task.id, owner.integrationAttemptId);
87
+ const entry = workspace.entries[0];
88
+ if (workspace.entries.length !== 1 || entry?.access !== "write"
89
+ || entry.projectId !== attempt?.projectId || entry.path !== workspace.root) {
90
+ add("workspace-identity-mismatch", "Integration resource does not match its single recorded Project worktree.", "one writable Project at the Integration root", "different or missing entry");
91
+ return checks;
92
+ }
71
93
  if (attempt === null || ["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
72
94
  add("owner-unsettled", "IntegrationAttempt must be terminal before cleanup.", "terminal attempt", attempt?.status ?? null);
73
95
  }
96
+ try {
97
+ if (home === undefined)
98
+ throw new Error("Runtime Home unavailable.");
99
+ const runtime = integrationRuntime ?? defaultIntegrationRuntimeIsolation(home, store.getHomeIdentity().homeId);
100
+ inspectTaskRuntimeCleanup(runtime.preflight({ workspace, allowExactActive: true }), attempt?.status === "committed" ? "completion" : "failure");
101
+ }
102
+ catch {
103
+ add("runtime-resource-unverified", "Integration runtime ownership or physical quiescence cannot be verified.", "exactly owned inactive runtime and cleanup claims", "unverified");
104
+ }
74
105
  break;
75
106
  }
76
107
  case "task":
@@ -79,9 +110,9 @@ export async function inspectWorkspaceCleanup(store, git, workspace, disposition
79
110
  }
80
111
  break;
81
112
  }
82
- const delivery = owner.type === "task" && (disposition === "integrated" || task.status === "archived" || forceArchive)
113
+ const delivery = owner.type === "task"
83
114
  ? await readArchiveDelivery(store, git, task, forceArchive) : undefined;
84
- if (delivery !== undefined && (!delivery.allMerged || !delivery.allVerified)) {
115
+ if (disposition === "integrated" && delivery !== undefined && (!delivery.allMerged || !delivery.allVerified)) {
85
116
  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
117
  }
87
118
  for (const entry of workspace.entries.filter(e => e.access === "write")) {
@@ -116,14 +147,20 @@ export async function inspectWorkspaceCleanup(store, git, workspace, disposition
116
147
  ? await git.inspectTaskClone({ path: entry.path, container: workspace.root, directory: entry.directory,
117
148
  taskSegment, branch: entry.branch })
118
149
  : await git.inspectWorktree({ repositoryPath: main.path, container, directory: entry.directory,
119
- taskSegment, roleName, expectedBranch });
150
+ taskSegment, roleName,
151
+ ...(owner.type === "integration-attempt" ? { expectedBranch } : {}) });
120
152
  if (state === "dirty")
121
153
  check("dirty-worktree", "Uncommitted or untracked changes must be preserved.", "clean", "dirty");
122
154
  if (owner.type === "task" && state !== "missing" && delivery !== undefined) {
123
155
  const covered = delivery.projects.find(p => p.projectId === entry.projectId);
124
156
  const expected = covered?.expectedLocalCommit;
125
157
  const observed = (await git.inspect(entry.path, "HEAD")).baseCommit;
126
- if (expected != null && expected !== observed && covered?.deliveryLocalCommit !== observed) {
158
+ const baseline = task.projectBindings.find(binding => binding.projectId === entry.projectId)?.baseCommit;
159
+ const unchangedAbandonment = disposition === "abandoned" && baseline !== undefined && observed === baseline;
160
+ if (!unchangedAbandonment && disposition === "abandoned" && (!covered?.merged || !covered.verified)) {
161
+ check("delivery-coverage", "Abandonment does not discard unverified local commits; only the exact unchanged baseline is disposable.", { baseline: baseline ?? null, verifiedDelivery: true }, observed);
162
+ }
163
+ else if (!unchangedAbandonment && expected != null && expected !== observed && covered?.deliveryLocalCommit !== observed) {
127
164
  check("head-mismatch", "Task main HEAD differs from the accepted head and its covered publication candidate.", { acceptedCommit: expected, deliveryLocalCommit: covered?.deliveryLocalCommit ?? null }, observed);
128
165
  }
129
166
  }
@@ -136,6 +173,49 @@ export async function inspectWorkspaceCleanup(store, git, workspace, disposition
136
173
  }
137
174
  return checks;
138
175
  }
176
+ /** Inspect the complete logical container before removing any Project. Files
177
+ * and unrecognized links are evidence, not disposable workspace scaffolding.
178
+ */
179
+ export async function inspectWorkspaceContainer(workspace) {
180
+ if (workspace.owner.type === "integration-attempt")
181
+ return []; // root is the Git worktree itself
182
+ const resource = managedWorkspaceKey(workspace.owner);
183
+ const sources = [resource];
184
+ const actions = workspaceCleanupActions(workspace);
185
+ try {
186
+ const root = await lstat(workspace.root).catch(error => {
187
+ if (error.code === "ENOENT")
188
+ return null;
189
+ throw error;
190
+ });
191
+ if (root === null)
192
+ return [];
193
+ if (!root.isDirectory() || root.isSymbolicLink())
194
+ return [{
195
+ resource, reason: "workspace-identity-mismatch", status: "blocked",
196
+ detail: "Logical workspace container is not an owned directory.",
197
+ expected: "directory", observed: "different filesystem type", sources, actions
198
+ }];
199
+ const unexpected = [];
200
+ for (const entry of await readdir(workspace.root, { withFileTypes: true })) {
201
+ const owned = workspace.entries.find(e => e.directory === entry.name);
202
+ const allowed = owned !== undefined && (owned.access === "write"
203
+ ? entry.isDirectory() && !entry.isSymbolicLink()
204
+ : entry.isSymbolicLink() && resolve(workspace.root, await readlink(join(workspace.root, entry.name))) === resolve(owned.path));
205
+ if (!allowed)
206
+ unexpected.push(entry.name);
207
+ }
208
+ return unexpected.length === 0 ? [] : [{
209
+ resource, reason: "workspace-material-retained", status: "blocked",
210
+ detail: "Logical container contains material outside its registered Project entries; no Project removal was started.",
211
+ expected: "only registered Project directories/links", observed: unexpected,
212
+ sources, actions
213
+ }];
214
+ }
215
+ catch (error) {
216
+ return cleanupCheckFromError(error, resource, sources, actions);
217
+ }
218
+ }
139
219
  /** Reuse Publication's projection, supplying live heads only where its existing
140
220
  * contract asks for them (active/cancelled). Completed/archived heads stay frozen.
141
221
  * Never prepare a workspace or substitute an old binding's commit for a failed read.
@@ -166,7 +246,8 @@ export async function readArchiveDelivery(store, git, task, forceArchive = false
166
246
  export function workspaceCleanupActions(workspace) {
167
247
  const owner = workspace.owner;
168
248
  switch (owner.type) {
169
- case "task": return [`yui task remote-delivery ${owner.taskId}`, `yui task show ${owner.taskId}`];
249
+ case "task": return [`yui task remote-delivery ${owner.taskId}`, `yui task show ${owner.taskId}`,
250
+ `yui task archive-cleanup ${owner.taskId}`];
170
251
  case "work-item": return [`yui task work show ${owner.taskId}/${owner.workItemId}`,
171
252
  `yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`];
172
253
  case "review-round": return [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
@@ -10,6 +10,7 @@ import { execFile } from "node:child_process";
10
10
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
11
11
  import { basename, join, resolve } from "node:path";
12
12
  import { promisify } from "node:util";
13
+ import { readGitWorktrees } from "../repository/gitWorktreeInventory.js";
13
14
  import { managedProjectPath } from "../repository/project.js";
14
15
  import { managedRuntimeRoot } from "../storage/homeLayout.js";
15
16
  import { isResourceQuarantinePath } from "./resourceRegistry.js";
@@ -54,8 +55,7 @@ export function parseGitWorktreePorcelain(output) {
54
55
  }
55
56
  async function listGitWorktrees(repositoryPath) {
56
57
  try {
57
- const { stdout } = await executeFile("git", ["-C", repositoryPath, "worktree", "list", "--porcelain"], { timeout: 10_000 });
58
- return parseGitWorktreePorcelain(stdout);
58
+ return await readGitWorktrees(repositoryPath);
59
59
  }
60
60
  catch (error) {
61
61
  throw new Error(`Failed to list Git worktrees for ${repositoryPath}: `
@@ -16,6 +16,7 @@
16
16
  * - `restoreAllResourceGc` is the explicit rollback entry point.
17
17
  */
18
18
  import { execFileSync } from "node:child_process";
19
+ import { supportedGit } from "../repository/gitCompatibility.js";
19
20
  import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
20
21
  import { dirname, join, resolve, relative, isAbsolute } from "node:path";
21
22
  import { isDeepStrictEqual } from "node:util";
@@ -384,13 +385,14 @@ function quarantineResource(home, record, now) {
384
385
  const receiptPath = `${quarantinePath}.receipt.json`;
385
386
  try {
386
387
  if (isGitWorktreePath(record.path)) {
388
+ const program = supportedGit();
387
389
  // Move the linked worktree, including its exact checkout and Git
388
390
  // metadata, into the Home-local quarantine. Restore is the inverse
389
391
  // move, so the recorded HEAD cannot drift with a branch.
390
392
  mkdirSync(quarantineRoot, { recursive: true, mode: 0o700 });
391
393
  // Keep the existing SQLite writer fence through this bounded move, so a
392
394
  // Task reopen or new durable owner cannot commit between check and move.
393
- execFileSync("git", ["-C", record.path, "worktree", "move", "--", record.path, quarantinePath], { timeout: 30_000 });
395
+ execFileSync(program.path, ["-C", record.path, "worktree", "move", "--", record.path, quarantinePath], { timeout: 30_000 });
394
396
  writeQuarantineReceipt(home, record, now, "move", receiptPath);
395
397
  return {
396
398
  ok: true,
@@ -496,9 +498,10 @@ function restoreQuarantinedRecord(record, liveRefs, now) {
496
498
  updatedAt: now.toISOString()
497
499
  };
498
500
  }
501
+ const program = isGitWorktreePath(quarantine.path) ? supportedGit() : undefined;
499
502
  mkdirSync(dirname(quarantine.originalPath), { recursive: true });
500
- if (isGitWorktreePath(quarantine.path)) {
501
- execFileSync("git", ["-C", quarantine.path, "worktree", "move", "--", quarantine.path, quarantine.originalPath], { timeout: 30_000 });
503
+ if (program !== undefined) {
504
+ execFileSync(program.path, ["-C", quarantine.path, "worktree", "move", "--", quarantine.path, quarantine.originalPath], { timeout: 30_000 });
502
505
  }
503
506
  else {
504
507
  renameSync(quarantine.path, quarantine.originalPath);
@@ -604,7 +607,7 @@ export async function purgeResourceQuarantine(home, options, store) {
604
607
  try {
605
608
  if (existsSync(quarantine.path)) {
606
609
  if (isGitWorktreePath(quarantine.path)) {
607
- execFileSync("git", ["-C", quarantine.path, "worktree", "remove", "--force", "--", quarantine.path], { timeout: 30_000 });
610
+ execFileSync(supportedGit().path, ["-C", quarantine.path, "worktree", "remove", "--force", "--", quarantine.path], { timeout: 30_000 });
608
611
  }
609
612
  rmSync(quarantine.path, { recursive: true, force: true });
610
613
  }
@@ -9,6 +9,7 @@
9
9
  import { execFileSync } from "node:child_process";
10
10
  import { existsSync } from "node:fs";
11
11
  import { basename, dirname, resolve } from "node:path";
12
+ import { gitOutputLine, supportedGit } from "../repository/gitCompatibility.js";
12
13
  import { upsertResourceRecord } from "./resourceRegistry.js";
13
14
  import { createResourceRegistryStore } from "./resourceRegistryStore.js";
14
15
  import { createResourceRecord, isReleaseNamespacePath, resourceId } from "./resourceTypes.js";
@@ -100,15 +101,17 @@ export class ResourceRegistrar {
100
101
  })]);
101
102
  }
102
103
  markWorkspaceDeleted(workspace) {
103
- try {
104
- this.markPathsDeleted([
105
- workspace.root,
106
- ...workspace.entries.map((entry) => entry.path)
107
- ]);
108
- }
109
- catch {
110
- // Best-effort receipt; workspace deletion must proceed regardless.
111
- }
104
+ // Callers remove the managed owner only after this succeeds. Losing the
105
+ // receipt must retain that exact owner so a missing-path retry can finish.
106
+ this.#markPathsDeleted([
107
+ workspace.root,
108
+ ...workspace.entries.filter(entry => entry.access === "write").map(entry => entry.path)
109
+ ], ownerFromManagedWorkspace(this.#home, workspace.owner));
110
+ }
111
+ markTaskRuntimeDeleted(descriptor) {
112
+ // The validated descriptor and matching registry owner survive deletion.
113
+ // A missing-root retry may settle this receipt, never a different owner.
114
+ this.#markPathsDeleted(Object.values(descriptor.roots), ownerFromManagedWorkspace(this.#home, descriptor.workspace.owner));
112
115
  }
113
116
  markPathsDeleted(paths) {
114
117
  try {
@@ -118,7 +121,7 @@ export class ResourceRegistrar {
118
121
  // Best-effort receipt; path deletion must proceed regardless.
119
122
  }
120
123
  }
121
- #markPathsDeleted(paths) {
124
+ #markPathsDeleted(paths, owner) {
122
125
  const targets = new Set(paths.map((path) => resolve(path)));
123
126
  const store = createResourceRegistryStore(this.#home);
124
127
  try {
@@ -128,6 +131,10 @@ export class ResourceRegistrar {
128
131
  for (const record of Object.values(state.records)) {
129
132
  if (!targets.has(record.path) || record.disposition === "deleted")
130
133
  continue;
134
+ if (owner !== undefined && ["home", "taskId", "workItemId", "reviewRoundId", "integrationAttemptId"]
135
+ .some(key => record.owner[key] !== owner[key])) {
136
+ throw new Error("Resource registration owner changed; deletion receipt retained for inspection.");
137
+ }
131
138
  next = upsertResourceRecord(next, {
132
139
  ...record,
133
140
  activeRefs: Object.freeze([]),
@@ -206,7 +213,7 @@ function ownerFromManagedWorkspace(home, owner, projectId) {
206
213
  };
207
214
  }
208
215
  function readGitWorktreeMetadata(path) {
209
- const commonDir = execFileSync("git", ["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] }).trim();
216
+ const commonDir = resolve(path, gitOutputLine(execFileSync(supportedGit().path, ["-C", path, "rev-parse", "--git-common-dir"], { encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] })));
210
217
  const repositoryPath = basename(commonDir) === ".git" ? dirname(commonDir) : commonDir;
211
218
  let branch;
212
219
  try {
@@ -41,6 +41,12 @@ export function createAgentEndpointFactory(start = startStructuredProviderSessio
41
41
  endpoint.observe(event);
42
42
  };
43
43
  const opened = await start(payload, {
44
+ // The native TUI owns rendering while attached. Raw JSON mirroring into
45
+ // its inherited PTY would corrupt its screen on every Provider event.
46
+ mirrorOutput: (stream, text) => {
47
+ if (!endpoint?.nativeAccess?.attached)
48
+ (stream === "stdout" ? process.stdout : process.stderr).write(text);
49
+ },
44
50
  onAccepted: (value) => emit({ type: "accepted", value }),
45
51
  onActivity: (value) => emit({ type: "activity", value }),
46
52
  onStarted: (value) => emit({ type: "started", value }),
@@ -74,6 +80,7 @@ function freezeConfiguration(value) {
74
80
  class BuiltinAgentEndpoint {
75
81
  driver;
76
82
  configuration;
83
+ get nativeAccess() { return this.driver.nativeAccess; }
77
84
  get ownedProcessId() { return this.driver.ownedProcessId; }
78
85
  get nativeAccountHome() { return this.driver.nativeAccountHome; }
79
86
  capabilities;
@@ -118,6 +125,7 @@ class BuiltinAgentEndpoint {
118
125
  get nativeSessionId() { return this.driver.nativeSessionId; }
119
126
  get conversationId() { return this.driver.conversationId; }
120
127
  get processInstanceId() { return this.driver.processInstanceId; }
128
+ get publicReply() { return this.driver.publicReply; }
121
129
  /**
122
130
  * Delegated rather than copied at construction: the Session reads its own
123
131
  * current state, and a value captured here would freeze the configuration as