@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
@@ -5,7 +5,13 @@ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { cleanupFailure } from "../workspace/cleanupInspection.js";
7
7
  import { externalProgramConfigViolations } from "../artifacts/managedGit.js";
8
+ import { compatibleGitArguments, gitOutputLine, supportedGit } from "./gitCompatibility.js";
9
+ import { gitPath, readGitWorktrees } from "./gitWorktreeInventory.js";
8
10
  const executeFile = promisify(execFile);
11
+ const isolatedFetchOptions = [
12
+ "--no-tags", "--no-prune", "--no-prune-tags", "--no-recurse-submodules",
13
+ "--no-auto-gc", "--no-write-commit-graph", "--no-write-fetch-head", "--refmap="
14
+ ];
9
15
  /** Observed partial effects, not a rollback or permission to replay a refresh. */
10
16
  export class GitWorkspaceRefreshError extends Error {
11
17
  result;
@@ -160,13 +166,13 @@ export class NodeGitWorkspace {
160
166
  const remote = safeRemote(input.remoteUrl);
161
167
  const configuredBranch = await safeFetchBranch(input.branch);
162
168
  const branch = configuredBranch === "HEAD"
163
- ? await resolveRemoteHeadBranch(remote)
169
+ ? await resolveRemoteHeadBranch(remote, repositoryPath)
164
170
  : configuredBranch;
165
171
  const fetchedRef = remoteBaselineRef(remote, branch);
166
172
  try {
167
173
  await git([
168
174
  "-C", repositoryPath,
169
- "fetch", "--no-tags", "--no-write-fetch-head",
175
+ "fetch", ...isolatedFetchOptions,
170
176
  remote,
171
177
  `refs/heads/${branch}:${fetchedRef}`
172
178
  ]);
@@ -212,8 +218,7 @@ export class NodeGitWorkspace {
212
218
  // An empty refmap disables opportunistic tracking updates even when the
213
219
  // Project URL is a named remote. Ignore configured prune/submodule effects.
214
220
  await git([
215
- "-C", initial.root, "fetch", "--no-tags", "--no-prune", "--no-prune-tags",
216
- "--no-recurse-submodules", "--no-auto-maintenance", "--no-write-fetch-head", "--refmap=",
221
+ "-C", initial.root, "fetch", ...isolatedFetchOptions,
217
222
  remote, `refs/heads/${stableBranch}:${temporaryRef}`
218
223
  ]);
219
224
  fetchedCommit = await resolveFetchedCommit(initial.root, temporaryRef);
@@ -239,11 +244,11 @@ export class NodeGitWorkspace {
239
244
  // Verify the stable branch and CAS the one configured destination in
240
245
  // the same Git transaction. HEAD/index/worktree still use normal ff.
241
246
  await git(["-C", initial.root, "update-ref", "--stdin"], { input: [
242
- "start", "option no-deref",
247
+ "option no-deref",
243
248
  `verify refs/heads/${stableBranch} ${fetchedCommit}`,
244
249
  "option no-deref",
245
250
  `update ${mapping.ref} ${fetchedCommit} ${old}`,
246
- "prepare", "commit", ""
251
+ ""
247
252
  ].join("\n") });
248
253
  tracking = {
249
254
  status: previous === fetchedCommit ? "current" : "updated",
@@ -305,15 +310,15 @@ export class NodeGitWorkspace {
305
310
  async resolveRemoteBaseline(input) {
306
311
  const remote = safeRemote(input.remoteUrl);
307
312
  const configuredRef = await safeFetchBranch(input.developmentRef);
313
+ const repository = await this.inspect(input.repositoryPath, "HEAD");
308
314
  const branch = configuredRef === "HEAD"
309
- ? await resolveRemoteHeadBranch(remote)
315
+ ? await resolveRemoteHeadBranch(remote, repository.root)
310
316
  : configuredRef;
311
- const repository = await this.inspect(input.repositoryPath, "HEAD");
312
317
  const temporaryRef = remoteBaselineRef(remote, branch);
313
318
  try {
314
319
  await git([
315
320
  "-C", repository.root,
316
- "fetch", "--no-tags", "--no-write-fetch-head",
321
+ "fetch", ...isolatedFetchOptions,
317
322
  remote,
318
323
  `refs/heads/${branch}:${temporaryRef}`
319
324
  ]);
@@ -321,7 +326,7 @@ export class NodeGitWorkspace {
321
326
  "-C", repository.root,
322
327
  "rev-parse", "--verify", "--end-of-options", `${temporaryRef}^{commit}`
323
328
  ]);
324
- const advertisedCommit = await resolveRemoteBranchCommit(remote, branch);
329
+ const advertisedCommit = await resolveRemoteBranchCommit(remote, branch, repository.root);
325
330
  if (fetchedCommit.toLowerCase() !== advertisedCommit) {
326
331
  throw new Error(`Project remote development branch changed while it was fetched: ${branch}.`);
327
332
  }
@@ -341,8 +346,10 @@ export class NodeGitWorkspace {
341
346
  }
342
347
  }
343
348
  async clone(input) {
349
+ supportedGit();
344
350
  const remote = safeRemote(input.remoteUrl);
345
- const destination = resolve(requireText(input.destination, "Project destination"));
351
+ requireText(input.destination, "Project destination");
352
+ const destination = resolve(input.destination);
346
353
  if (await pathKind(destination) !== undefined) {
347
354
  throw new Error(`Project destination already exists: ${destination}.`);
348
355
  }
@@ -415,10 +422,8 @@ export class NodeGitWorkspace {
415
422
  async inspect(repositoryPath, baseRef = "HEAD") {
416
423
  const requested = await canonicalDirectory(repositoryPath, "Project");
417
424
  const ref = safeRef(baseRef);
418
- const root = await canonicalDirectory(await gitLine(["-C", requested, "rev-parse", "--show-toplevel"]), "Project root");
419
- const gitDirectory = await canonicalDirectory(await gitLine([
420
- "-C", root, "rev-parse", "--path-format=absolute", "--git-common-dir"
421
- ]), "Git common directory");
425
+ const root = await canonicalDirectory(await gitPath(requested, ["--show-toplevel"]), "Project root");
426
+ const gitDirectory = await canonicalDirectory(await gitPath(root, ["--git-common-dir"]), "Git common directory");
422
427
  const baseCommit = await gitLine([
423
428
  "-C", root, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`
424
429
  ]);
@@ -485,7 +490,7 @@ export class NodeGitWorkspace {
485
490
  const importedRef = `${importRoot}/${index}`;
486
491
  await git([
487
492
  "-C", destination.root,
488
- "fetch", "--no-tags", "--no-write-fetch-head", "--",
493
+ "fetch", ...isolatedFetchOptions, "--",
489
494
  source.root, `${snapshot.ref}:${importedRef}`
490
495
  ]);
491
496
  const imported = await resolveRefCommit(destination.root, importedRef);
@@ -567,36 +572,11 @@ export class NodeGitWorkspace {
567
572
  const excluded = input.excludeWorktreePath === undefined
568
573
  ? undefined
569
574
  : await realpath(resolve(input.excludeWorktreePath)).catch(() => undefined);
570
- const porcelain = await git(["-C", root, "worktree", "list", "--porcelain"]);
571
- const records = [];
572
- let current = { prunable: false };
573
- for (const line of porcelain.split("\n")) {
574
- if (line.length === 0) {
575
- const path = current.path;
576
- if (path !== undefined) {
577
- records.push({ path, branch: current.branch, prunable: current.prunable });
578
- }
579
- current = { prunable: false };
580
- continue;
581
- }
582
- if (line.startsWith("worktree ")) {
583
- current.path = line.slice("worktree ".length);
584
- }
585
- else if (line.startsWith("branch ")) {
586
- current.branch = line.slice("branch ".length);
587
- }
588
- else if (line.startsWith("prunable")) {
589
- current.prunable = true;
590
- }
591
- }
592
- const lastPath = current.path;
593
- if (lastPath !== undefined) {
594
- records.push({ path: lastPath, branch: current.branch, prunable: current.prunable });
595
- }
575
+ const records = await readGitWorktrees(root);
596
576
  for (const record of records) {
597
577
  // A dead registration (its directory is gone) occupies nothing; a
598
578
  // worktree on another ref or a detached HEAD does not occupy this one.
599
- if (record.prunable || record.branch !== wanted)
579
+ if ((record.prunable && !record.locked) || record.branch !== wanted)
600
580
  continue;
601
581
  // git reports canonical absolute paths; compare canonicals so a
602
582
  // symlinked workspace root cannot make the recorded worktree look
@@ -724,11 +704,7 @@ export class NodeGitWorkspace {
724
704
  }
725
705
  async listWorktrees(repositoryPath) {
726
706
  const root = await canonicalDirectory(repositoryPath, "Project");
727
- const output = await git(["-C", root, "worktree", "list", "--porcelain"]);
728
- return output
729
- .split(/\r?\n/u)
730
- .filter((line) => line.startsWith("worktree "))
731
- .map((line) => line.slice("worktree ".length).trim());
707
+ return (await readGitWorktrees(root)).map(entry => entry.path);
732
708
  }
733
709
  async listCommitsBetween(input) {
734
710
  const root = await canonicalDirectory(input.repositoryPath, "Project");
@@ -821,7 +797,7 @@ export class NodeGitWorkspace {
821
797
  const state = await this.inspectWorktree(input);
822
798
  if (state === "dirty")
823
799
  return state;
824
- const container = await canonicalContainer(input.container, false);
800
+ const container = await cleanupContainer(input.container);
825
801
  const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
826
802
  const path = managedPath(container, input.directory);
827
803
  const project = await this.inspect(input.repositoryPath);
@@ -919,9 +895,8 @@ export class NodeGitWorkspace {
919
895
  await assertExpectedBranch(expected, input.branch);
920
896
  if (!await this.inspectClean(expected))
921
897
  return "dirty";
922
- const worktrees = (await git(["-C", expected, "worktree", "list", "--porcelain", "-z"]))
923
- .split("\0").filter(line => line.startsWith("worktree "));
924
- if (worktrees.length !== 1 || worktrees[0] !== `worktree ${expected}`) {
898
+ const worktrees = await readGitWorktrees(expected);
899
+ if (worktrees.length !== 1 || worktrees[0].path !== expected) {
925
900
  cleanupFailure("git-worktree-registrations", "Task clone still owns Git worktree registrations; retained.", 1, worktrees.length);
926
901
  }
927
902
  return "clean";
@@ -970,13 +945,13 @@ export class NodeGitWorkspace {
970
945
  return "removed";
971
946
  }
972
947
  async inspectWorktree(input) {
973
- const container = await canonicalContainer(input.container, false);
948
+ const container = await cleanupContainer(input.container);
974
949
  const path = managedPath(container, input.directory);
975
950
  const kind = await pathKind(path);
976
951
  const branch = input.expectedBranch ?? worktreeIdentity(input.taskSegment, input.roleName).branch;
977
952
  if (kind === undefined) {
978
953
  const project = await this.inspect(input.repositoryPath);
979
- await inspectMissingWorktreeRegistration(project.root, path, branch);
954
+ await inspectMissingWorktreeRegistration(project.root, path, branch, input.expectedBranch !== undefined);
980
955
  return "missing";
981
956
  }
982
957
  if (kind === "symlink")
@@ -984,7 +959,7 @@ export class NodeGitWorkspace {
984
959
  const canonicalContainerPath = await canonicalContainer(container, false);
985
960
  const project = await this.inspect(input.repositoryPath);
986
961
  await assertOwnedWorktree(project, canonicalContainerPath, path);
987
- await assertExpectedBranch(path, branch);
962
+ await assertExpectedBranch(path, branch, input.expectedBranch !== undefined);
988
963
  const porcelain = await readWorktreeStatus(path);
989
964
  return porcelain.length > 0 ? "dirty" : "clean";
990
965
  }
@@ -1000,19 +975,19 @@ export class NodeGitWorkspace {
1000
975
  });
1001
976
  }
1002
977
  async #removeManagedWorktree(input) {
1003
- const container = await canonicalContainer(input.container, false);
978
+ const container = await cleanupContainer(input.container);
1004
979
  const path = managedPath(container, input.identity.directory);
1005
980
  const kind = await pathKind(path);
1006
981
  if (kind === "symlink")
1007
982
  throw new Error("Managed worktree path must not be a symbolic link.");
1008
983
  const repository = await this.inspect(input.repositoryPath);
1009
984
  if (kind === undefined) {
1010
- await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch);
985
+ await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch, true);
1011
986
  }
1012
987
  if (kind === "directory") {
1013
988
  const canonicalContainerPath = await canonicalContainer(container, false);
1014
989
  await assertOwnedWorktree(repository, canonicalContainerPath, path);
1015
- await assertExpectedBranch(path, input.identity.branch);
990
+ await assertExpectedBranch(path, input.identity.branch, true);
1016
991
  if (input.discardChanges === true) {
1017
992
  await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
1018
993
  }
@@ -1043,21 +1018,22 @@ export class NodeGitWorkspace {
1043
1018
  /** Missing paths do not imply missing Git metadata. Never prune another
1044
1019
  * workspace's registration as a side effect of cleaning this exact owner.
1045
1020
  */
1046
- async function removeMissingWorktreeRegistration(repositoryRoot, path, branch) {
1047
- if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch))
1021
+ async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1022
+ if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached))
1048
1023
  return;
1049
1024
  if (await pathKind(path) !== undefined)
1050
1025
  throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
1051
1026
  await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
1052
1027
  }
1053
- async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch) {
1054
- const records = (await git(["-C", repositoryRoot, "worktree", "list", "--porcelain", "-z"])).split("\0\0");
1055
- const fields = records.map(record => record.split("\0")).find(record => record.includes(`worktree ${path}`));
1056
- if (fields === undefined)
1028
+ async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1029
+ const entry = (await readGitWorktrees(repositoryRoot)).find(record => record.path === path);
1030
+ if (entry === undefined)
1057
1031
  return false;
1058
- if (!fields.includes(`branch refs/heads/${branch}`) || fields.some(field => field.startsWith("locked"))) {
1059
- cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: fields.find(field => field.startsWith("branch "))?.slice(7) ?? null,
1060
- locked: fields.some(field => field.startsWith("locked")) });
1032
+ const pinnedDetached = allowDetached && entry.detached
1033
+ && await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
1034
+ && entry.head === await resolveRefCommit(repositoryRoot, `refs/heads/${branch}`);
1035
+ if ((entry.branch !== `refs/heads/${branch}` && !pinnedDetached) || entry.locked) {
1036
+ cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: entry.branch ?? null, locked: entry.locked });
1061
1037
  }
1062
1038
  return true;
1063
1039
  }
@@ -1100,8 +1076,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
1100
1076
  // Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
1101
1077
  // identify this exact Integration branch; unrelated detachment is invalid.
1102
1078
  for (const backend of ["rebase-merge", "rebase-apply"]) {
1103
- const headNamePath = await gitLine(["-C", path, "rev-parse", "--path-format=absolute",
1104
- "--git-path", `${backend}/head-name`]);
1079
+ const headNamePath = await gitPath(path, ["--git-path", `${backend}/head-name`]);
1105
1080
  let headName;
1106
1081
  try {
1107
1082
  headName = (await readFile(headNamePath, "utf8")).trim();
@@ -1115,7 +1090,12 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
1115
1090
  return;
1116
1091
  throw new Error(`Managed worktree is rebasing an unexpected branch: ${headName}.`);
1117
1092
  }
1118
- throw new Error("Managed worktree has detached HEAD without its expected rebase.");
1093
+ // A terminal Integration can legitimately retain a detached checkout.
1094
+ // Its exact owned path/common-dir were checked by the caller; require its
1095
+ // retained branch to pin the same commit before allowing clean removal.
1096
+ if (await resolveRefCommit(path, "HEAD") === await resolveRefCommit(path, `refs/heads/${expected}`))
1097
+ return;
1098
+ cleanupFailure("workspace-identity-mismatch", "Detached worktree is not pinned by its owned branch or rebase.", expected, "unproven detached HEAD");
1119
1099
  }
1120
1100
  const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
1121
1101
  if (branch !== expected) {
@@ -1127,10 +1107,10 @@ async function assertOwnedWorktree(project, container, path) {
1127
1107
  assertContained(container, canonicalPath);
1128
1108
  if (canonicalPath !== path)
1129
1109
  throw new Error("Managed worktree resolves through a symbolic link.");
1130
- const root = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--show-toplevel"]), "Managed worktree root");
1110
+ const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Managed worktree root");
1131
1111
  if (root !== path)
1132
1112
  throw new Error("Managed worktree root does not match its deterministic path.");
1133
- const common = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Managed Git common directory");
1113
+ const common = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Managed Git common directory");
1134
1114
  if (common !== project.gitDirectory) {
1135
1115
  cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
1136
1116
  }
@@ -1154,7 +1134,7 @@ async function inspectExactRecordedWorktree(input) {
1154
1134
  if (path !== expectedPath) {
1155
1135
  throw new Error("Recorded managed worktree resolves through a symbolic link.");
1156
1136
  }
1157
- const root = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--show-toplevel"]), "Recorded managed worktree root");
1137
+ const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Recorded managed worktree root");
1158
1138
  if (root !== path) {
1159
1139
  throw new Error("Recorded managed worktree root does not match its deterministic path.");
1160
1140
  }
@@ -1162,7 +1142,7 @@ async function inspectExactRecordedWorktree(input) {
1162
1142
  if (branch !== input.branch) {
1163
1143
  throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
1164
1144
  }
1165
- const gitDirectory = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Recorded managed Git common directory");
1145
+ const gitDirectory = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Recorded managed Git common directory");
1166
1146
  const destination = await new NodeGitWorkspace().inspect(input.repositoryPath);
1167
1147
  const head = await resolveRefCommit(path, "HEAD");
1168
1148
  const retained = await resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
@@ -1237,9 +1217,30 @@ async function canonicalContainer(path, create) {
1237
1217
  // stay rejected by the managed-path lstat checks.
1238
1218
  return canonicalDirectory(lexical, "Worktree container");
1239
1219
  }
1220
+ /** A previous removal may have deleted the whole owner container before its
1221
+ * receipt committed. Resolve only surviving ancestors, without creating it.
1222
+ * Git registration inspection remains mandatory even when this path is absent.
1223
+ */
1224
+ async function cleanupContainer(path) {
1225
+ const lexical = resolve(path);
1226
+ if (await pathKind(lexical) === "symlink") {
1227
+ cleanupFailure("workspace-identity-mismatch", "Cleanup container must not be a symbolic link.", "owned directory or missing path", "symbolic link");
1228
+ }
1229
+ try {
1230
+ return await canonicalContainer(lexical, false);
1231
+ }
1232
+ catch (error) {
1233
+ if (!isErrno(error, "ENOENT"))
1234
+ throw error;
1235
+ const parent = dirname(lexical);
1236
+ if (parent === lexical)
1237
+ throw error;
1238
+ return join(await cleanupContainer(parent), relative(parent, lexical));
1239
+ }
1240
+ }
1240
1241
  async function canonicalDirectory(path, label) {
1241
- const value = requireText(path, label);
1242
- const canonical = await realpath(isAbsolute(value) ? value : resolve(value));
1242
+ requireText(path, label);
1243
+ const canonical = await realpath(isAbsolute(path) ? path : resolve(path));
1243
1244
  return canonical;
1244
1245
  }
1245
1246
  function managedPath(container, directory) {
@@ -1278,7 +1279,7 @@ async function readWorktreeStatus(path) {
1278
1279
  return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
1279
1280
  }
1280
1281
  async function readStatusGit(path, args) {
1281
- return git(["--no-optional-locks", "-c", "core.fsmonitor=false",
1282
+ return git(["--no-optional-locks", "-c", "core.fsmonitor=",
1282
1283
  "-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
1283
1284
  "-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
1284
1285
  }
@@ -1324,8 +1325,10 @@ async function assertStatusProgramsAbsent(path) {
1324
1325
  }
1325
1326
  }
1326
1327
  async function git(args, options = {}) {
1328
+ const program = supportedGit();
1329
+ const invocation = compatibleGitArguments(args);
1327
1330
  try {
1328
- const execution = executeFile("git", [...args], {
1331
+ const execution = executeFile(program.path, invocation, {
1329
1332
  encoding: "utf8",
1330
1333
  maxBuffer: 1024 * 1024,
1331
1334
  timeout: 30_000,
@@ -1346,6 +1349,7 @@ async function git(args, options = {}) {
1346
1349
  }
1347
1350
  }
1348
1351
  async function gitSucceeds(args) {
1352
+ supportedGit();
1349
1353
  try {
1350
1354
  await git(args);
1351
1355
  return true;
@@ -1355,7 +1359,7 @@ async function gitSucceeds(args) {
1355
1359
  }
1356
1360
  }
1357
1361
  async function gitLine(args) {
1358
- const lines = (await git(args)).trimEnd().split("\n");
1362
+ const lines = gitOutputLine(await git(args)).split("\n");
1359
1363
  if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
1360
1364
  throw new Error("Git returned invalid output.");
1361
1365
  }
@@ -1504,6 +1508,7 @@ function safeRef(value) {
1504
1508
  return ref;
1505
1509
  }
1506
1510
  async function safeFetchBranch(value) {
1511
+ supportedGit();
1507
1512
  const configuredRef = safeRef(value);
1508
1513
  if (configuredRef === "HEAD")
1509
1514
  return configuredRef;
@@ -1595,7 +1600,8 @@ function isCommit(value) {
1595
1600
  return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
1596
1601
  }
1597
1602
  function safeRemote(value) {
1598
- const remote = requireText(value, "Git remote URL");
1603
+ requireText(value, "Git remote URL");
1604
+ const remote = value;
1599
1605
  if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
1600
1606
  throw new Error("Git remote URL is invalid.");
1601
1607
  }
@@ -0,0 +1,121 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import { gitOutputLine, supportedGit } from "./gitCompatibility.js";
6
+ const execute = promisify(execFile);
7
+ async function run(args) {
8
+ return (await execute(supportedGit().path, args, {
9
+ encoding: "utf8", timeout: 10_000, maxBuffer: 1024 * 1024
10
+ })).stdout;
11
+ }
12
+ export async function gitPath(cwd, args) {
13
+ return resolve(cwd, gitOutputLine(await run(["-C", cwd, "rev-parse", ...args])));
14
+ }
15
+ async function exists(path) {
16
+ try {
17
+ await lstat(path);
18
+ return true;
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return false;
23
+ throw error;
24
+ }
25
+ }
26
+ /**
27
+ * Old porcelain omits locks and does not delimit/quote arbitrary paths. Read
28
+ * Git's worktree administrative records instead. These are observations only:
29
+ * never write/prune them, and never infer ownership merely from a directory.
30
+ * Mutating consumers still check the recorded owner and ask Git to remove it.
31
+ */
32
+ export async function readGitWorktrees(cwd) {
33
+ const common = await realpath(await gitPath(cwd, ["--git-common-dir"]));
34
+ const result = [];
35
+ // This is also Git's own main-worktree listing convention (including bare
36
+ // and separate-git-dir repositories), not a claim of managed ownership.
37
+ const main = basename(common) === ".git" ? dirname(common) : common;
38
+ result.push(await inspect(common, main, false));
39
+ const directory = join(common, "worktrees");
40
+ let entries;
41
+ try {
42
+ entries = await readdir(directory, { withFileTypes: true });
43
+ }
44
+ catch (error) {
45
+ if (error.code === "ENOENT")
46
+ return result;
47
+ throw error;
48
+ }
49
+ for (const entry of entries) {
50
+ if (!entry.isDirectory())
51
+ throw new Error(`Uncertain Git worktree administration: ${join(directory, entry.name)}; retained.`);
52
+ const admin = join(directory, entry.name);
53
+ const pointer = gitOutputLine(await readFile(join(admin, "gitdir"), "utf8"));
54
+ if (basename(pointer) !== ".git") {
55
+ throw new Error(`Invalid Git worktree back-pointer: ${admin}; retained.`);
56
+ }
57
+ // Relative back-pointers are based at the administrative directory,
58
+ // not the caller's cwd (Git's worktree.useRelativePaths setting).
59
+ const path = dirname(resolve(admin, pointer));
60
+ const adminCommon = await realpath(resolve(cwd, gitOutputLine(await run([
61
+ "-C", cwd, `--git-dir=${admin}`, "rev-parse", "--git-common-dir"
62
+ ]))));
63
+ if (adminCommon !== common)
64
+ throw new Error(`Git worktree common directory mismatch: ${admin}; retained.`);
65
+ if (await exists(path)) {
66
+ const actual = await realpath(await gitPath(path, ["--git-dir"]));
67
+ if (actual !== admin)
68
+ throw new Error(`Git worktree back-pointer mismatch: ${path}; retained.`);
69
+ }
70
+ result.push(await inspect(admin, path, true));
71
+ }
72
+ if (new Set(result.map(entry => entry.path)).size !== result.length) {
73
+ throw new Error("Duplicate Git worktree registrations; retained.");
74
+ }
75
+ return result;
76
+ }
77
+ async function inspect(admin, path, linked) {
78
+ // Ask Git rather than parsing HEAD: Git owns ref storage (including newer
79
+ // backends), symbolic resolution and per-worktree pseudorefs.
80
+ let branch;
81
+ try {
82
+ branch = gitOutputLine(await run([`--git-dir=${admin}`, "symbolic-ref", "--quiet", "HEAD"]));
83
+ }
84
+ catch (error) {
85
+ if (error.code !== 1)
86
+ throw error;
87
+ }
88
+ if (branch !== undefined && !branch.startsWith("refs/")) {
89
+ throw new Error(`Invalid Git worktree HEAD: ${admin}; retained.`);
90
+ }
91
+ let head;
92
+ try {
93
+ head = gitOutputLine(await run([`--git-dir=${admin}`, "rev-parse", "--verify", "HEAD"]));
94
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(head))
95
+ throw new Error("Invalid Git HEAD");
96
+ }
97
+ catch (error) {
98
+ // A genuinely unborn branch (main or linked) is valid discovery, not cleanup proof.
99
+ if (branch === undefined)
100
+ throw error;
101
+ try {
102
+ await run([`--git-dir=${admin}`, "show-ref", "--verify", "--quiet", branch]);
103
+ }
104
+ catch (missing) {
105
+ if (missing.code === 1) {
106
+ return {
107
+ path, branch, detached: false, prunable: !await exists(path),
108
+ locked: linked && await exists(join(admin, "locked"))
109
+ };
110
+ }
111
+ throw missing;
112
+ }
113
+ throw error;
114
+ }
115
+ return {
116
+ path, ...(head === undefined ? {} : { head }),
117
+ ...(branch === undefined ? {} : { branch }),
118
+ detached: branch === undefined, prunable: !await exists(path),
119
+ locked: linked && await exists(join(admin, "locked"))
120
+ };
121
+ }