@zq-silk/yui 2.3.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 (183) hide show
  1. package/README.md +15 -1
  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 +17 -8
  7. package/dist/cli/invocationAuthority.js +1 -1
  8. package/dist/commands/executionAuditCommands.js +5 -0
  9. package/dist/commands/globalRoleCommands.js +41 -3
  10. package/dist/commands/projectCommands.js +89 -94
  11. package/dist/commands/roleSkillValidation.js +4 -8
  12. package/dist/commands/taskCommands.js +25 -2
  13. package/dist/commands/taskUsageCommand.js +38 -0
  14. package/dist/context/roleSessionContext.js +18 -2
  15. package/dist/context/runContextPack.js +5 -1
  16. package/dist/context/sessionBootstrapManifest.js +10 -1
  17. package/dist/context/skillCatalog.js +108 -0
  18. package/dist/context/skillPackage.js +161 -0
  19. package/dist/context/taskContext.js +3 -2
  20. package/dist/context/taskSearch.js +127 -0
  21. package/dist/controlPlaneCli.js +25 -20
  22. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  23. package/dist/controller/jobControl.js +2 -0
  24. package/dist/controller/nativeHostInput.js +37 -0
  25. package/dist/controller/runtime.js +93 -2
  26. package/dist/controller/runtimeEventInbox.js +2 -2
  27. package/dist/controller/runtimeEventProcessor.js +3 -3
  28. package/dist/doctor/doctor.js +10 -0
  29. package/dist/execution/reviewMainRun.js +1 -0
  30. package/dist/execution/workItemMainRun.js +1 -0
  31. package/dist/executor/codexConfigConflict.js +6 -4
  32. package/dist/executor/effectiveLaunch.js +24 -1
  33. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  34. package/dist/integration/gitIntegrationService.js +7 -12
  35. package/dist/integration/integrationSourceApplication.js +4 -2
  36. package/dist/job/httpPreview.js +61 -0
  37. package/dist/job/jobRunner.js +12 -2
  38. package/dist/kernel/builtinCapabilities.js +3 -1
  39. package/dist/message/message.js +15 -1
  40. package/dist/output/rolePresentation.js +4 -1
  41. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  42. package/dist/repository/gitCompatibility.js +62 -0
  43. package/dist/repository/gitWorkspace.js +48 -71
  44. package/dist/repository/gitWorktreeInventory.js +121 -0
  45. package/dist/repository/project.js +74 -25
  46. package/dist/resources/resourceDiscovery.js +2 -2
  47. package/dist/resources/resourceGc.js +7 -4
  48. package/dist/resources/resourceRegistrar.js +2 -1
  49. package/dist/runtime/agentEndpoint.js +8 -0
  50. package/dist/runtime/agentHost.js +170 -3
  51. package/dist/runtime/codexAppServerRuntime.js +1 -1
  52. package/dist/runtime/codexNativeAccess.js +175 -0
  53. package/dist/runtime/controlledCodexTui.js +116 -0
  54. package/dist/runtime/publicReply.js +23 -0
  55. package/dist/runtime/runtimeObservation.js +48 -0
  56. package/dist/runtime/structuredProviderHost.js +16 -7
  57. package/dist/runtime/taskCostMetrics.js +133 -0
  58. package/dist/runtime/taskUsageMetrics.js +11 -2
  59. package/dist/runtime/taskUsageQuery.js +38 -0
  60. package/dist/scheduler/taskExecutionProjection.js +2 -0
  61. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  62. package/dist/storage/homeLayout.js +13 -0
  63. package/dist/storage/sqliteSchema.js +44 -0
  64. package/dist/storage/sqliteStore.js +53 -0
  65. package/dist/storage/storageVersions.js +1 -1
  66. package/dist/storage/storeRpc.js +2 -0
  67. package/dist/storage/taskSearch.js +35 -0
  68. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  69. package/dist/task/ordinaryArchive.js +42 -0
  70. package/dist/tmux/tmuxManager.js +8 -8
  71. package/dist/web/assets/assetManifest.js +11 -51
  72. package/dist/web/assets/client/app/catalog.js +215 -0
  73. package/dist/web/assets/client/app/main.js +148 -0
  74. package/dist/web/assets/client/app/selection.js +139 -0
  75. package/dist/web/assets/client/app/settings.js +326 -0
  76. package/dist/web/assets/client/app/taskView.js +280 -0
  77. package/dist/web/assets/client/domain/context.js +96 -0
  78. package/dist/web/assets/client/domain/records.js +126 -0
  79. package/dist/web/assets/client/domain/runs.js +119 -0
  80. package/dist/web/assets/client/domain/taskForms.js +197 -0
  81. package/dist/web/assets/client/domain/vocab.js +69 -0
  82. package/dist/web/assets/client/domain/work.js +116 -0
  83. package/dist/web/assets/client/i18n/en.js +900 -0
  84. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  85. package/dist/web/assets/client/index.js +114 -0
  86. package/dist/web/assets/client/layout/resizer.js +37 -0
  87. package/dist/web/assets/client/layout/router.js +35 -0
  88. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  89. package/dist/web/assets/client/layout/sizing.js +95 -0
  90. package/dist/web/assets/client/layout/workspace.js +247 -0
  91. package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
  92. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  93. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  94. package/dist/web/assets/client/lib/i18n.js +54 -0
  95. package/dist/web/assets/client/lib/markdown.js +167 -0
  96. package/dist/web/assets/client/lib/prefs.js +28 -0
  97. package/dist/web/assets/client/lib/theme.js +68 -0
  98. package/dist/web/assets/client/ui/containers.js +101 -0
  99. package/dist/web/assets/client/ui/controls.js +51 -0
  100. package/dist/web/assets/client/ui/forms.js +48 -0
  101. package/dist/web/assets/client/ui/metrics.js +137 -0
  102. package/dist/web/assets/client/ui/primitives.js +92 -0
  103. package/dist/web/assets/client/ui/text.js +33 -0
  104. package/dist/web/assets/client/ui/toast.js +12 -0
  105. package/dist/web/assets/client/views/bodySearch.js +172 -0
  106. package/dist/web/assets/client/views/createTask.js +85 -0
  107. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  108. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  109. package/dist/web/assets/client/views/dock/preview.js +129 -0
  110. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  111. package/dist/web/assets/client/views/globalInput.js +67 -0
  112. package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
  113. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  114. package/dist/web/assets/client/views/skills.js +152 -0
  115. package/dist/web/assets/client/views/task/actions.js +36 -0
  116. package/dist/web/assets/client/views/task/delivery.js +185 -0
  117. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  118. package/dist/web/assets/client/views/task/files.js +272 -0
  119. package/dist/web/assets/client/views/task/observation.js +153 -0
  120. package/dist/web/assets/client/views/task/overview.js +161 -0
  121. package/dist/web/assets/client/views/task/page.js +131 -0
  122. package/dist/web/assets/client/views/task/records.js +163 -0
  123. package/dist/web/assets/client/views/task/runtime.js +130 -0
  124. package/dist/web/assets/client/views/task/timeline.js +190 -0
  125. package/dist/web/assets/shared/geometry.js +19 -0
  126. package/dist/web/assets/shared/taskTabs.js +8 -0
  127. package/dist/web/assets/shared/themes.js +93 -0
  128. package/dist/web/assets/shell/dialogs.js +63 -0
  129. package/dist/web/assets/shell/dock.js +40 -0
  130. package/dist/web/assets/shell/index.js +40 -0
  131. package/dist/web/assets/shell/settings.js +34 -0
  132. package/dist/web/assets/shell/sidebar.js +45 -0
  133. package/dist/web/assets/styles/index.js +41 -0
  134. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
  135. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  136. package/dist/web/assets/styles/markdown.js +22 -3
  137. package/dist/web/assets/styles/tokens.js +10 -63
  138. package/dist/web/assets/styles/ui/controls.js +76 -0
  139. package/dist/web/assets/styles/ui/forms.js +46 -0
  140. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  141. package/dist/web/assets/styles/views/delivery.js +29 -0
  142. package/dist/web/assets/styles/views/dock.js +71 -0
  143. package/dist/web/assets/styles/views/page.js +32 -0
  144. package/dist/web/assets/styles/views/records.js +70 -0
  145. package/dist/web/assets/styles/views/settings.js +26 -0
  146. package/dist/web/assets/styles/views/sidebar.js +74 -0
  147. package/dist/web/assets/styles/views/task.js +80 -0
  148. package/dist/web/assets/styles/views/terminal.js +38 -0
  149. package/dist/web/taskPreviews.js +205 -0
  150. package/dist/web/tmuxWebTerminal.js +16 -9
  151. package/dist/web/webConversation.js +373 -0
  152. package/dist/web/webMaterials.js +155 -0
  153. package/dist/web/webServer.js +295 -18
  154. package/dist/web/webSettings.js +331 -0
  155. package/dist/web/webSkills.js +86 -0
  156. package/dist/web/webSnapshot.js +40 -8
  157. package/dist/web/webTaskSurface.js +106 -8
  158. package/dist/workspace/gitChangeSetCapture.js +3 -1
  159. package/docs/managed-turn-and-session-runtime.md +9 -0
  160. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  161. package/docs/release-workflow.md +28 -0
  162. package/docs/release-workflow.zh-CN.md +21 -0
  163. package/docs/storage-baseline.md +38 -1
  164. package/docs/storage-baseline.zh-CN.md +22 -0
  165. package/docs/task-discovery.md +28 -0
  166. package/docs/testing/verification-levels.md +2 -2
  167. package/i18n/README.zh-CN.md +1 -1
  168. package/package.json +1 -1
  169. package/dist/web/assets/client/app.js +0 -808
  170. package/dist/web/assets/client/components.js +0 -477
  171. package/dist/web/assets/client/detail.js +0 -379
  172. package/dist/web/assets/client/dock.js +0 -309
  173. package/dist/web/assets/client/evidence.js +0 -364
  174. package/dist/web/assets/client/forms.js +0 -230
  175. package/dist/web/assets/client/i18n.js +0 -1277
  176. package/dist/web/assets/client/markdown.js +0 -114
  177. package/dist/web/assets/client/records.js +0 -198
  178. package/dist/web/assets/client/sections.js +0 -349
  179. package/dist/web/assets/client/theme.js +0 -71
  180. package/dist/web/assets/shell.js +0 -161
  181. package/dist/web/assets/styles/components.js +0 -216
  182. package/dist/web/assets/styles/views.js +0 -309
  183. /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
@@ -59,7 +59,8 @@ const definitions = [
59
59
  {
60
60
  name: "artifact.save", summary: "Save a file artifact by relativePath into the Task's local Git repository; returns a commit-pinned reference.",
61
61
  effect: "local-mutation", requiredPermissions: ["task:read"], source: "artifacts.saveArtifactCapability",
62
- inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
62
+ inputSchema: object({ taskId: text, relativePath: text, content: { type: "string" },
63
+ encoding: { enum: ["utf8", "base64"] }, message: text, expectedHead: text }, ["taskId", "relativePath", "content"]),
63
64
  outputSchema: { type: "object", required: ["taskId", "commit", "relativePath"] }
64
65
  },
65
66
  {
@@ -319,6 +320,7 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
319
320
  if (name === "artifact.save")
320
321
  return saveArtifactCapability(store.rootDirectory(), taskId, {
321
322
  relativePath: params.relativePath, content: params.content,
323
+ ...(params.encoding === undefined ? {} : { encoding: params.encoding }),
322
324
  ...(params.message === undefined ? {} : { message: params.message }),
323
325
  ...(params.expectedHead === undefined ? {} : { expectedHead: params.expectedHead })
324
326
  });
@@ -42,6 +42,9 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
42
42
  : { inputControl: {
43
43
  action: context.inputControl.action,
44
44
  requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
45
+ ...(context.inputControl.expectedSessionId === undefined ? {} : {
46
+ expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
47
+ }),
45
48
  ...(context.inputControl.expectedTarget === undefined
46
49
  ? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
47
50
  } }),
@@ -236,6 +239,8 @@ export function validateTaskMessage(message) {
236
239
  throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
237
240
  }
238
241
  requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
242
+ if (message.inputControl.expectedSessionId !== undefined)
243
+ requireText(message.inputControl.expectedSessionId, "Message Session");
239
244
  if (message.inputControl.expectedTarget !== undefined) {
240
245
  requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
241
246
  if (message.inputControl.action !== "steer") {
@@ -317,7 +322,11 @@ export function validateTaskMessage(message) {
317
322
  throw new Error("A recipient requires an exact WorkItem or ReviewRound.");
318
323
  }
319
324
  }
320
- if (message.continuation !== undefined && message.recipient?.ownerRunId === undefined) {
325
+ const selectedSessionNondelivery = message.inputControl?.action === "queue"
326
+ && message.inputControl.expectedSessionId !== undefined
327
+ && message.continuation?.runId === undefined
328
+ && message.continuation?.notDeliveredReason !== undefined;
329
+ if (message.continuation !== undefined && message.recipient?.ownerRunId === undefined && !selectedSessionNondelivery) {
321
330
  throw new Error("Message continuation requires an owner Assignment.");
322
331
  }
323
332
  if (message.continuation?.runId !== undefined) {
@@ -373,6 +382,9 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
373
382
  : { inputControl: {
374
383
  action: context.inputControl.action,
375
384
  requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
385
+ ...(context.inputControl.expectedSessionId === undefined ? {} : {
386
+ expectedSessionId: requireText(context.inputControl.expectedSessionId, "Message Session")
387
+ }),
376
388
  ...(context.inputControl.expectedTarget === undefined
377
389
  ? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
378
390
  } }),
@@ -404,6 +416,8 @@ export function validateGlobalRoleMessage(message) {
404
416
  throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
405
417
  }
406
418
  requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
419
+ if (message.inputControl.expectedSessionId !== undefined)
420
+ requireText(message.inputControl.expectedSessionId, "Message Session");
407
421
  if (message.inputControl.expectedTarget !== undefined) {
408
422
  requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
409
423
  if (message.inputControl.action !== "steer") {
@@ -22,7 +22,10 @@ export function renderRoleLaunchComparison(role, effective) {
22
22
  permission(binding),
23
23
  current.defaultAccess
24
24
  ];
25
- }), defaultTableWidth());
25
+ }), defaultTableWidth()) + "\n\nSkill package evidence:\n" + (effective.skillPackages === undefined
26
+ ? " Legacy / externally recorded: complete package versions were not recorded."
27
+ : effective.skillPackages.map(skill => ` ${skill.id} @ ${skill.digest}\n ${skill.source.kind}: ${skill.source.path}\n`
28
+ + ` ${skill.fileCount} files; ${skill.byteSize} bytes; inventory: ${skill.manifestPath}`).join("\n"));
26
29
  }
27
30
  export function activeRoleSummary(role) {
28
31
  const binding = role.agentBindings[role.activeAgentId];
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { supportedGit } from "./gitCompatibility.js";
3
4
  /**
4
5
  * Freeze the exact committed heads of a durable managed Lane workspace at the
5
6
  * synchronous runtime-terminalization boundary, before opening the result
@@ -67,7 +68,7 @@ export function snapshotExecutionLaneWorkspaceSync(store, workspace) {
67
68
  }
68
69
  function git(cwd, args) {
69
70
  try {
70
- return execFileSync("git", args, {
71
+ return execFileSync(supportedGit().path, args, {
71
72
  cwd,
72
73
  encoding: "utf8",
73
74
  timeout: 5_000,
@@ -0,0 +1,62 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { resolveExecutable } from "../external/pinnedCommandRunner.js";
4
+ // The fetch isolation boundary needs --no-write-fetch-head. Older Git cannot
5
+ // implement it by saving/restoring FETCH_HEAD without racing another writer.
6
+ export const MINIMUM_GIT_VERSION = "2.29.0";
7
+ export function gitVersionSupport(output) {
8
+ const match = /^git version (\d+)\.(\d+)(?:\.(\d+))?(?:[.\s-].*)?$/u.exec(output.trim());
9
+ const version = match === null ? "unknown" : `${match[1]}.${match[2]}.${match[3] ?? "0"}`;
10
+ const supported = match !== null && (Number(match[1]) > 2
11
+ || (Number(match[1]) === 2 && Number(match[2]) >= 29));
12
+ return {
13
+ version, supported,
14
+ detail: `Git ${version}; requires >= ${MINIMUM_GIT_VERSION} (isolated fetch without writing FETCH_HEAD). `
15
+ + "Read-only version check; repository permissions, transport and server capabilities are not tested."
16
+ };
17
+ }
18
+ let cached;
19
+ /** Pin the executable checked, reusing only an unchanged on-disk identity. */
20
+ export function supportedGit() {
21
+ const selected = resolveExecutable("git", process.env.PATH);
22
+ if (selected === undefined)
23
+ throw new Error("Git executable not found in PATH; install a supported Git before retrying. No Git operation was started.");
24
+ const path = realpathSync(selected);
25
+ const stat = statSync(path);
26
+ const identity = `${path}:${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
27
+ if (cached?.identity === identity)
28
+ return cached;
29
+ const output = execFileSync(path, ["--version"], {
30
+ encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"]
31
+ });
32
+ const support = gitVersionSupport(output);
33
+ if (!support.supported) {
34
+ throw new Error(`${path}: ${support.detail} No Git operation was started by this call. `
35
+ + "Select a supported Git in PATH, run doctor, then retry; existing resources are retained.");
36
+ }
37
+ cached = { identity, path, version: support.version };
38
+ return cached;
39
+ }
40
+ /** Git paths are relative to the actual -C directory, not the Node process. */
41
+ export function gitOutputLine(output) {
42
+ if (!output.endsWith("\n") || output.length === 1 || output.includes("\0")) {
43
+ throw new Error("Git returned an invalid path or line.");
44
+ }
45
+ // Remove exactly Git's record terminator, never spaces/tabs in a path.
46
+ return output.slice(0, -1);
47
+ }
48
+ /** rev-parse --verify accepts exactly one validated operand on old Git.
49
+ * Keep option-looking data out before removing the newer option terminator.
50
+ */
51
+ export function compatibleGitArguments(args) {
52
+ const marker = args.indexOf("--end-of-options");
53
+ if (marker < 0)
54
+ return [...args];
55
+ const operand = args[marker + 1];
56
+ if (!args.includes("rev-parse") || !args.includes("--verify")
57
+ || marker !== args.length - 2 || operand === undefined
58
+ || operand.length === 0 || operand.startsWith("-") || /[\0\r\n]/u.test(operand)) {
59
+ throw new Error("Unsafe Git revision operand; no Git operation was started.");
60
+ }
61
+ return [...args.slice(0, marker), operand];
62
+ }
@@ -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");
@@ -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";
@@ -1051,16 +1026,14 @@ async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, a
1051
1026
  await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
1052
1027
  }
1053
1028
  async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
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)
1029
+ const entry = (await readGitWorktrees(repositoryRoot)).find(record => record.path === path);
1030
+ if (entry === undefined)
1057
1031
  return false;
1058
- const pinnedDetached = allowDetached && fields.includes("detached")
1032
+ const pinnedDetached = allowDetached && entry.detached
1059
1033
  && await gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
1060
- && fields.includes(`HEAD ${await resolveRefCommit(repositoryRoot, `refs/heads/${branch}`)}`);
1061
- if ((!fields.includes(`branch refs/heads/${branch}`) && !pinnedDetached) || fields.some(field => field.startsWith("locked"))) {
1062
- 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,
1063
- locked: fields.some(field => field.startsWith("locked")) });
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 });
1064
1037
  }
1065
1038
  return true;
1066
1039
  }
@@ -1103,8 +1076,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
1103
1076
  // Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
1104
1077
  // identify this exact Integration branch; unrelated detachment is invalid.
1105
1078
  for (const backend of ["rebase-merge", "rebase-apply"]) {
1106
- const headNamePath = await gitLine(["-C", path, "rev-parse", "--path-format=absolute",
1107
- "--git-path", `${backend}/head-name`]);
1079
+ const headNamePath = await gitPath(path, ["--git-path", `${backend}/head-name`]);
1108
1080
  let headName;
1109
1081
  try {
1110
1082
  headName = (await readFile(headNamePath, "utf8")).trim();
@@ -1135,10 +1107,10 @@ async function assertOwnedWorktree(project, container, path) {
1135
1107
  assertContained(container, canonicalPath);
1136
1108
  if (canonicalPath !== path)
1137
1109
  throw new Error("Managed worktree resolves through a symbolic link.");
1138
- 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");
1139
1111
  if (root !== path)
1140
1112
  throw new Error("Managed worktree root does not match its deterministic path.");
1141
- 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");
1142
1114
  if (common !== project.gitDirectory) {
1143
1115
  cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
1144
1116
  }
@@ -1162,7 +1134,7 @@ async function inspectExactRecordedWorktree(input) {
1162
1134
  if (path !== expectedPath) {
1163
1135
  throw new Error("Recorded managed worktree resolves through a symbolic link.");
1164
1136
  }
1165
- 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");
1166
1138
  if (root !== path) {
1167
1139
  throw new Error("Recorded managed worktree root does not match its deterministic path.");
1168
1140
  }
@@ -1170,7 +1142,7 @@ async function inspectExactRecordedWorktree(input) {
1170
1142
  if (branch !== input.branch) {
1171
1143
  throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
1172
1144
  }
1173
- 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");
1174
1146
  const destination = await new NodeGitWorkspace().inspect(input.repositoryPath);
1175
1147
  const head = await resolveRefCommit(path, "HEAD");
1176
1148
  const retained = await resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
@@ -1267,8 +1239,8 @@ async function cleanupContainer(path) {
1267
1239
  }
1268
1240
  }
1269
1241
  async function canonicalDirectory(path, label) {
1270
- const value = requireText(path, label);
1271
- const canonical = await realpath(isAbsolute(value) ? value : resolve(value));
1242
+ requireText(path, label);
1243
+ const canonical = await realpath(isAbsolute(path) ? path : resolve(path));
1272
1244
  return canonical;
1273
1245
  }
1274
1246
  function managedPath(container, directory) {
@@ -1307,7 +1279,7 @@ async function readWorktreeStatus(path) {
1307
1279
  return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
1308
1280
  }
1309
1281
  async function readStatusGit(path, args) {
1310
- return git(["--no-optional-locks", "-c", "core.fsmonitor=false",
1282
+ return git(["--no-optional-locks", "-c", "core.fsmonitor=",
1311
1283
  "-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
1312
1284
  "-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
1313
1285
  }
@@ -1353,8 +1325,10 @@ async function assertStatusProgramsAbsent(path) {
1353
1325
  }
1354
1326
  }
1355
1327
  async function git(args, options = {}) {
1328
+ const program = supportedGit();
1329
+ const invocation = compatibleGitArguments(args);
1356
1330
  try {
1357
- const execution = executeFile("git", [...args], {
1331
+ const execution = executeFile(program.path, invocation, {
1358
1332
  encoding: "utf8",
1359
1333
  maxBuffer: 1024 * 1024,
1360
1334
  timeout: 30_000,
@@ -1375,6 +1349,7 @@ async function git(args, options = {}) {
1375
1349
  }
1376
1350
  }
1377
1351
  async function gitSucceeds(args) {
1352
+ supportedGit();
1378
1353
  try {
1379
1354
  await git(args);
1380
1355
  return true;
@@ -1384,7 +1359,7 @@ async function gitSucceeds(args) {
1384
1359
  }
1385
1360
  }
1386
1361
  async function gitLine(args) {
1387
- const lines = (await git(args)).trimEnd().split("\n");
1362
+ const lines = gitOutputLine(await git(args)).split("\n");
1388
1363
  if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
1389
1364
  throw new Error("Git returned invalid output.");
1390
1365
  }
@@ -1533,6 +1508,7 @@ function safeRef(value) {
1533
1508
  return ref;
1534
1509
  }
1535
1510
  async function safeFetchBranch(value) {
1511
+ supportedGit();
1536
1512
  const configuredRef = safeRef(value);
1537
1513
  if (configuredRef === "HEAD")
1538
1514
  return configuredRef;
@@ -1624,7 +1600,8 @@ function isCommit(value) {
1624
1600
  return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
1625
1601
  }
1626
1602
  function safeRemote(value) {
1627
- const remote = requireText(value, "Git remote URL");
1603
+ requireText(value, "Git remote URL");
1604
+ const remote = value;
1628
1605
  if (remote.startsWith("-") || /[\r\n]/.test(remote)) {
1629
1606
  throw new Error("Git remote URL is invalid.");
1630
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
+ }