@zq-silk/yui 2.22.0 → 2.25.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 (89) hide show
  1. package/README.md +9 -4
  2. package/dist/artifacts/artifactCapability.js +6 -6
  3. package/dist/artifacts/gitArtifactRef.js +10 -10
  4. package/dist/artifacts/jobArtifact.js +5 -5
  5. package/dist/artifacts/managedGit.js +15 -12
  6. package/dist/artifacts/taskArtifactRepository.js +35 -33
  7. package/dist/cli/commandCatalog.js +2 -2
  8. package/dist/cli/updateCommand.js +20 -3
  9. package/dist/cli/updateOrchestrator.js +13 -0
  10. package/dist/cli/updatePorts.js +27 -0
  11. package/dist/cli.js +9 -2
  12. package/dist/commands/configCommands.js +64 -1
  13. package/dist/commands/projectCommands.js +9 -9
  14. package/dist/commands/taskCompletionGate.js +1 -1
  15. package/dist/commands/taskIntegrationCommands.js +1 -1
  16. package/dist/commands/taskPublicationAdoptCommand.js +1 -1
  17. package/dist/commands/taskRoleRuntimeStatus.js +4 -0
  18. package/dist/commands/taskUpstreamCommands.js +1 -1
  19. package/dist/config/configCatalog.js +3 -0
  20. package/dist/config/gitConfig.js +20 -0
  21. package/dist/config/yuiConfig.js +1 -0
  22. package/dist/context/currentSkills.js +32 -1
  23. package/dist/context/sessionBootstrapManifest.js +17 -4
  24. package/dist/context/sessionSkillNotice.js +33 -12
  25. package/dist/controlPlaneCli.js +4 -4
  26. package/dist/controller/clientRuntime.js +37 -18
  27. package/dist/controller/controller.js +16 -1
  28. package/dist/controller/fileSchedulerStoreAdapter.js +2 -0
  29. package/dist/controller/hostLifecycle.js +153 -0
  30. package/dist/controller/hostLifecycleChecks.js +27 -0
  31. package/dist/controller/hostUpgrade.js +344 -0
  32. package/dist/controller/jobArtifactControl.js +3 -3
  33. package/dist/controller/jobDefinitionControl.js +1 -1
  34. package/dist/controller/runtime.js +67 -16
  35. package/dist/controller/sessionNotify.js +5 -46
  36. package/dist/execution/codexThreadNaming.js +54 -43
  37. package/dist/executor/fileRoleLaunchPlanner.js +18 -3
  38. package/dist/integration/gitIntegrationService.js +1 -1
  39. package/dist/kernel/builtinCapabilities.js +3 -3
  40. package/dist/repository/gitCommand.js +153 -0
  41. package/dist/repository/gitCompatibility.js +3 -2
  42. package/dist/repository/gitWorkspace.js +579 -563
  43. package/dist/repository/gitWorktreeInventory.js +15 -19
  44. package/dist/repository/projectRefresh.js +1 -1
  45. package/dist/repository/taskBaseFreshness.js +1 -4
  46. package/dist/repository/taskWorkspacePreparer.js +1 -1
  47. package/dist/runtime/agentEndpoint.js +11 -1
  48. package/dist/runtime/agentEndpointIdentity.js +2 -0
  49. package/dist/runtime/agentHost.js +167 -42
  50. package/dist/runtime/agentHostImplementation.js +3 -1
  51. package/dist/runtime/codexAppServerRuntime.js +3 -0
  52. package/dist/runtime/codexInteractiveHost.js +62 -17
  53. package/dist/runtime/codexNativeAccess.js +1 -1
  54. package/dist/runtime/contextRecovery.js +15 -0
  55. package/dist/runtime/hostSessionLifecycle.js +83 -0
  56. package/dist/runtime/hostUpgradeGate.js +43 -0
  57. package/dist/runtime/lifecycleChecks.js +47 -0
  58. package/dist/runtime/lifecycleReservation.js +9 -1
  59. package/dist/runtime/nativeExecutable.js +5 -2
  60. package/dist/runtime/nativeTitleReceipt.js +31 -0
  61. package/dist/runtime/nativeTitleSync.js +192 -0
  62. package/dist/runtime/platform.js +11 -0
  63. package/dist/runtime/providerRetry.js +4 -4
  64. package/dist/runtime/structuredProviderHost.js +42 -10
  65. package/dist/storage/sqliteSchema.js +30 -0
  66. package/dist/storage/storageVersions.js +1 -1
  67. package/dist/storage/taskStore.js +7 -1
  68. package/dist/web/webConversation.js +2 -2
  69. package/dist/web/webMaterials.js +19 -19
  70. package/dist/web/webTaskSurface.js +3 -3
  71. package/dist/workspace/workItemChangeSetManager.js +3 -3
  72. package/docs/managed-turn-and-session-runtime.md +96 -1
  73. package/docs/managed-turn-and-session-runtime.zh-CN.md +70 -1
  74. package/docs/project-refresh.md +3 -2
  75. package/docs/project-refresh.zh-CN.md +2 -1
  76. package/docs/release-workflow.md +86 -2
  77. package/docs/release-workflow.zh-CN.md +65 -1
  78. package/docs/roles-and-configuration.md +34 -0
  79. package/docs/roles-and-configuration.zh-CN.md +27 -0
  80. package/docs/storage-baseline.md +14 -1
  81. package/docs/storage-baseline.zh-CN.md +7 -1
  82. package/docs/testing/verification-levels.md +7 -7
  83. package/docs/testing/verification-levels.zh-CN.md +6 -6
  84. package/i18n/README.zh-CN.md +8 -4
  85. package/package.json +1 -1
  86. package/skills/yui-runtime/SKILL.md +7 -2
  87. package/skills/yui-runtime/VERSION +1 -1
  88. package/dist/runtime/native/darwin-x64/claude-process-owner +0 -0
  89. package/dist/runtime/native/darwin-x64/process-identity +0 -0
@@ -1,13 +1,12 @@
1
- import { execFile } from "node:child_process";
2
1
  import { createHash, randomBytes } from "node:crypto";
3
2
  import { lstat, mkdir, readFile, realpath, rm } from "node:fs/promises";
4
3
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
- import { promisify } from "node:util";
4
+ import { resolveGitLocalTimeoutSeconds, resolveGitRemoteInactivityTimeoutSeconds, resolveGitRemoteTimeoutSeconds } from "../config/gitConfig.js";
6
5
  import { cleanupFailure } from "../workspace/cleanupInspection.js";
7
6
  import { externalProgramConfigViolations } from "../artifacts/managedGit.js";
8
7
  import { compatibleGitArguments, gitOutputLine, supportedGit } from "./gitCompatibility.js";
9
8
  import { gitPath, readGitWorktrees } from "./gitWorktreeInventory.js";
10
- const executeFile = promisify(execFile);
9
+ import { executeGitCommand, GitCommandError } from "./gitCommand.js";
11
10
  const isolatedFetchOptions = [
12
11
  "--no-tags", "--no-prune", "--no-prune-tags", "--no-recurse-submodules",
13
12
  "--no-auto-gc", "--no-write-commit-graph", "--no-write-fetch-head", "--refmap="
@@ -32,8 +31,28 @@ export class RemoteBaselineConflictError extends Error {
32
31
  }
33
32
  /** The small Git boundary used by project registration and Task workspaces. */
34
33
  export class NodeGitWorkspace {
34
+ getConfig;
35
+ constructor(getConfig = () => ({})) {
36
+ this.getConfig = getConfig;
37
+ }
38
+ #localBudget() {
39
+ return { totalMs: resolveGitLocalTimeoutSeconds(this.getConfig().gitLocalTimeoutSeconds) * 1_000 };
40
+ }
41
+ #gitPath(cwd, args) {
42
+ return gitPath(cwd, args, this.#localBudget());
43
+ }
44
+ #readGitWorktrees(cwd) {
45
+ return readGitWorktrees(cwd, this.#localBudget());
46
+ }
47
+ #remoteBudget() {
48
+ const config = this.getConfig();
49
+ return {
50
+ inactivityMs: resolveGitRemoteInactivityTimeoutSeconds(config.gitRemoteInactivityTimeoutSeconds) * 1_000,
51
+ totalMs: resolveGitRemoteTimeoutSeconds(config.gitRemoteTimeoutSeconds) * 1_000
52
+ };
53
+ }
35
54
  async resolveTree(repositoryPath, commit) {
36
- return gitLine([
55
+ return this.#gitLine([
37
56
  "-C", repositoryPath,
38
57
  "rev-parse", "--verify", "--end-of-options", `${commit}^{tree}`
39
58
  ]);
@@ -46,7 +65,7 @@ export class NodeGitWorkspace {
46
65
  const sourceTree = await this.resolveTree(input.repositoryPath, source);
47
66
  const pageSize = 1000;
48
67
  for (let skip = 0;; skip += pageSize) {
49
- const output = await git([
68
+ const output = await this.#git([
50
69
  "-C", input.repositoryPath,
51
70
  "log", `--max-count=${pageSize}`, `--skip=${skip}`,
52
71
  "--format=%H%x09%T", history
@@ -78,7 +97,7 @@ export class NodeGitWorkspace {
78
97
  async treesAgreeOnPaths(input) {
79
98
  if (input.paths.length === 0)
80
99
  return false;
81
- return gitSucceeds([
100
+ return this.#gitSucceeds([
82
101
  "-C", input.repositoryPath,
83
102
  "--literal-pathspecs",
84
103
  "diff", "--quiet",
@@ -91,7 +110,7 @@ export class NodeGitWorkspace {
91
110
  * projections use this as objective Git evidence.
92
111
  */
93
112
  async changedFilesBetween(input) {
94
- const output = await git([
113
+ const output = await this.#git([
95
114
  "-C", input.repositoryPath,
96
115
  "diff", "--name-only",
97
116
  input.fromCommit, input.toCommit
@@ -100,7 +119,7 @@ export class NodeGitWorkspace {
100
119
  }
101
120
  /** Issue 07: exact unified diff text between two commits. */
102
121
  async diffTextBetween(input) {
103
- return git([
122
+ return this.#git([
104
123
  "-C", input.repositoryPath,
105
124
  "diff", "--no-color", "--no-ext-diff",
106
125
  ...(input.exact ? ["--no-textconv", "--binary", "--full-index", "--no-renames"] : []),
@@ -109,7 +128,7 @@ export class NodeGitWorkspace {
109
128
  }
110
129
  /** Issue 07: sums `git diff --numstat` added/deleted lines. */
111
130
  async diffNumstatBetween(input) {
112
- const output = await git([
131
+ const output = await this.#git([
113
132
  "-C", input.repositoryPath,
114
133
  "diff", "--numstat",
115
134
  input.fromCommit, input.toCommit
@@ -144,11 +163,11 @@ export class NodeGitWorkspace {
144
163
  */
145
164
  async resolveRemoteHead(input) {
146
165
  const remote = safeRemote(input.remoteUrl);
147
- const configuredBranch = await safeFetchBranch(input.branch);
166
+ const configuredBranch = await this.#safeFetchBranch(input.branch);
148
167
  const branch = configuredBranch === "HEAD"
149
- ? await resolveRemoteHeadBranch(remote)
168
+ ? await this.#resolveRemoteHeadBranch(remote, this.#remoteBudget())
150
169
  : configuredBranch;
151
- const commit = await resolveRemoteBranchCommit(remote, branch);
170
+ const commit = await this.#resolveRemoteBranchCommit(remote, branch, this.#remoteBudget());
152
171
  return { branch, commit };
153
172
  }
154
173
  /** Fetch a remote branch into a managed worktree without changing HEAD. */
@@ -158,46 +177,46 @@ export class NodeGitWorkspace {
158
177
  return { branch: fetched.branch, commit: fetched.commit };
159
178
  }
160
179
  finally {
161
- await gitSucceeds(["-C", input.repositoryPath, "update-ref", "-d", fetched.fetchedRef]);
180
+ await this.#gitSucceeds(["-C", input.repositoryPath, "update-ref", "-d", fetched.fetchedRef]);
162
181
  }
163
182
  }
164
183
  async #fetchRemoteHeadIntoWorktree(input) {
165
184
  const repositoryPath = (await this.inspect(input.repositoryPath, "HEAD")).root;
166
185
  const remote = safeRemote(input.remoteUrl);
167
- const configuredBranch = await safeFetchBranch(input.branch);
186
+ const configuredBranch = await this.#safeFetchBranch(input.branch);
168
187
  const branch = configuredBranch === "HEAD"
169
- ? await resolveRemoteHeadBranch(remote, repositoryPath)
188
+ ? await this.#resolveRemoteHeadBranch(remote, this.#remoteBudget(), repositoryPath)
170
189
  : configuredBranch;
171
190
  const fetchedRef = remoteBaselineRef(remote, branch);
172
191
  try {
173
- await git([
192
+ await this.#git([
174
193
  "-C", repositoryPath,
175
- "fetch", ...isolatedFetchOptions,
194
+ "fetch", "--progress", ...isolatedFetchOptions,
176
195
  remote,
177
196
  `refs/heads/${branch}:${fetchedRef}`
178
- ]);
179
- const commit = await resolveFetchedCommit(repositoryPath, fetchedRef);
197
+ ], { budget: this.#remoteBudget() });
198
+ const commit = await this.#resolveFetchedCommit(repositoryPath, fetchedRef);
180
199
  return { branch, commit, fetchedRef };
181
200
  }
182
201
  catch (error) {
183
- await gitSucceeds(["-C", repositoryPath, "update-ref", "-d", fetchedRef]);
202
+ await this.#gitSucceeds(["-C", repositoryPath, "update-ref", "-d", fetchedRef]).catch(() => false);
184
203
  throw error;
185
204
  }
186
205
  }
187
206
  async refresh(input) {
188
207
  const remote = safeRemote(input.remoteUrl);
189
- const configuredStableRef = await safeFetchBranch(input.stableRef);
208
+ const configuredStableRef = await this.#safeFetchBranch(input.stableRef);
190
209
  const initial = await this.inspect(input.repositoryPath, "HEAD");
191
210
  await this.#assertRefreshCheckout(initial.root, configuredStableRef, initial.baseCommit);
192
211
  const stableBranch = configuredStableRef === "HEAD"
193
- ? await resolveRemoteHeadBranch(remote, initial.root)
212
+ ? await this.#resolveRemoteHeadBranch(remote, this.#remoteBudget(), initial.root)
194
213
  : configuredStableRef;
195
214
  if (stableBranch !== configuredStableRef) {
196
215
  await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
197
216
  }
198
- const mapping = await resolveFetchTracking(initial.root, remote, stableBranch);
217
+ const mapping = await this.#resolveFetchTracking(initial.root, remote, stableBranch);
199
218
  const previous = mapping.status === "managed"
200
- ? await readDirectTrackingRef(initial.root, mapping.ref)
219
+ ? await this.#readDirectTrackingRef(initial.root, mapping.ref)
201
220
  : null;
202
221
  let tracking = mapping.status === "managed"
203
222
  ? { status: "current", remoteName: mapping.remoteName, ref: mapping.ref,
@@ -209,7 +228,7 @@ export class NodeGitWorkspace {
209
228
  let failure;
210
229
  let cleanupFailed = false;
211
230
  const assertMapping = async () => {
212
- const current = await resolveFetchTracking(initial.root, remote, stableBranch);
231
+ const current = await this.#resolveFetchTracking(initial.root, remote, stableBranch);
213
232
  if (JSON.stringify(current) !== JSON.stringify(mapping)) {
214
233
  throw new Error("Project fetch URL or tracking mapping changed during refresh.");
215
234
  }
@@ -217,12 +236,12 @@ export class NodeGitWorkspace {
217
236
  try {
218
237
  // An empty refmap disables opportunistic tracking updates even when the
219
238
  // Project URL is a named remote. Ignore configured prune/submodule effects.
220
- await git([
221
- "-C", initial.root, "fetch", ...isolatedFetchOptions,
239
+ await this.#git([
240
+ "-C", initial.root, "fetch", "--progress", ...isolatedFetchOptions,
222
241
  remote, `refs/heads/${stableBranch}:${temporaryRef}`
223
- ]);
224
- fetchedCommit = await resolveFetchedCommit(initial.root, temporaryRef);
225
- const advertisedCommit = await resolveRemoteBranchCommit(remote, stableBranch, initial.root);
242
+ ], { budget: this.#remoteBudget() });
243
+ fetchedCommit = await this.#resolveFetchedCommit(initial.root, temporaryRef);
244
+ const advertisedCommit = await this.#resolveRemoteBranchCommit(remote, stableBranch, this.#remoteBudget(), initial.root);
226
245
  if (fetchedCommit !== advertisedCommit) {
227
246
  throw new Error(`Project remote stable branch changed while it was fetched: ${stableBranch}.`);
228
247
  }
@@ -233,17 +252,17 @@ export class NodeGitWorkspace {
233
252
  if (!await this.isAncestor(initial.root, initial.baseCommit, fetchedCommit)) {
234
253
  throw new Error(`Project checkout cannot be fast-forwarded from ${initial.baseCommit} to ${fetchedCommit}.`);
235
254
  }
236
- await git(["-C", initial.root, "merge", "--ff-only", "--no-edit", fetchedCommit]);
255
+ await this.#git(["-C", initial.root, "merge", "--ff-only", "--no-edit", fetchedCommit]);
237
256
  }
238
257
  await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
239
258
  await assertMapping();
240
259
  if (mapping.status === "managed") {
241
260
  // Do not follow a symbolic tracking ref into somebody else's branch.
242
- await readDirectTrackingRef(initial.root, mapping.ref);
261
+ await this.#readDirectTrackingRef(initial.root, mapping.ref);
243
262
  const old = previous ?? "0".repeat(fetchedCommit.length);
244
263
  // Verify the stable branch and CAS the one configured destination in
245
264
  // the same Git transaction. HEAD/index/worktree still use normal ff.
246
- await git(["-C", initial.root, "update-ref", "--stdin"], { input: [
265
+ await this.#git(["-C", initial.root, "update-ref", "--stdin"], { input: [
247
266
  "option no-deref",
248
267
  `verify refs/heads/${stableBranch} ${fetchedCommit}`,
249
268
  "option no-deref",
@@ -259,7 +278,7 @@ export class NodeGitWorkspace {
259
278
  await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
260
279
  await assertMapping();
261
280
  if (mapping.status === "managed"
262
- && await readDirectTrackingRef(initial.root, mapping.ref) !== fetchedCommit) {
281
+ && await this.#readDirectTrackingRef(initial.root, mapping.ref) !== fetchedCommit) {
263
282
  throw new Error(`Project tracking ref changed after refresh: ${mapping.ref}.`);
264
283
  }
265
284
  }
@@ -268,9 +287,9 @@ export class NodeGitWorkspace {
268
287
  }
269
288
  finally {
270
289
  if (fetchedCommit !== undefined) {
271
- cleanupFailed = !await gitSucceeds([
290
+ cleanupFailed = !await this.#gitSucceeds([
272
291
  "-C", initial.root, "update-ref", "--no-deref", "-d", temporaryRef, fetchedCommit
273
- ]);
292
+ ]).catch(error => { failure ??= error; return false; });
274
293
  if (cleanupFailed && failure === undefined) {
275
294
  failure = new Error(`Project refresh temporary ref cleanup failed: ${temporaryRef}.`);
276
295
  }
@@ -283,7 +302,7 @@ export class NodeGitWorkspace {
283
302
  tracking = {
284
303
  status: "failed", remoteName: mapping.remoteName, ref: mapping.ref,
285
304
  fromCommit: previous,
286
- toCommit: await readDirectTrackingRef(initial.root, mapping.ref).catch(() => null),
305
+ toCommit: await this.#readDirectTrackingRef(initial.root, mapping.ref).catch(() => null),
287
306
  reason: detail
288
307
  };
289
308
  }
@@ -309,24 +328,24 @@ export class NodeGitWorkspace {
309
328
  */
310
329
  async resolveRemoteBaseline(input) {
311
330
  const remote = safeRemote(input.remoteUrl);
312
- const configuredRef = await safeFetchBranch(input.developmentRef);
331
+ const configuredRef = await this.#safeFetchBranch(input.developmentRef);
313
332
  const repository = await this.inspect(input.repositoryPath, "HEAD");
314
333
  const branch = configuredRef === "HEAD"
315
- ? await resolveRemoteHeadBranch(remote, repository.root)
334
+ ? await this.#resolveRemoteHeadBranch(remote, this.#remoteBudget(), repository.root)
316
335
  : configuredRef;
317
336
  const temporaryRef = remoteBaselineRef(remote, branch);
318
337
  try {
319
- await git([
338
+ await this.#git([
320
339
  "-C", repository.root,
321
- "fetch", ...isolatedFetchOptions,
340
+ "fetch", "--progress", ...isolatedFetchOptions,
322
341
  remote,
323
342
  `refs/heads/${branch}:${temporaryRef}`
324
- ]);
325
- const fetchedCommit = await gitLine([
343
+ ], { budget: this.#remoteBudget() });
344
+ const fetchedCommit = await this.#gitLine([
326
345
  "-C", repository.root,
327
346
  "rev-parse", "--verify", "--end-of-options", `${temporaryRef}^{commit}`
328
347
  ]);
329
- const advertisedCommit = await resolveRemoteBranchCommit(remote, branch, repository.root);
348
+ const advertisedCommit = await this.#resolveRemoteBranchCommit(remote, branch, this.#remoteBudget(), repository.root);
330
349
  if (fetchedCommit.toLowerCase() !== advertisedCommit) {
331
350
  throw new Error(`Project remote development branch changed while it was fetched: ${branch}.`);
332
351
  }
@@ -339,14 +358,14 @@ export class NodeGitWorkspace {
339
358
  // A temporary namespace keeps the fetch independent from the stable
340
359
  // branch and FETCH_HEAD. Cleanup is best effort so the original fetch
341
360
  // or consistency error remains the actionable diagnosis.
342
- await gitSucceeds([
361
+ await this.#gitSucceeds([
343
362
  "-C", repository.root,
344
363
  "update-ref", "-d", temporaryRef
345
- ]);
364
+ ]).catch(() => false);
346
365
  }
347
366
  }
348
367
  async clone(input) {
349
- supportedGit();
368
+ supportedGit(this.#localBudget().totalMs);
350
369
  const remote = safeRemote(input.remoteUrl);
351
370
  requireText(input.destination, "Project destination");
352
371
  const destination = resolve(input.destination);
@@ -361,23 +380,27 @@ export class NodeGitWorkspace {
361
380
  await mkdir(destination, { mode: 0o700 });
362
381
  const createdDirectory = await lstat(destination);
363
382
  try {
364
- await git([
365
- "clone",
383
+ await this.#git([
384
+ "clone", "--progress",
366
385
  ...(branch === undefined ? [] : ["--branch", branch]),
367
386
  "--",
368
387
  remote,
369
388
  destination
370
- ]);
389
+ ], { budget: this.#remoteBudget() });
371
390
  if (input.localBranch !== undefined) {
372
391
  const localBranch = safeRef(input.localBranch);
373
- if (!await gitSucceeds(["check-ref-format", "--branch", localBranch])) {
392
+ if (!await this.#gitSucceeds(["check-ref-format", "--branch", localBranch])) {
374
393
  throw new Error(`Local clone branch is invalid: ${localBranch}.`);
375
394
  }
376
- await git(["-C", destination, "branch", "-M", "--", localBranch]);
395
+ await this.#git(["-C", destination, "branch", "-M", "--", localBranch]);
377
396
  }
378
397
  return await this.inspect(destination, "HEAD");
379
398
  }
380
399
  catch (error) {
400
+ if (error instanceof AggregateError && error.cause instanceof GitCommandError) {
401
+ error.message += ` Temporary clone retained at ${destination}; transport termination is unconfirmed.`;
402
+ throw error;
403
+ }
381
404
  try {
382
405
  const current = await lstat(destination).catch(failure => {
383
406
  if (isErrno(failure, "ENOENT"))
@@ -408,23 +431,23 @@ export class NodeGitWorkspace {
408
431
  catch (originalError) {
409
432
  const root = (await this.inspect(repositoryPath, "HEAD")).root;
410
433
  const name = safeRef(branch);
411
- if (!await gitSucceeds(["check-ref-format", "--branch", name]))
434
+ if (!await this.#gitSucceeds(["check-ref-format", "--branch", name]))
412
435
  throw originalError;
413
436
  const remoteRef = `refs/remotes/origin/${name}`;
414
- if (!await gitSucceeds([
437
+ if (!await this.#gitSucceeds([
415
438
  "-C", root, "show-ref", "--verify", "--quiet", remoteRef
416
439
  ]))
417
440
  throw originalError;
418
- await git(["-C", root, "branch", "--", name, remoteRef]);
441
+ await this.#git(["-C", root, "branch", "--", name, remoteRef]);
419
442
  return this.inspect(root, name);
420
443
  }
421
444
  }
422
445
  async inspect(repositoryPath, baseRef = "HEAD") {
423
446
  const requested = await canonicalDirectory(repositoryPath, "Project");
424
447
  const ref = safeRef(baseRef);
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");
427
- const baseCommit = await gitLine([
448
+ const root = await canonicalDirectory(await this.#gitPath(requested, ["--show-toplevel"]), "Project root");
449
+ const gitDirectory = await canonicalDirectory(await this.#gitPath(root, ["--git-common-dir"]), "Git common directory");
450
+ const baseCommit = await this.#gitLine([
428
451
  "-C", root, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`
429
452
  ]);
430
453
  if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/i.test(baseCommit)) {
@@ -443,7 +466,7 @@ export class NodeGitWorkspace {
443
466
  // ref still points at <old>. Using the same commit for both new and old
444
467
  // makes it a pure compare-and-swap probe — the ref does not move, but a
445
468
  // concurrent advance between the last read and this call is detected.
446
- await git([
469
+ await this.#git([
447
470
  "-C", root, "update-ref",
448
471
  safeRef(ref),
449
472
  expectedCommit,
@@ -452,14 +475,14 @@ export class NodeGitWorkspace {
452
475
  }
453
476
  async refExists(repositoryPath, ref) {
454
477
  const root = (await this.inspect(repositoryPath)).root;
455
- return gitSucceeds([
478
+ return this.#gitSucceeds([
456
479
  "-C", root, "rev-parse", "--verify", "--quiet", "--end-of-options",
457
480
  `${safeRef(ref)}^{commit}`
458
481
  ]);
459
482
  }
460
483
  async listRefs(repositoryPath, pattern) {
461
484
  const root = (await this.inspect(repositoryPath)).root;
462
- const output = await git([
485
+ const output = await this.#git([
463
486
  "-C", root, "for-each-ref", "--format=%(refname)", "--", safeRef(pattern)
464
487
  ]);
465
488
  return output.length === 0 ? [] : output.split("\n").map((line) => line.trim())
@@ -474,9 +497,9 @@ export class NodeGitWorkspace {
474
497
  const snapshots = [];
475
498
  for (const ref of refs) {
476
499
  const name = safeRef(ref);
477
- const commit = await resolveRefCommit(source.root, name);
500
+ const commit = await this.#resolveRefCommit(source.root, name);
478
501
  if (await this.refExists(destination.root, name)) {
479
- const existing = await resolveRefCommit(destination.root, name);
502
+ const existing = await this.#resolveRefCommit(destination.root, name);
480
503
  if (existing !== commit) {
481
504
  throw new Error(`Destination ref already exists at a different commit: ${name}.`);
482
505
  }
@@ -488,12 +511,12 @@ export class NodeGitWorkspace {
488
511
  try {
489
512
  for (const [index, snapshot] of snapshots.entries()) {
490
513
  const importedRef = `${importRoot}/${index}`;
491
- await git([
514
+ await this.#git([
492
515
  "-C", destination.root,
493
516
  "fetch", ...isolatedFetchOptions, "--",
494
517
  source.root, `${snapshot.ref}:${importedRef}`
495
518
  ]);
496
- const imported = await resolveRefCommit(destination.root, importedRef);
519
+ const imported = await this.#resolveRefCommit(destination.root, importedRef);
497
520
  if (imported !== snapshot.commit) {
498
521
  throw new Error(`Imported ref did not preserve its exact commit: ${snapshot.ref}.`);
499
522
  }
@@ -502,18 +525,18 @@ export class NodeGitWorkspace {
502
525
  // Freeze the source snapshots through publication. A moving local ref is
503
526
  // never silently copied under an earlier/later identity.
504
527
  for (const snapshot of snapshots) {
505
- if (await resolveRefCommit(source.root, snapshot.ref) !== snapshot.commit) {
528
+ if (await this.#resolveRefCommit(source.root, snapshot.ref) !== snapshot.commit) {
506
529
  throw new Error(`Source ref changed while it was being copied: ${snapshot.ref}.`);
507
530
  }
508
531
  }
509
532
  for (const snapshot of snapshots) {
510
533
  if (await this.refExists(destination.root, snapshot.ref)) {
511
- if (await resolveRefCommit(destination.root, snapshot.ref) !== snapshot.commit) {
534
+ if (await this.#resolveRefCommit(destination.root, snapshot.ref) !== snapshot.commit) {
512
535
  throw new Error(`Destination ref changed while it was being copied: ${snapshot.ref}.`);
513
536
  }
514
537
  continue;
515
538
  }
516
- await git([
539
+ await this.#git([
517
540
  "-C", destination.root,
518
541
  "update-ref", "--no-deref", snapshot.ref, snapshot.commit,
519
542
  "0".repeat(snapshot.commit.length)
@@ -524,7 +547,7 @@ export class NodeGitWorkspace {
524
547
  finally {
525
548
  for (const entry of temporary) {
526
549
  if (await this.refExists(destination.root, entry.ref)) {
527
- await git([
550
+ await this.#git([
528
551
  "-C", destination.root,
529
552
  "update-ref", "-d", "--no-deref", entry.ref, entry.commit
530
553
  ]);
@@ -536,13 +559,13 @@ export class NodeGitWorkspace {
536
559
  const root = (await this.inspect(input.repositoryPath)).root;
537
560
  const source = safeRef(input.sourceRef);
538
561
  const target = safeRef(input.archiveRef);
539
- const commit = (await gitLine([
562
+ const commit = (await this.#gitLine([
540
563
  "-C", root, "rev-parse", "--verify", "--end-of-options", `${source}^{commit}`
541
564
  ])).toLowerCase();
542
565
  if (await this.refExists(root, target)) {
543
566
  // Resumable: a previous attempt already created the archive ref. Only
544
567
  // the same commit may be resumed; a different archive fails closed.
545
- const archived = (await gitLine([
568
+ const archived = (await this.#gitLine([
546
569
  "-C", root, "rev-parse", "--verify", "--end-of-options", `${target}^{commit}`
547
570
  ])).toLowerCase();
548
571
  if (archived !== commit) {
@@ -551,14 +574,14 @@ export class NodeGitWorkspace {
551
574
  }
552
575
  else {
553
576
  // Create the archive ref only if it does not exist yet (old value zero).
554
- await git([
577
+ await this.#git([
555
578
  "-C", root, "update-ref", "--no-deref", target, commit, "0".repeat(commit.length)
556
579
  ]);
557
580
  }
558
581
  // Delete the source only if it still exists and still points at the
559
582
  // archived commit; an already-deleted source makes the archive a no-op.
560
583
  if (await this.refExists(root, source)) {
561
- await git([
584
+ await this.#git([
562
585
  "-C", root, "update-ref", "-d", "--no-deref", source, commit
563
586
  ]);
564
587
  }
@@ -572,7 +595,7 @@ export class NodeGitWorkspace {
572
595
  const excluded = input.excludeWorktreePath === undefined
573
596
  ? undefined
574
597
  : await realpath(resolve(input.excludeWorktreePath)).catch(() => undefined);
575
- const records = await readGitWorktrees(root);
598
+ const records = await this.#readGitWorktrees(root);
576
599
  for (const record of records) {
577
600
  // A dead registration (its directory is gone) occupies nothing; a
578
601
  // worktree on another ref or a detached HEAD does not occupy this one.
@@ -590,7 +613,7 @@ export class NodeGitWorkspace {
590
613
  }
591
614
  async isAncestor(repositoryPath, ancestor, descendant) {
592
615
  const root = (await this.inspect(repositoryPath)).root;
593
- return gitSucceeds([
616
+ return this.#gitSucceeds([
594
617
  "-C", root,
595
618
  "merge-base", "--is-ancestor",
596
619
  safeRef(ancestor),
@@ -599,8 +622,8 @@ export class NodeGitWorkspace {
599
622
  }
600
623
  async inspectRemoteTracking(input) {
601
624
  const root = (await this.inspect(input.repositoryPath)).root;
602
- const branch = await safeFetchBranch(input.branch);
603
- const mapping = await resolveFetchTracking(root, safeRemote(input.remoteUrl), branch);
625
+ const branch = await this.#safeFetchBranch(input.branch);
626
+ const mapping = await this.#resolveFetchTracking(root, safeRemote(input.remoteUrl), branch);
604
627
  if (mapping.status !== "managed")
605
628
  return null;
606
629
  if (!await this.refExists(root, mapping.ref))
@@ -614,7 +637,7 @@ export class NodeGitWorkspace {
614
637
  }
615
638
  async mergeBase(input) {
616
639
  const root = (await this.inspect(input.repositoryPath)).root;
617
- return gitLine([
640
+ return this.#gitLine([
618
641
  "-C", root,
619
642
  "merge-base",
620
643
  safeRef(input.leftCommit),
@@ -623,17 +646,17 @@ export class NodeGitWorkspace {
623
646
  }
624
647
  async headRef(repositoryPath) {
625
648
  const requested = await canonicalDirectory(repositoryPath, "Project");
626
- return gitLine(["-C", requested, "rev-parse", "--abbrev-ref", "HEAD"]);
649
+ return this.#gitLine(["-C", requested, "rev-parse", "--abbrev-ref", "HEAD"]);
627
650
  }
628
651
  async isClean(repositoryPath) {
629
652
  const root = (await this.inspect(repositoryPath)).root;
630
- const status = await git(["--no-optional-locks", "-C", root,
653
+ const status = await this.#git(["--no-optional-locks", "-C", root,
631
654
  "status", "--porcelain=v1", "--untracked-files=all"]);
632
655
  return status.length === 0;
633
656
  }
634
657
  async inspectClean(repositoryPath) {
635
658
  const root = (await this.inspect(repositoryPath)).root;
636
- const status = await readWorktreeStatus(root);
659
+ const status = await this.#readWorktreeStatus(root);
637
660
  return status.length === 0;
638
661
  }
639
662
  async mergeWorktree(input) {
@@ -649,11 +672,11 @@ export class NodeGitWorkspace {
649
672
  // Merge all selected Lane heads in one Git transaction. A conflict in
650
673
  // any Lane therefore aborts the complete Candidate materialization
651
674
  // instead of leaving an earlier Lane merged into the WorkItem target.
652
- await git(["-C", target, "merge", "--no-edit", "--no-ff", ...sources]);
675
+ await this.#git(["-C", target, "merge", "--no-edit", "--no-ff", ...sources]);
653
676
  }
654
677
  catch (error) {
655
678
  try {
656
- await git(["-C", target, "merge", "--abort"]);
679
+ await this.#git(["-C", target, "merge", "--abort"]);
657
680
  }
658
681
  catch (abortError) {
659
682
  throw new Error(`Git merge failed and conflict cleanup could not be completed for ${target}.`, { cause: abortError });
@@ -673,7 +696,7 @@ export class NodeGitWorkspace {
673
696
  throw new Error(`Git reset target changed before compensation: ${target} (${currentHead}).`);
674
697
  }
675
698
  try {
676
- await git(["-C", target, "reset", "--hard", restoreHead]);
699
+ await this.#git(["-C", target, "reset", "--hard", restoreHead]);
677
700
  const restoredHead = (await this.inspect(target, "HEAD")).baseCommit;
678
701
  if (restoredHead !== restoreHead || !await this.isClean(target)) {
679
702
  throw new Error(`Git reset target did not restore its exact head: ${target}.`);
@@ -695,8 +718,8 @@ export class NodeGitWorkspace {
695
718
  throw new Error(`Project checkout changed before reset: ${root} (${currentHead}).`);
696
719
  }
697
720
  // Verify the target commit resolves before moving the branch.
698
- await gitLine(["-C", root, "rev-parse", "--verify", "--end-of-options", `${targetCommit}^{commit}`]);
699
- await git(["-C", root, "reset", "--hard", targetCommit]);
721
+ await this.#gitLine(["-C", root, "rev-parse", "--verify", "--end-of-options", `${targetCommit}^{commit}`]);
722
+ await this.#git(["-C", root, "reset", "--hard", targetCommit]);
700
723
  const restoredHead = (await this.inspect(root, "HEAD")).baseCommit;
701
724
  if (restoredHead.toLowerCase() !== targetCommit.toLowerCase() || !await this.isClean(root)) {
702
725
  throw new Error(`Project checkout did not reset to the exact target commit: ${root}.`);
@@ -704,12 +727,12 @@ export class NodeGitWorkspace {
704
727
  }
705
728
  async listWorktrees(repositoryPath) {
706
729
  const root = await canonicalDirectory(repositoryPath, "Project");
707
- return (await readGitWorktrees(root)).map(entry => entry.path);
730
+ return (await this.#readGitWorktrees(root)).map(entry => entry.path);
708
731
  }
709
732
  async listCommitsBetween(input) {
710
733
  const root = await canonicalDirectory(input.repositoryPath, "Project");
711
734
  const range = `${requireText(input.baseCommit, "Base commit")}..${requireText(input.headCommit, "Head commit")}`;
712
- const output = await git(["-C", root, "log", "--oneline", range]);
735
+ const output = await this.#git(["-C", root, "log", "--oneline", range]);
713
736
  return output
714
737
  .split(/\r?\n/u)
715
738
  .map((line) => line.trim())
@@ -767,9 +790,9 @@ export class NodeGitWorkspace {
767
790
  // A worktree already created before a crash remains usable even if the
768
791
  // original base branch/tag is later deleted.
769
792
  const project = await this.inspect(input.repositoryPath);
770
- await assertOwnedWorktree(project, container, path);
771
- await assertExpectedBranch(path, identity.branch, input.allowRebase);
772
- const head = await gitLine([
793
+ await this.#assertOwnedWorktree(project, container, path);
794
+ await this.#assertExpectedBranch(path, identity.branch, input.allowRebase);
795
+ const head = await this.#gitLine([
773
796
  "-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
774
797
  ]);
775
798
  return { path, branch: identity.branch, baseCommit: head.toLowerCase() };
@@ -777,18 +800,18 @@ export class NodeGitWorkspace {
777
800
  const project = await this.inspect(input.repositoryPath, input.baseRef);
778
801
  await canonicalContainer(dirname(path), true);
779
802
  const branchRef = `refs/heads/${identity.branch}`;
780
- const branchExists = await gitSucceeds([
803
+ const branchExists = await this.#gitSucceeds([
781
804
  "-C", project.root, "show-ref", "--verify", "--quiet", branchRef
782
805
  ]);
783
- await git(branchExists
806
+ await this.#git(branchExists
784
807
  ? ["-C", project.root, "worktree", "add", "--", path, identity.branch]
785
808
  : [
786
809
  "-C", project.root, "worktree", "add", "-b", identity.branch,
787
810
  "--", path, project.baseCommit
788
811
  ]);
789
- await assertOwnedWorktree(project, container, path);
790
- await assertExpectedBranch(path, identity.branch);
791
- const head = await gitLine([
812
+ await this.#assertOwnedWorktree(project, container, path);
813
+ await this.#assertExpectedBranch(path, identity.branch);
814
+ const head = await this.#gitLine([
792
815
  "-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
793
816
  ]);
794
817
  return { path, branch: identity.branch, baseCommit: head.toLowerCase() };
@@ -803,14 +826,14 @@ export class NodeGitWorkspace {
803
826
  const project = await this.inspect(input.repositoryPath);
804
827
  if (state === "missing") {
805
828
  if (input.deleteBranch === true) {
806
- await removeMissingWorktreeRegistration(project.root, path, branch);
807
- await deleteBranchIfPresent(project.root, branch);
829
+ await this.#removeMissingWorktreeRegistration(project.root, path, branch);
830
+ await this.#deleteBranchIfPresent(project.root, branch);
808
831
  }
809
832
  return state;
810
833
  }
811
- await git(["-C", project.root, "worktree", "remove", "--", path]);
834
+ await this.#git(["-C", project.root, "worktree", "remove", "--", path]);
812
835
  if (input.deleteBranch === true) {
813
- await deleteBranchIfPresent(project.root, branch);
836
+ await this.#deleteBranchIfPresent(project.root, branch);
814
837
  }
815
838
  return "removed";
816
839
  }
@@ -838,9 +861,9 @@ export class NodeGitWorkspace {
838
861
  throw new Error("Expected a linked worktree, not a standalone clone.");
839
862
  }
840
863
  const project = await this.inspect(path, "HEAD");
841
- await assertOwnedWorktree(project, resolve(identity.container), path);
864
+ await this.#assertOwnedWorktree(project, resolve(identity.container), path);
842
865
  const branch = worktreeIdentity(identity.taskSegment, identity.roleName).branch;
843
- await assertExpectedBranch(path, branch);
866
+ await this.#assertExpectedBranch(path, branch);
844
867
  if (project.baseCommit !== identity.expectedCommit) {
845
868
  throw new Error("Unadopted worktree HEAD changed since preparation; retained.");
846
869
  }
@@ -849,10 +872,10 @@ export class NodeGitWorkspace {
849
872
  // Use the worktree's proven common directory, never the changed catalog.
850
873
  // Git still enforces locks/submodule restrictions. A Git error is not
851
874
  // authority for direct filesystem deletion, even for unadopted work.
852
- await git(["--git-dir", project.gitDirectory, "worktree", "remove", "--", path]);
875
+ await this.#git(["--git-dir", project.gitDirectory, "worktree", "remove", "--", path]);
853
876
  worktreeRemoved = true;
854
877
  if (identity.deleteBranch === true) {
855
- await git(["--git-dir", project.gitDirectory, "update-ref", "-d", `refs/heads/${branch}`, identity.expectedCommit]);
878
+ await this.#git(["--git-dir", project.gitDirectory, "update-ref", "-d", `refs/heads/${branch}`, identity.expectedCommit]);
856
879
  }
857
880
  return "removed";
858
881
  }
@@ -867,7 +890,7 @@ export class NodeGitWorkspace {
867
890
  if (state !== "clean")
868
891
  return state;
869
892
  if (input.expectedCommit !== undefined
870
- && await resolveRefCommit(input.path, "HEAD") !== input.expectedCommit) {
893
+ && await this.#resolveRefCommit(input.path, "HEAD") !== input.expectedCommit) {
871
894
  throw new Error(`Unadopted Task clone HEAD changed since preparation; retained at ${input.path}.`);
872
895
  }
873
896
  await rm(managedPath(resolve(input.container), input.directory), { recursive: true });
@@ -892,23 +915,23 @@ export class NodeGitWorkspace {
892
915
  if (repository.root !== expected || repository.gitDirectory !== join(expected, ".git")) {
893
916
  cleanupFailure("workspace-identity-mismatch", "Task clone Git ownership cannot be established.", "exact root and Git directory", "different Git ownership");
894
917
  }
895
- await assertExpectedBranch(expected, input.branch);
918
+ await this.#assertExpectedBranch(expected, input.branch);
896
919
  if (!await this.inspectClean(expected))
897
920
  return "dirty";
898
- const worktrees = await readGitWorktrees(expected);
921
+ const worktrees = await this.#readGitWorktrees(expected);
899
922
  if (worktrees.length !== 1 || worktrees[0].path !== expected) {
900
923
  cleanupFailure("git-worktree-registrations", "Task clone still owns Git worktree registrations; retained.", 1, worktrees.length);
901
924
  }
902
925
  return "clean";
903
926
  }
904
927
  async inspectRecordedWorktree(input) {
905
- const inspected = await inspectExactRecordedWorktree(input);
928
+ const inspected = await this.#inspectExactRecordedWorktree(input);
906
929
  return inspected?.state ?? "missing";
907
930
  }
908
931
  async removeRecordedWorktree(input) {
909
932
  let inspected;
910
933
  try {
911
- inspected = await inspectExactRecordedWorktree(input);
934
+ inspected = await this.#inspectExactRecordedWorktree(input);
912
935
  }
913
936
  catch (error) {
914
937
  // A deleted external checkout takes the worktree's common dir with it,
@@ -925,18 +948,18 @@ export class NodeGitWorkspace {
925
948
  return "dirty";
926
949
  // Revalidate immediately before asking the worktree's own Git common-dir
927
950
  // to remove it. This never relies on the Project catalog's former path.
928
- const current = await inspectExactRecordedWorktree(input);
951
+ const current = await this.#inspectExactRecordedWorktree(input);
929
952
  if (current === undefined)
930
953
  return "missing";
931
954
  if (current.state === "dirty")
932
955
  return "dirty";
933
- await retainCommitRef(current.destinationRoot, input.retainedRef, current.head);
934
- await git(["-C", current.path, "worktree", "remove", "--", current.path]);
956
+ await this.#retainCommitRef(current.destinationRoot, input.retainedRef, current.head);
957
+ await this.#git(["-C", current.path, "worktree", "remove", "--", current.path]);
935
958
  if (await pathKind(current.path) !== undefined) {
936
959
  throw new Error(`Recorded managed worktree remained after removal: ${current.path}.`);
937
960
  }
938
961
  if (current.gitDirectory !== current.destinationGitDirectory) {
939
- await git([
962
+ await this.#git([
940
963
  `--git-dir=${current.gitDirectory}`,
941
964
  "update-ref", "-d", "--no-deref", `refs/heads/${safeRef(input.branch)}`,
942
965
  current.head
@@ -951,16 +974,16 @@ export class NodeGitWorkspace {
951
974
  const branch = input.expectedBranch ?? worktreeIdentity(input.taskSegment, input.roleName).branch;
952
975
  if (kind === undefined) {
953
976
  const project = await this.inspect(input.repositoryPath);
954
- await inspectMissingWorktreeRegistration(project.root, path, branch, input.expectedBranch !== undefined);
977
+ await this.#inspectMissingWorktreeRegistration(project.root, path, branch, input.expectedBranch !== undefined);
955
978
  return "missing";
956
979
  }
957
980
  if (kind === "symlink")
958
981
  cleanupFailure("workspace-identity-mismatch", "Managed worktree path must not be a symbolic link.", "real directory", "symbolic link");
959
982
  const canonicalContainerPath = await canonicalContainer(container, false);
960
983
  const project = await this.inspect(input.repositoryPath);
961
- await assertOwnedWorktree(project, canonicalContainerPath, path);
962
- await assertExpectedBranch(path, branch, input.expectedBranch !== undefined);
963
- const porcelain = await readWorktreeStatus(path);
984
+ await this.#assertOwnedWorktree(project, canonicalContainerPath, path);
985
+ await this.#assertExpectedBranch(path, branch, input.expectedBranch !== undefined);
986
+ const porcelain = await this.#readWorktreeStatus(path);
964
987
  return porcelain.length > 0 ? "dirty" : "clean";
965
988
  }
966
989
  async removeIntegrationWorktree(input) {
@@ -982,68 +1005,468 @@ export class NodeGitWorkspace {
982
1005
  throw new Error("Managed worktree path must not be a symbolic link.");
983
1006
  const repository = await this.inspect(input.repositoryPath);
984
1007
  if (kind === undefined) {
985
- await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch, true);
1008
+ await this.#removeMissingWorktreeRegistration(repository.root, path, input.identity.branch, true);
986
1009
  }
987
1010
  if (kind === "directory") {
988
1011
  const canonicalContainerPath = await canonicalContainer(container, false);
989
- await assertOwnedWorktree(repository, canonicalContainerPath, path);
990
- await assertExpectedBranch(path, input.identity.branch, true);
1012
+ await this.#assertOwnedWorktree(repository, canonicalContainerPath, path);
1013
+ await this.#assertExpectedBranch(path, input.identity.branch, true);
991
1014
  if (input.discardChanges === true) {
992
- await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
1015
+ await this.#git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
993
1016
  }
994
1017
  else {
995
- const porcelain = await readWorktreeStatus(path);
1018
+ const porcelain = await this.#readWorktreeStatus(path);
996
1019
  if (porcelain.length > 0)
997
1020
  return "dirty";
998
1021
  if (input.expectedBaseCommit !== undefined
999
1022
  && input.allowCommittedChanges !== true) {
1000
- const head = await gitLine([
1023
+ const head = await this.#gitLine([
1001
1024
  "-C", path, "rev-parse", "--verify", "--end-of-options", "HEAD^{commit}"
1002
1025
  ]);
1003
1026
  if (head.toLowerCase() !== input.expectedBaseCommit.toLowerCase())
1004
1027
  return "dirty";
1005
1028
  }
1006
- await git(["-C", repository.root, "worktree", "remove", "--", path]);
1029
+ await this.#git(["-C", repository.root, "worktree", "remove", "--", path]);
1007
1030
  }
1008
1031
  }
1009
1032
  const branchRef = `refs/heads/${input.identity.branch}`;
1010
- if (await gitSucceeds([
1033
+ if (await this.#gitSucceeds([
1011
1034
  "-C", repository.root, "show-ref", "--verify", "--quiet", branchRef
1012
1035
  ])) {
1013
- await git(["-C", repository.root, "branch", "-D", "--", input.identity.branch]);
1036
+ await this.#git(["-C", repository.root, "branch", "-D", "--", input.identity.branch]);
1014
1037
  }
1015
1038
  return kind === "directory" ? "removed" : "missing";
1016
1039
  }
1017
- }
1018
- /** Missing paths do not imply missing Git metadata. Never prune another
1019
- * workspace's registration as a side effect of cleaning this exact owner.
1020
- */
1021
- async function removeMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1022
- if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached))
1023
- return;
1024
- if (await pathKind(path) !== undefined)
1025
- throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
1026
- await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
1027
- }
1028
- async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1029
- const entry = (await readGitWorktrees(repositoryRoot)).find(record => record.path === path);
1030
- if (entry === undefined)
1031
- return false;
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 });
1037
- }
1038
- return true;
1039
- }
1040
- async function deleteBranchIfPresent(repositoryRoot, branch) {
1041
- const branchRef = `refs/heads/${branch}`;
1042
- if (!await gitSucceeds([
1043
- "-C", repositoryRoot, "show-ref", "--verify", "--quiet", branchRef
1044
- ]))
1045
- return;
1046
- await git(["-C", repositoryRoot, "branch", "-D", "--", branch]);
1040
+ /** Missing paths do not imply missing Git metadata. Never prune another
1041
+ * workspace's registration as a side effect of cleaning this exact owner.
1042
+ */
1043
+ async #removeMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1044
+ if (!await this.#inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached))
1045
+ return;
1046
+ if (await pathKind(path) !== undefined)
1047
+ throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
1048
+ await this.#git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
1049
+ }
1050
+ async #inspectMissingWorktreeRegistration(repositoryRoot, path, branch, allowDetached = false) {
1051
+ const entry = (await this.#readGitWorktrees(repositoryRoot)).find(record => record.path === path);
1052
+ if (entry === undefined)
1053
+ return false;
1054
+ const pinnedDetached = allowDetached && entry.detached
1055
+ && await this.#gitSucceeds(["-C", repositoryRoot, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`])
1056
+ && entry.head === await this.#resolveRefCommit(repositoryRoot, `refs/heads/${branch}`);
1057
+ if ((entry.branch !== `refs/heads/${branch}` && !pinnedDetached) || entry.locked) {
1058
+ cleanupFailure("git-registration-mismatch", "Missing worktree has uncertain or locked Git ownership; metadata retained.", { branch, locked: false }, { branch: entry.branch ?? null, locked: entry.locked });
1059
+ }
1060
+ return true;
1061
+ }
1062
+ async #deleteBranchIfPresent(repositoryRoot, branch) {
1063
+ const branchRef = `refs/heads/${branch}`;
1064
+ if (!await this.#gitSucceeds([
1065
+ "-C", repositoryRoot, "show-ref", "--verify", "--quiet", branchRef
1066
+ ]))
1067
+ return;
1068
+ await this.#git(["-C", repositoryRoot, "branch", "-D", "--", branch]);
1069
+ }
1070
+ async #assertExpectedBranch(path, expected, allowRebase = false) {
1071
+ if (allowRebase && !await this.#gitSucceeds(["-C", path, "symbolic-ref", "--quiet", "HEAD"])) {
1072
+ // Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
1073
+ // identify this exact Integration branch; unrelated detachment is invalid.
1074
+ for (const backend of ["rebase-merge", "rebase-apply"]) {
1075
+ const headNamePath = await this.#gitPath(path, ["--git-path", `${backend}/head-name`]);
1076
+ let headName;
1077
+ try {
1078
+ headName = (await readFile(headNamePath, "utf8")).trim();
1079
+ }
1080
+ catch (error) {
1081
+ if (error.code === "ENOENT")
1082
+ continue;
1083
+ throw error;
1084
+ }
1085
+ if (headName === `refs/heads/${expected}`)
1086
+ return;
1087
+ throw new Error(`Managed worktree is rebasing an unexpected branch: ${headName}.`);
1088
+ }
1089
+ // A terminal Integration can legitimately retain a detached checkout.
1090
+ // Its exact owned path/common-dir were checked by the caller; require its
1091
+ // retained branch to pin the same commit before allowing clean removal.
1092
+ if (await this.#resolveRefCommit(path, "HEAD") === await this.#resolveRefCommit(path, `refs/heads/${expected}`))
1093
+ return;
1094
+ cleanupFailure("workspace-identity-mismatch", "Detached worktree is not pinned by its owned branch or rebase.", expected, "unproven detached HEAD");
1095
+ }
1096
+ const branch = await this.#gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
1097
+ if (branch !== expected) {
1098
+ cleanupFailure("workspace-identity-mismatch", "Managed worktree is on an unexpected branch.", expected, branch);
1099
+ }
1100
+ }
1101
+ async #assertOwnedWorktree(project, container, path) {
1102
+ const canonicalPath = await canonicalDirectory(path, "Managed worktree");
1103
+ assertContained(container, canonicalPath);
1104
+ if (canonicalPath !== path)
1105
+ throw new Error("Managed worktree resolves through a symbolic link.");
1106
+ const root = await canonicalDirectory(await this.#gitPath(path, ["--show-toplevel"]), "Managed worktree root");
1107
+ if (root !== path)
1108
+ throw new Error("Managed worktree root does not match its deterministic path.");
1109
+ const common = await canonicalDirectory(await this.#gitPath(path, ["--git-common-dir"]), "Managed Git common directory");
1110
+ if (common !== project.gitDirectory) {
1111
+ cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
1112
+ }
1113
+ }
1114
+ async #inspectExactRecordedWorktree(input) {
1115
+ const container = await canonicalContainer(input.container, false);
1116
+ const expectedBranch = worktreeIdentity(input.taskSegment, input.roleName).branch;
1117
+ const expectedPath = managedPath(container, input.directory);
1118
+ if (resolve(input.path) !== expectedPath || input.branch !== expectedBranch) {
1119
+ throw new Error("Recorded managed worktree identity is invalid.");
1120
+ }
1121
+ const kind = await pathKind(expectedPath);
1122
+ if (kind === undefined)
1123
+ return undefined;
1124
+ if (kind === "symlink") {
1125
+ throw new Error("Recorded managed worktree path must not be a symbolic link.");
1126
+ }
1127
+ const canonicalContainerPath = await canonicalContainer(container, false);
1128
+ const path = await canonicalDirectory(expectedPath, "Recorded managed worktree");
1129
+ assertContained(canonicalContainerPath, path);
1130
+ if (path !== expectedPath) {
1131
+ throw new Error("Recorded managed worktree resolves through a symbolic link.");
1132
+ }
1133
+ const root = await canonicalDirectory(await this.#gitPath(path, ["--show-toplevel"]), "Recorded managed worktree root");
1134
+ if (root !== path) {
1135
+ throw new Error("Recorded managed worktree root does not match its deterministic path.");
1136
+ }
1137
+ const branch = await this.#gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
1138
+ if (branch !== input.branch) {
1139
+ throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
1140
+ }
1141
+ const gitDirectory = await canonicalDirectory(await this.#gitPath(path, ["--git-common-dir"]), "Recorded managed Git common directory");
1142
+ const destination = await this.inspect(input.repositoryPath);
1143
+ const head = await this.#resolveRefCommit(path, "HEAD");
1144
+ const retained = await this.#resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
1145
+ if (retained !== head) {
1146
+ throw new Error(`Recorded managed worktree is not retained by the current Project: ${input.branch}.`);
1147
+ }
1148
+ const status = await this.#git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
1149
+ return {
1150
+ path,
1151
+ gitDirectory,
1152
+ destinationGitDirectory: destination.gitDirectory,
1153
+ destinationRoot: destination.root,
1154
+ head,
1155
+ state: status.length === 0 ? "clean" : "dirty"
1156
+ };
1157
+ }
1158
+ async #resolveRefCommit(repositoryPath, ref) {
1159
+ const commit = (await this.#gitLine([
1160
+ "-C", repositoryPath,
1161
+ "rev-parse", "--verify", "--end-of-options", `${safeRef(ref)}^{commit}`
1162
+ ])).toLowerCase();
1163
+ if (!isCommit(commit))
1164
+ throw new Error("Git returned an invalid ref commit.");
1165
+ return commit;
1166
+ }
1167
+ async #retainCommitRef(repositoryPath, ref, commit) {
1168
+ const target = safeRef(ref);
1169
+ if (await this.#gitSucceeds([
1170
+ "-C", repositoryPath,
1171
+ "rev-parse", "--verify", "--quiet", "--end-of-options", `${target}^{commit}`
1172
+ ])) {
1173
+ if (await this.#resolveRefCommit(repositoryPath, target) !== commit) {
1174
+ throw new Error(`Retained ref already exists at a different commit: ${target}.`);
1175
+ }
1176
+ return;
1177
+ }
1178
+ await this.#git([
1179
+ "-C", repositoryPath,
1180
+ "update-ref", "--no-deref", target, commit, "0".repeat(commit.length)
1181
+ ]);
1182
+ }
1183
+ /** Status can execute clean/process filters to compare equal-size changed
1184
+ * files. Preserve native normalization: report unknown instead of disabling
1185
+ * a filter and misclassifying its output as clean/dirty. Include scopes so a
1186
+ * native global/worktree include cannot hide the same execution requirement.
1187
+ */
1188
+ async #readWorktreeStatus(path) {
1189
+ await this.#assertStatusProgramsAbsent(path);
1190
+ return this.#readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
1191
+ }
1192
+ async #readStatusGit(path, args) {
1193
+ return this.#git(["--no-optional-locks", "-c", "core.fsmonitor=",
1194
+ "-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
1195
+ "-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
1196
+ }
1197
+ async #assertStatusProgramsAbsent(path) {
1198
+ const configuration = await this.#readStatusGit(path, ["config", "--list", "--null", "--includes"]);
1199
+ const filters = externalProgramConfigViolations(configuration)
1200
+ .filter(key => key.startsWith("filter.") && (key.endsWith(".clean") || key.endsWith(".process")));
1201
+ const files = (await this.#readStatusGit(path, ["ls-files", "--stage", "-z"])).split("\0");
1202
+ if (filters.length > 0) {
1203
+ // Merely having native LFS configured globally is not a violation. Only
1204
+ // filters selected by tracked regular files' effective attributes can run
1205
+ // during status. check-attr reads those selections without executing them.
1206
+ const configured = new Set(filters.map(key => key.slice(7, key.lastIndexOf("."))));
1207
+ const tracked = [...new Set(files.filter(line => line.startsWith("100"))
1208
+ .map(line => line.slice(line.indexOf("\t") + 1)))];
1209
+ for (let offset = 0; offset < tracked.length; offset += 128) {
1210
+ const attributes = (await this.#readStatusGit(path, ["check-attr", "-z", "filter", "--",
1211
+ ...tracked.slice(offset, offset + 128)])).split("\0");
1212
+ for (let index = 2; index < attributes.length; index += 3) {
1213
+ if (!configured.has(attributes[index].toLowerCase()))
1214
+ continue;
1215
+ cleanupFailure("git-status-requires-filter", "Git status may execute a selected filter program; read-only inspection will not run it.", "status without external filter execution", { trackedPath: attributes[index - 2], filter: attributes[index] }, "unknown");
1216
+ }
1217
+ }
1218
+ }
1219
+ // Status also examines initialized submodules. Inspect their configuration
1220
+ // before asking Git to recurse; uninitialized gitlinks execute nothing.
1221
+ for (const record of files.filter(line => line.startsWith("160000 "))) {
1222
+ const child = join(path, record.slice(record.indexOf("\t") + 1));
1223
+ assertContained(path, child);
1224
+ const gitEntry = await lstat(join(child, ".git")).catch(error => {
1225
+ if (isErrno(error, "ENOENT"))
1226
+ return undefined;
1227
+ throw error;
1228
+ });
1229
+ if (gitEntry === undefined)
1230
+ continue;
1231
+ if (await realpath(child) !== child
1232
+ || (await this.#readStatusGit(child, ["rev-parse", "--show-toplevel"])).trimEnd() !== child) {
1233
+ cleanupFailure("workspace-identity-mismatch", "Submodule identity cannot be safely inspected.", "owned submodule directory", "different or symbolic root");
1234
+ }
1235
+ await this.#assertStatusProgramsAbsent(child);
1236
+ }
1237
+ }
1238
+ async #git(args, options = {}) {
1239
+ const localBudget = this.#localBudget();
1240
+ const program = supportedGit(localBudget.totalMs);
1241
+ const invocation = compatibleGitArguments(args);
1242
+ const result = await executeGitCommand(program.path, invocation, {
1243
+ ...options,
1244
+ budget: options.budget ?? localBudget,
1245
+ stderrTail: options.budget?.inactivityMs !== undefined,
1246
+ ...(options.environment === undefined ? {} : { environment: { ...process.env, ...options.environment } })
1247
+ });
1248
+ return result.stdout.toString("utf8");
1249
+ }
1250
+ async #gitSucceeds(args) {
1251
+ supportedGit(this.#localBudget().totalMs);
1252
+ try {
1253
+ await this.#git(args);
1254
+ return true;
1255
+ }
1256
+ catch (error) {
1257
+ if (error instanceof GitCommandError && error.timeoutKind === undefined
1258
+ && error.signal === null && typeof error.code === "number")
1259
+ return false;
1260
+ throw error;
1261
+ }
1262
+ }
1263
+ async #gitLine(args) {
1264
+ const lines = gitOutputLine(await this.#git(args)).split("\n");
1265
+ if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
1266
+ throw new Error("Git returned invalid output.");
1267
+ }
1268
+ return lines[0];
1269
+ }
1270
+ async #parseFetchSpec(value) {
1271
+ const negative = value.startsWith("^");
1272
+ const text = value.replace(/^[+^]/u, "");
1273
+ const parts = text.split(":");
1274
+ const source = parts[0];
1275
+ const destination = parts[1] || undefined;
1276
+ if (parts.length > 2 || (negative && parts.length !== 1)
1277
+ || !source.startsWith("refs/")
1278
+ || !await this.#gitSucceeds(["check-ref-format", "--refspec-pattern", source])
1279
+ || (destination !== undefined && (!destination.startsWith("refs/")
1280
+ || !await this.#gitSucceeds(["check-ref-format", "--refspec-pattern", destination])
1281
+ || source.includes("*") !== destination.includes("*"))))
1282
+ return null;
1283
+ return { source, ...(destination === undefined ? {} : { destination }), negative };
1284
+ }
1285
+ async #gitConfigValues(root, key) {
1286
+ try {
1287
+ return (await this.#git(["-C", root, "config", "--null", "--get-all", key]))
1288
+ .split("\0").filter((value) => value.length > 0);
1289
+ }
1290
+ catch (error) {
1291
+ const cause = error instanceof Error ? error.cause : undefined;
1292
+ if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === 1) {
1293
+ return [];
1294
+ }
1295
+ throw error;
1296
+ }
1297
+ }
1298
+ /**
1299
+ * Prove a unique configured fetch destination, never infer it from a remote
1300
+ * name. Git resolves insteadOf aliases; push URLs are deliberately irrelevant.
1301
+ * Unsafe/ambiguous configurations remain usable for HEAD-only refresh.
1302
+ */
1303
+ async #resolveFetchTracking(root, remote, branch) {
1304
+ const wantedUrl = await this.#gitLine(["-C", root, "ls-remote", "--get-url", "--", remote]);
1305
+ const names = (await this.#git(["-C", root, "remote"])).trim().split("\n").filter(Boolean).sort();
1306
+ const remotes = await Promise.all(names.map(async (name) => ({
1307
+ name,
1308
+ urls: (await this.#git(["-C", root, "remote", "get-url", "--all", "--", name])).trimEnd().split("\n"),
1309
+ specs: await this.#gitConfigValues(root, `remote.${name}.fetch`)
1310
+ })));
1311
+ const evidence = JSON.stringify({ wantedUrl, remotes });
1312
+ const unmanaged = (reason) => ({ status: "unmanaged", reason, evidence });
1313
+ const matching = remotes.filter(({ urls }) => urls[0] === wantedUrl);
1314
+ if (matching.length !== 1 || matching[0].urls.length !== 1) {
1315
+ return unmanaged("Project fetch URL does not identify exactly one single-URL Git remote.");
1316
+ }
1317
+ const selected = matching[0];
1318
+ const candidates = await Promise.all(remotes.map(async (entry) => ({
1319
+ ...entry, parsed: await Promise.all(entry.specs.map(value => this.#parseFetchSpec(value)))
1320
+ })));
1321
+ if (candidates.some(({ parsed: specs }) => specs.some((spec) => spec === null))) {
1322
+ return unmanaged("A configured fetch refspec is outside the supported full-ref exact/one-star mapping.");
1323
+ }
1324
+ const parsed = candidates.map((entry) => ({
1325
+ ...entry, parsed: entry.parsed.filter((spec) => spec !== null)
1326
+ }));
1327
+ const source = `refs/heads/${branch}`;
1328
+ const excluded = (specs, ref) => specs.some((spec) => spec.negative && refspecCapture(spec.source, ref) !== null);
1329
+ const selectedSpecs = parsed.find(({ name }) => name === selected.name).parsed;
1330
+ if (excluded(selectedSpecs, source))
1331
+ return unmanaged(`Fetch refspec excludes ${source}.`);
1332
+ const destinations = new Set();
1333
+ for (const spec of selectedSpecs) {
1334
+ if (spec.negative || spec.destination === undefined)
1335
+ continue;
1336
+ const capture = refspecCapture(spec.source, source);
1337
+ if (capture !== null)
1338
+ destinations.add(spec.destination.replace("*", capture));
1339
+ }
1340
+ if (destinations.size !== 1) {
1341
+ return unmanaged(`Fetch refspec does not map ${source} to exactly one destination.`);
1342
+ }
1343
+ const ref = [...destinations][0];
1344
+ if (!ref.startsWith("refs/remotes/") || !await this.#gitSucceeds(["check-ref-format", ref])) {
1345
+ return unmanaged(`Fetch destination is not a supported remote-tracking ref: ${ref}.`);
1346
+ }
1347
+ for (const entry of parsed) {
1348
+ for (const spec of entry.parsed) {
1349
+ if (spec.negative || spec.destination === undefined)
1350
+ continue;
1351
+ const capture = refspecCapture(spec.destination, ref);
1352
+ if (capture === null)
1353
+ continue;
1354
+ const otherSource = spec.source.replace("*", capture);
1355
+ if (!excluded(entry.parsed, otherSource)
1356
+ && (entry.name !== selected.name || otherSource !== source)) {
1357
+ return unmanaged(`Another fetch source also manages ${ref}.`);
1358
+ }
1359
+ }
1360
+ }
1361
+ try {
1362
+ await this.#readDirectTrackingRef(root, ref);
1363
+ }
1364
+ catch (error) {
1365
+ return unmanaged(error instanceof Error ? error.message : String(error));
1366
+ }
1367
+ return { status: "managed", remoteName: selected.name, remoteUrl: wantedUrl, ref, evidence };
1368
+ }
1369
+ async #readDirectTrackingRef(root, ref) {
1370
+ const output = await this.#git([
1371
+ "-C", root, "for-each-ref", "--format=%(refname)%09%(objectname)%09%(symref)", "--", ref
1372
+ ]);
1373
+ const record = output.split("\n").find((line) => line.split("\t")[0] === ref);
1374
+ if (record === undefined) {
1375
+ // for-each-ref omits dangling symbolic refs. Never treat those as absent.
1376
+ if (await this.#gitSucceeds(["-C", root, "symbolic-ref", "--quiet", ref])) {
1377
+ throw new Error(`Tracking destination is symbolic: ${ref}.`);
1378
+ }
1379
+ return null;
1380
+ }
1381
+ const [, commit, symbolic] = record.split("\t");
1382
+ if (symbolic !== "" || commit === undefined || !isCommit(commit)) {
1383
+ throw new Error(`Tracking destination is not a direct object ref: ${ref}.`);
1384
+ }
1385
+ return commit.toLowerCase();
1386
+ }
1387
+ async #safeFetchBranch(value) {
1388
+ supportedGit(this.#localBudget().totalMs);
1389
+ const configuredRef = safeRef(value);
1390
+ if (configuredRef === "HEAD")
1391
+ return configuredRef;
1392
+ const branchPrefix = "refs/heads/";
1393
+ const branch = configuredRef.startsWith(branchPrefix)
1394
+ ? configuredRef.slice(branchPrefix.length)
1395
+ : configuredRef;
1396
+ if (!await this.#gitSucceeds(["check-ref-format", "--branch", branch])) {
1397
+ throw new Error("Git stable branch is invalid.");
1398
+ }
1399
+ return branch;
1400
+ }
1401
+ async #resolveRemoteHeadBranch(remote, budget, repositoryPath) {
1402
+ let output;
1403
+ try {
1404
+ output = await this.#git([
1405
+ ...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
1406
+ "ls-remote", "--symref", remote, "HEAD"
1407
+ ], { budget });
1408
+ }
1409
+ catch (error) {
1410
+ const detail = error instanceof Error ? error.message : "Git command failed.";
1411
+ throw new Error(`Project remote HEAD could not be resolved: ${detail}`, { cause: error });
1412
+ }
1413
+ const symbolicLines = output.split("\n").filter((line) => line.startsWith("ref: "));
1414
+ const prefix = "ref: refs/heads/";
1415
+ const suffix = "\tHEAD";
1416
+ const symbolic = symbolicLines.length === 1 ? symbolicLines[0] : "";
1417
+ if (!symbolic.startsWith(prefix) || !symbolic.endsWith(suffix)) {
1418
+ throw invalidRemoteHead();
1419
+ }
1420
+ const branch = symbolic.slice(prefix.length, -suffix.length);
1421
+ if (branch === "HEAD")
1422
+ throw invalidRemoteHead();
1423
+ try {
1424
+ return await this.#safeFetchBranch(branch);
1425
+ }
1426
+ catch (error) {
1427
+ if (error instanceof GitCommandError)
1428
+ throw error;
1429
+ throw invalidRemoteHead();
1430
+ }
1431
+ }
1432
+ async #resolveRemoteBranchCommit(remote, branch, budget, repositoryPath) {
1433
+ const target = `refs/heads/${branch}`;
1434
+ let output;
1435
+ try {
1436
+ output = await this.#git([
1437
+ ...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
1438
+ "ls-remote", "--refs", remote, target
1439
+ ], { budget });
1440
+ }
1441
+ catch (error) {
1442
+ const detail = error instanceof Error ? error.message : "Git command failed.";
1443
+ throw new Error(`Project remote development branch could not be resolved: ${branch}: ${detail}`, { cause: error });
1444
+ }
1445
+ const matches = output
1446
+ .split("\n")
1447
+ .map((line) => line.trim())
1448
+ .filter((line) => line.length > 0)
1449
+ .map((line) => line.split(/\s+/u))
1450
+ .filter(([commit, ref]) => ref === target && commit !== undefined);
1451
+ if (matches.length !== 1) {
1452
+ throw new Error(`Project remote development branch could not be resolved: ${branch}.`);
1453
+ }
1454
+ const commit = matches[0][0];
1455
+ if (!isCommit(commit)) {
1456
+ throw new Error(`Project remote development branch returned an invalid commit: ${branch}.`);
1457
+ }
1458
+ return commit.toLowerCase();
1459
+ }
1460
+ async #resolveFetchedCommit(repositoryPath, fetchedRef) {
1461
+ const commit = (await this.#gitLine([
1462
+ "-C", repositoryPath,
1463
+ "rev-parse", "--verify", "--end-of-options", `${fetchedRef}^{commit}`
1464
+ ])).toLowerCase();
1465
+ if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
1466
+ throw new Error("Git returned an invalid fetched remote commit.");
1467
+ }
1468
+ return commit;
1469
+ }
1047
1470
  }
1048
1471
  export function worktreeIdentity(taskId, roleName) {
1049
1472
  const taskKey = safeIdentity(taskId, "Task id");
@@ -1071,103 +1494,6 @@ function gitRefSegment(identity) {
1071
1494
  const digest = createHash("sha256").update(identity).digest("hex").slice(0, 24);
1072
1495
  return `encoded-${digest}`;
1073
1496
  }
1074
- async function assertExpectedBranch(path, expected, allowRebase = false) {
1075
- if (allowRebase && !await gitSucceeds(["-C", path, "symbolic-ref", "--quiet", "HEAD"])) {
1076
- // Rebase temporarily detaches HEAD. Git's per-worktree metadata must still
1077
- // identify this exact Integration branch; unrelated detachment is invalid.
1078
- for (const backend of ["rebase-merge", "rebase-apply"]) {
1079
- const headNamePath = await gitPath(path, ["--git-path", `${backend}/head-name`]);
1080
- let headName;
1081
- try {
1082
- headName = (await readFile(headNamePath, "utf8")).trim();
1083
- }
1084
- catch (error) {
1085
- if (error.code === "ENOENT")
1086
- continue;
1087
- throw error;
1088
- }
1089
- if (headName === `refs/heads/${expected}`)
1090
- return;
1091
- throw new Error(`Managed worktree is rebasing an unexpected branch: ${headName}.`);
1092
- }
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");
1099
- }
1100
- const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
1101
- if (branch !== expected) {
1102
- cleanupFailure("workspace-identity-mismatch", "Managed worktree is on an unexpected branch.", expected, branch);
1103
- }
1104
- }
1105
- async function assertOwnedWorktree(project, container, path) {
1106
- const canonicalPath = await canonicalDirectory(path, "Managed worktree");
1107
- assertContained(container, canonicalPath);
1108
- if (canonicalPath !== path)
1109
- throw new Error("Managed worktree resolves through a symbolic link.");
1110
- const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Managed worktree root");
1111
- if (root !== path)
1112
- throw new Error("Managed worktree root does not match its deterministic path.");
1113
- const common = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Managed Git common directory");
1114
- if (common !== project.gitDirectory) {
1115
- cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
1116
- }
1117
- }
1118
- async function inspectExactRecordedWorktree(input) {
1119
- const container = await canonicalContainer(input.container, false);
1120
- const expectedBranch = worktreeIdentity(input.taskSegment, input.roleName).branch;
1121
- const expectedPath = managedPath(container, input.directory);
1122
- if (resolve(input.path) !== expectedPath || input.branch !== expectedBranch) {
1123
- throw new Error("Recorded managed worktree identity is invalid.");
1124
- }
1125
- const kind = await pathKind(expectedPath);
1126
- if (kind === undefined)
1127
- return undefined;
1128
- if (kind === "symlink") {
1129
- throw new Error("Recorded managed worktree path must not be a symbolic link.");
1130
- }
1131
- const canonicalContainerPath = await canonicalContainer(container, false);
1132
- const path = await canonicalDirectory(expectedPath, "Recorded managed worktree");
1133
- assertContained(canonicalContainerPath, path);
1134
- if (path !== expectedPath) {
1135
- throw new Error("Recorded managed worktree resolves through a symbolic link.");
1136
- }
1137
- const root = await canonicalDirectory(await gitPath(path, ["--show-toplevel"]), "Recorded managed worktree root");
1138
- if (root !== path) {
1139
- throw new Error("Recorded managed worktree root does not match its deterministic path.");
1140
- }
1141
- const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
1142
- if (branch !== input.branch) {
1143
- throw new Error(`Recorded managed worktree is on an unexpected branch: ${branch}.`);
1144
- }
1145
- const gitDirectory = await canonicalDirectory(await gitPath(path, ["--git-common-dir"]), "Recorded managed Git common directory");
1146
- const destination = await new NodeGitWorkspace().inspect(input.repositoryPath);
1147
- const head = await resolveRefCommit(path, "HEAD");
1148
- const retained = await resolveRefCommit(destination.root, `refs/heads/${input.branch}`);
1149
- if (retained !== head) {
1150
- throw new Error(`Recorded managed worktree is not retained by the current Project: ${input.branch}.`);
1151
- }
1152
- const status = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
1153
- return {
1154
- path,
1155
- gitDirectory,
1156
- destinationGitDirectory: destination.gitDirectory,
1157
- destinationRoot: destination.root,
1158
- head,
1159
- state: status.length === 0 ? "clean" : "dirty"
1160
- };
1161
- }
1162
- async function resolveRefCommit(repositoryPath, ref) {
1163
- const commit = (await gitLine([
1164
- "-C", repositoryPath,
1165
- "rev-parse", "--verify", "--end-of-options", `${safeRef(ref)}^{commit}`
1166
- ])).toLowerCase();
1167
- if (!isCommit(commit))
1168
- throw new Error("Git returned an invalid ref commit.");
1169
- return commit;
1170
- }
1171
1497
  /**
1172
1498
  * Classify a recorded worktree whose Project repository can no longer be
1173
1499
  * inspected. A deleted external checkout takes the worktree's common dir with
@@ -1191,22 +1517,6 @@ async function recordedWorktreeWithoutRepository(input, cause) {
1191
1517
  + `The Project repository is gone, so the worktree's Git state cannot be verified; `
1192
1518
  + `remove the directory manually once its contents are safe.`);
1193
1519
  }
1194
- async function retainCommitRef(repositoryPath, ref, commit) {
1195
- const target = safeRef(ref);
1196
- if (await gitSucceeds([
1197
- "-C", repositoryPath,
1198
- "rev-parse", "--verify", "--quiet", "--end-of-options", `${target}^{commit}`
1199
- ])) {
1200
- if (await resolveRefCommit(repositoryPath, target) !== commit) {
1201
- throw new Error(`Retained ref already exists at a different commit: ${target}.`);
1202
- }
1203
- return;
1204
- }
1205
- await git([
1206
- "-C", repositoryPath,
1207
- "update-ref", "--no-deref", target, commit, "0".repeat(commit.length)
1208
- ]);
1209
- }
1210
1520
  async function canonicalContainer(path, create) {
1211
1521
  const lexical = resolve(path);
1212
1522
  if (create)
@@ -1269,102 +1579,6 @@ async function pathKind(path) {
1269
1579
  throw error;
1270
1580
  }
1271
1581
  }
1272
- /** Status can execute clean/process filters to compare equal-size changed
1273
- * files. Preserve native normalization: report unknown instead of disabling
1274
- * a filter and misclassifying its output as clean/dirty. Include scopes so a
1275
- * native global/worktree include cannot hide the same execution requirement.
1276
- */
1277
- async function readWorktreeStatus(path) {
1278
- await assertStatusProgramsAbsent(path);
1279
- return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
1280
- }
1281
- async function readStatusGit(path, args) {
1282
- return git(["--no-optional-locks", "-c", "core.fsmonitor=",
1283
- "-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
1284
- "-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
1285
- }
1286
- async function assertStatusProgramsAbsent(path) {
1287
- const configuration = await readStatusGit(path, ["config", "--list", "--null", "--includes"]);
1288
- const filters = externalProgramConfigViolations(configuration)
1289
- .filter(key => key.startsWith("filter.") && (key.endsWith(".clean") || key.endsWith(".process")));
1290
- const files = (await readStatusGit(path, ["ls-files", "--stage", "-z"])).split("\0");
1291
- if (filters.length > 0) {
1292
- // Merely having native LFS configured globally is not a violation. Only
1293
- // filters selected by tracked regular files' effective attributes can run
1294
- // during status. check-attr reads those selections without executing them.
1295
- const configured = new Set(filters.map(key => key.slice(7, key.lastIndexOf("."))));
1296
- const tracked = [...new Set(files.filter(line => line.startsWith("100"))
1297
- .map(line => line.slice(line.indexOf("\t") + 1)))];
1298
- for (let offset = 0; offset < tracked.length; offset += 128) {
1299
- const attributes = (await readStatusGit(path, ["check-attr", "-z", "filter", "--",
1300
- ...tracked.slice(offset, offset + 128)])).split("\0");
1301
- for (let index = 2; index < attributes.length; index += 3) {
1302
- if (!configured.has(attributes[index].toLowerCase()))
1303
- continue;
1304
- cleanupFailure("git-status-requires-filter", "Git status may execute a selected filter program; read-only inspection will not run it.", "status without external filter execution", { trackedPath: attributes[index - 2], filter: attributes[index] }, "unknown");
1305
- }
1306
- }
1307
- }
1308
- // Status also examines initialized submodules. Inspect their configuration
1309
- // before asking Git to recurse; uninitialized gitlinks execute nothing.
1310
- for (const record of files.filter(line => line.startsWith("160000 "))) {
1311
- const child = join(path, record.slice(record.indexOf("\t") + 1));
1312
- assertContained(path, child);
1313
- const gitEntry = await lstat(join(child, ".git")).catch(error => {
1314
- if (isErrno(error, "ENOENT"))
1315
- return undefined;
1316
- throw error;
1317
- });
1318
- if (gitEntry === undefined)
1319
- continue;
1320
- if (await realpath(child) !== child
1321
- || (await readStatusGit(child, ["rev-parse", "--show-toplevel"])).trimEnd() !== child) {
1322
- cleanupFailure("workspace-identity-mismatch", "Submodule identity cannot be safely inspected.", "owned submodule directory", "different or symbolic root");
1323
- }
1324
- await assertStatusProgramsAbsent(child);
1325
- }
1326
- }
1327
- async function git(args, options = {}) {
1328
- const program = supportedGit();
1329
- const invocation = compatibleGitArguments(args);
1330
- try {
1331
- const execution = executeFile(program.path, invocation, {
1332
- encoding: "utf8",
1333
- maxBuffer: 1024 * 1024,
1334
- timeout: 30_000,
1335
- ...(options.environment === undefined ? {} : { env: { ...process.env, ...options.environment } })
1336
- });
1337
- if (options.input !== undefined)
1338
- execution.child.stdin.end(options.input);
1339
- const result = await execution;
1340
- return result.stdout;
1341
- }
1342
- catch (error) {
1343
- const stderr = typeof error === "object" && error !== null && "stderr" in error
1344
- ? String(error.stderr).trim()
1345
- : "";
1346
- throw new Error(stderr.length === 0 ? "Git command failed." : `Git command failed: ${stderr}`, {
1347
- cause: error
1348
- });
1349
- }
1350
- }
1351
- async function gitSucceeds(args) {
1352
- supportedGit();
1353
- try {
1354
- await git(args);
1355
- return true;
1356
- }
1357
- catch {
1358
- return false;
1359
- }
1360
- }
1361
- async function gitLine(args) {
1362
- const lines = gitOutputLine(await git(args)).split("\n");
1363
- if (lines.length !== 1 || lines[0]?.length === 0 || lines[0]?.includes("\0")) {
1364
- throw new Error("Git returned invalid output.");
1365
- }
1366
- return lines[0];
1367
- }
1368
1582
  /** Match one exact or one-star Git refspec, in either direction. */
1369
1583
  function refspecCapture(pattern, ref) {
1370
1584
  const star = pattern.indexOf("*");
@@ -1376,123 +1590,6 @@ function refspecCapture(pattern, ref) {
1376
1590
  && ref.length >= prefix.length + suffix.length
1377
1591
  ? ref.slice(prefix.length, ref.length - suffix.length) : null;
1378
1592
  }
1379
- async function parseFetchSpec(value) {
1380
- const negative = value.startsWith("^");
1381
- const text = value.replace(/^[+^]/u, "");
1382
- const parts = text.split(":");
1383
- const source = parts[0];
1384
- const destination = parts[1] || undefined;
1385
- if (parts.length > 2 || (negative && parts.length !== 1)
1386
- || !source.startsWith("refs/")
1387
- || !await gitSucceeds(["check-ref-format", "--refspec-pattern", source])
1388
- || (destination !== undefined && (!destination.startsWith("refs/")
1389
- || !await gitSucceeds(["check-ref-format", "--refspec-pattern", destination])
1390
- || source.includes("*") !== destination.includes("*"))))
1391
- return null;
1392
- return { source, ...(destination === undefined ? {} : { destination }), negative };
1393
- }
1394
- async function gitConfigValues(root, key) {
1395
- try {
1396
- return (await git(["-C", root, "config", "--null", "--get-all", key]))
1397
- .split("\0").filter((value) => value.length > 0);
1398
- }
1399
- catch (error) {
1400
- const cause = error instanceof Error ? error.cause : undefined;
1401
- if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === 1) {
1402
- return [];
1403
- }
1404
- throw error;
1405
- }
1406
- }
1407
- /**
1408
- * Prove a unique configured fetch destination, never infer it from a remote
1409
- * name. Git resolves insteadOf aliases; push URLs are deliberately irrelevant.
1410
- * Unsafe/ambiguous configurations remain usable for HEAD-only refresh.
1411
- */
1412
- async function resolveFetchTracking(root, remote, branch) {
1413
- const wantedUrl = await gitLine(["-C", root, "ls-remote", "--get-url", "--", remote]);
1414
- const names = (await git(["-C", root, "remote"])).trim().split("\n").filter(Boolean).sort();
1415
- const remotes = await Promise.all(names.map(async (name) => ({
1416
- name,
1417
- urls: (await git(["-C", root, "remote", "get-url", "--all", "--", name])).trimEnd().split("\n"),
1418
- specs: await gitConfigValues(root, `remote.${name}.fetch`)
1419
- })));
1420
- const evidence = JSON.stringify({ wantedUrl, remotes });
1421
- const unmanaged = (reason) => ({ status: "unmanaged", reason, evidence });
1422
- const matching = remotes.filter(({ urls }) => urls[0] === wantedUrl);
1423
- if (matching.length !== 1 || matching[0].urls.length !== 1) {
1424
- return unmanaged("Project fetch URL does not identify exactly one single-URL Git remote.");
1425
- }
1426
- const selected = matching[0];
1427
- const candidates = await Promise.all(remotes.map(async (entry) => ({
1428
- ...entry, parsed: await Promise.all(entry.specs.map(parseFetchSpec))
1429
- })));
1430
- if (candidates.some(({ parsed: specs }) => specs.some((spec) => spec === null))) {
1431
- return unmanaged("A configured fetch refspec is outside the supported full-ref exact/one-star mapping.");
1432
- }
1433
- const parsed = candidates.map((entry) => ({
1434
- ...entry, parsed: entry.parsed.filter((spec) => spec !== null)
1435
- }));
1436
- const source = `refs/heads/${branch}`;
1437
- const excluded = (specs, ref) => specs.some((spec) => spec.negative && refspecCapture(spec.source, ref) !== null);
1438
- const selectedSpecs = parsed.find(({ name }) => name === selected.name).parsed;
1439
- if (excluded(selectedSpecs, source))
1440
- return unmanaged(`Fetch refspec excludes ${source}.`);
1441
- const destinations = new Set();
1442
- for (const spec of selectedSpecs) {
1443
- if (spec.negative || spec.destination === undefined)
1444
- continue;
1445
- const capture = refspecCapture(spec.source, source);
1446
- if (capture !== null)
1447
- destinations.add(spec.destination.replace("*", capture));
1448
- }
1449
- if (destinations.size !== 1) {
1450
- return unmanaged(`Fetch refspec does not map ${source} to exactly one destination.`);
1451
- }
1452
- const ref = [...destinations][0];
1453
- if (!ref.startsWith("refs/remotes/") || !await gitSucceeds(["check-ref-format", ref])) {
1454
- return unmanaged(`Fetch destination is not a supported remote-tracking ref: ${ref}.`);
1455
- }
1456
- for (const entry of parsed) {
1457
- for (const spec of entry.parsed) {
1458
- if (spec.negative || spec.destination === undefined)
1459
- continue;
1460
- const capture = refspecCapture(spec.destination, ref);
1461
- if (capture === null)
1462
- continue;
1463
- const otherSource = spec.source.replace("*", capture);
1464
- if (!excluded(entry.parsed, otherSource)
1465
- && (entry.name !== selected.name || otherSource !== source)) {
1466
- return unmanaged(`Another fetch source also manages ${ref}.`);
1467
- }
1468
- }
1469
- }
1470
- try {
1471
- await readDirectTrackingRef(root, ref);
1472
- }
1473
- catch (error) {
1474
- return unmanaged(error instanceof Error ? error.message : String(error));
1475
- }
1476
- return { status: "managed", remoteName: selected.name, remoteUrl: wantedUrl, ref, evidence };
1477
- }
1478
- async function readDirectTrackingRef(root, ref) {
1479
- const output = await git([
1480
- "-C", root, "for-each-ref", "--format=%(refname)%09%(objectname)%09%(symref)", "--", ref
1481
- ]);
1482
- const record = output.split("\n").find((line) => line.split("\t")[0] === ref);
1483
- if (record === undefined) {
1484
- // for-each-ref omits dangling symbolic refs. Never treat those as absent.
1485
- if (await gitSucceeds(["-C", root, "symbolic-ref", "--quiet", ref])) {
1486
- throw new Error(`Tracking destination is symbolic: ${ref}.`);
1487
- }
1488
- return null;
1489
- }
1490
- const [, commit, symbolic] = record.split("\t");
1491
- if (symbolic !== "" || commit === undefined || !isCommit(commit)) {
1492
- throw new Error(`Tracking destination is not a direct object ref: ${ref}.`);
1493
- }
1494
- return commit.toLowerCase();
1495
- }
1496
1593
  function safeIdentity(value, label) {
1497
1594
  const identity = requireText(value, label);
1498
1595
  if ([".", "..", "__proto__", "prototype", "constructor"].includes(identity)
@@ -1507,87 +1604,6 @@ function safeRef(value) {
1507
1604
  throw new Error("Git base ref is invalid.");
1508
1605
  return ref;
1509
1606
  }
1510
- async function safeFetchBranch(value) {
1511
- supportedGit();
1512
- const configuredRef = safeRef(value);
1513
- if (configuredRef === "HEAD")
1514
- return configuredRef;
1515
- const branchPrefix = "refs/heads/";
1516
- const branch = configuredRef.startsWith(branchPrefix)
1517
- ? configuredRef.slice(branchPrefix.length)
1518
- : configuredRef;
1519
- if (!await gitSucceeds(["check-ref-format", "--branch", branch])) {
1520
- throw new Error("Git stable branch is invalid.");
1521
- }
1522
- return branch;
1523
- }
1524
- async function resolveRemoteHeadBranch(remote, repositoryPath) {
1525
- let output;
1526
- try {
1527
- output = await git([
1528
- ...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
1529
- "ls-remote", "--symref", remote, "HEAD"
1530
- ]);
1531
- }
1532
- catch (error) {
1533
- const detail = error instanceof Error ? error.message : "Git command failed.";
1534
- throw new Error(`Project remote HEAD could not be resolved: ${detail}`, { cause: error });
1535
- }
1536
- const symbolicLines = output.split("\n").filter((line) => line.startsWith("ref: "));
1537
- const prefix = "ref: refs/heads/";
1538
- const suffix = "\tHEAD";
1539
- const symbolic = symbolicLines.length === 1 ? symbolicLines[0] : "";
1540
- if (!symbolic.startsWith(prefix) || !symbolic.endsWith(suffix)) {
1541
- throw invalidRemoteHead();
1542
- }
1543
- const branch = symbolic.slice(prefix.length, -suffix.length);
1544
- if (branch === "HEAD")
1545
- throw invalidRemoteHead();
1546
- try {
1547
- return await safeFetchBranch(branch);
1548
- }
1549
- catch {
1550
- throw invalidRemoteHead();
1551
- }
1552
- }
1553
- async function resolveRemoteBranchCommit(remote, branch, repositoryPath) {
1554
- const target = `refs/heads/${branch}`;
1555
- let output;
1556
- try {
1557
- output = await git([
1558
- ...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
1559
- "ls-remote", "--refs", remote, target
1560
- ]);
1561
- }
1562
- catch (error) {
1563
- const detail = error instanceof Error ? error.message : "Git command failed.";
1564
- throw new Error(`Project remote development branch could not be resolved: ${branch}: ${detail}`, { cause: error });
1565
- }
1566
- const matches = output
1567
- .split("\n")
1568
- .map((line) => line.trim())
1569
- .filter((line) => line.length > 0)
1570
- .map((line) => line.split(/\s+/u))
1571
- .filter(([commit, ref]) => ref === target && commit !== undefined);
1572
- if (matches.length !== 1) {
1573
- throw new Error(`Project remote development branch could not be resolved: ${branch}.`);
1574
- }
1575
- const commit = matches[0][0];
1576
- if (!isCommit(commit)) {
1577
- throw new Error(`Project remote development branch returned an invalid commit: ${branch}.`);
1578
- }
1579
- return commit.toLowerCase();
1580
- }
1581
- async function resolveFetchedCommit(repositoryPath, fetchedRef) {
1582
- const commit = (await gitLine([
1583
- "-C", repositoryPath,
1584
- "rev-parse", "--verify", "--end-of-options", `${fetchedRef}^{commit}`
1585
- ])).toLowerCase();
1586
- if (!/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/u.test(commit)) {
1587
- throw new Error("Git returned an invalid fetched remote commit.");
1588
- }
1589
- return commit;
1590
- }
1591
1607
  function remoteBaselineRef(remote, branch) {
1592
1608
  const identity = `${remote}\0${branch}\0${process.pid}\0${Date.now()}\0${Math.random()}`;
1593
1609
  const digest = createHash("sha256").update(identity).digest("hex");