@zq-silk/yui 0.15.9 → 0.15.12

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 (160) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agent/launchEnvironment.js +7 -0
  5. package/dist/agentRun/agentRun.js +3 -0
  6. package/dist/cli/commandCatalog.js +64 -16
  7. package/dist/cli/interactionPolicy.js +7 -3
  8. package/dist/cli/managedDiagnostics.js +1 -1
  9. package/dist/cli/updateOrchestrator.js +24 -1
  10. package/dist/cli/updatePorts.js +7 -3
  11. package/dist/cli/upgradeCommand.js +42 -2
  12. package/dist/cli.js +381 -107
  13. package/dist/commands/executionAuditCommands.js +10 -0
  14. package/dist/commands/globalRoleCommands.js +339 -4
  15. package/dist/commands/projectCommands.js +50 -22
  16. package/dist/commands/releaseCommands.js +18 -0
  17. package/dist/commands/taskActor.js +25 -0
  18. package/dist/commands/taskCommands.js +586 -96
  19. package/dist/commands/taskIntegrationCommands.js +19 -39
  20. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  21. package/dist/commands/taskOverviewCommand.js +4 -3
  22. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  23. package/dist/commands/taskPublicationCommands.js +11 -2
  24. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  25. package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
  26. package/dist/commands/taskRoleRuntimeStatus.js +35 -0
  27. package/dist/context/runContextPack.js +3 -0
  28. package/dist/context/taskCatalog.js +187 -0
  29. package/dist/context/taskContext.js +55 -6
  30. package/dist/controller/agentHostObservation.js +155 -0
  31. package/dist/controller/clientRuntime.js +17 -2
  32. package/dist/controller/controller.js +14 -2
  33. package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
  34. package/dist/controller/globalInputDelivery.js +132 -0
  35. package/dist/controller/jobControl.js +6 -2
  36. package/dist/controller/providerRetryAdmission.js +100 -0
  37. package/dist/controller/providerRetryDelivery.js +218 -0
  38. package/dist/controller/resourceInventory.js +14 -4
  39. package/dist/controller/resourceInventoryLinux.js +2 -6
  40. package/dist/controller/runtime.js +117 -7
  41. package/dist/controller/runtimeEventInbox.js +32 -3
  42. package/dist/controller/runtimeEventProcessor.js +26 -6
  43. package/dist/controller/runtimeHookRunFence.js +75 -19
  44. package/dist/controller/structuredProviderObservation.js +133 -70
  45. package/dist/coordination/workMailboxQueue.js +5 -0
  46. package/dist/execution/workItemExecutionProjection.js +1 -1
  47. package/dist/executor/agentExecutor.js +64 -4
  48. package/dist/executor/executorRegistry.js +3 -0
  49. package/dist/executor/fileRoleLaunchPlanner.js +78 -118
  50. package/dist/integration/deliveryObligation.js +2 -1
  51. package/dist/integration/gitIntegrationService.js +329 -386
  52. package/dist/integration/integrationAttempt.js +30 -4
  53. package/dist/integration/integrationQueueService.js +7 -7
  54. package/dist/integration/integrationSourceApplication.js +323 -0
  55. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  56. package/dist/message/globalInterrupt.js +33 -0
  57. package/dist/message/globalProviderRetry.js +15 -0
  58. package/dist/message/inputControlResolution.js +106 -0
  59. package/dist/message/message.js +367 -0
  60. package/dist/message/messageContinuation.js +126 -3
  61. package/dist/message/taskInterrupt.js +34 -0
  62. package/dist/observability/executionAudit.js +19 -0
  63. package/dist/observability/orchestrationMetrics.js +1 -1
  64. package/dist/release/releaseHandover.js +22 -0
  65. package/dist/release/releaseWorkflowPorts.js +15 -7
  66. package/dist/repository/gitWorkspace.js +430 -107
  67. package/dist/repository/projectMaintenanceLock.js +75 -18
  68. package/dist/repository/taskWorkspaceCoordinator.js +182 -101
  69. package/dist/repository/taskWorkspacePreparer.js +205 -72
  70. package/dist/repository/workItemCandidateSnapshot.js +34 -0
  71. package/dist/repository/workspaceCleanupInspection.js +187 -0
  72. package/dist/resources/resourceDiscovery.js +3 -2
  73. package/dist/runtime/agentError.js +5 -3
  74. package/dist/runtime/agentHost.js +179 -82
  75. package/dist/runtime/agentHostCompatibility.js +127 -0
  76. package/dist/runtime/agentHostProtocol.js +53 -0
  77. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  78. package/dist/runtime/codexAppServerRuntime.js +34 -3
  79. package/dist/runtime/executionEnvironment.js +0 -19
  80. package/dist/runtime/launchBroker.js +6 -0
  81. package/dist/runtime/providerControl.js +5 -1
  82. package/dist/runtime/providerRetry.js +198 -0
  83. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  84. package/dist/runtime/sessionReconciliation.js +4 -4
  85. package/dist/runtime/sessionTokenMetrics.js +15 -5
  86. package/dist/runtime/structuredProviderHost.js +6 -2
  87. package/dist/runtime/taskRuntimeIsolation.js +30 -6
  88. package/dist/runtime/taskUsageMetrics.js +275 -0
  89. package/dist/runtime/tmuxAdapters.js +5 -3
  90. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  91. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  92. package/dist/scheduler/operatorEvent.js +4 -0
  93. package/dist/scheduler/taskExecutionProjection.js +38 -6
  94. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  95. package/dist/scheduler/wakeReason.js +7 -1
  96. package/dist/scheduler/wakeupQueue.js +2 -0
  97. package/dist/setup/setupCommand.js +26 -8
  98. package/dist/storage/homeLayout.js +130 -0
  99. package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
  100. package/dist/storage/migrations/integrationContinuation.js +104 -0
  101. package/dist/storage/migrations/unifyHomeLayout.js +925 -0
  102. package/dist/storage/sqliteSchema.js +167 -4
  103. package/dist/storage/sqliteStore.js +57 -1
  104. package/dist/storage/storageVersions.js +1 -1
  105. package/dist/storage/storeRpc.js +2 -0
  106. package/dist/storage/taskCatalog.js +123 -0
  107. package/dist/storage/taskStore.js +2 -0
  108. package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
  109. package/dist/task/archiveDiagnostics.js +129 -0
  110. package/dist/task/archivePreflight.js +124 -0
  111. package/dist/task/nextAction.js +44 -11
  112. package/dist/task/publicationAdoption.js +56 -0
  113. package/dist/task/publicationReference.js +10 -0
  114. package/dist/task/remoteDelivery.js +31 -16
  115. package/dist/web/assets/client/app.js +147 -17
  116. package/dist/web/assets/client/components.js +56 -13
  117. package/dist/web/assets/client/i18n.js +78 -4
  118. package/dist/web/assets/client/taskSurface.js +108 -1
  119. package/dist/web/assets/client/view.js +39 -8
  120. package/dist/web/assets/shell.js +29 -0
  121. package/dist/web/assets/styles/layout.js +8 -1
  122. package/dist/web/assets/styles/widgets.js +12 -0
  123. package/dist/web/webServer.js +131 -4
  124. package/dist/web/webSnapshot.js +16 -6
  125. package/dist/web/webTaskSurface.js +222 -5
  126. package/dist/workspace/cleanupInspection.js +63 -0
  127. package/dist/workspace/workItemChangeSetManager.js +111 -35
  128. package/docs/agent-result-consumption.md +4 -0
  129. package/docs/agent-result-consumption.zh-CN.md +3 -0
  130. package/docs/agent-runtime-drivers.md +7 -0
  131. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  132. package/docs/architecture/README.md +2 -0
  133. package/docs/architecture/README.zh-CN.md +3 -1
  134. package/docs/architecture/capabilities-and-resources.md +30 -5
  135. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  136. package/docs/managed-turn-and-session-runtime.md +47 -0
  137. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  138. package/docs/observability/README.md +62 -0
  139. package/docs/observability/README.zh-CN.md +47 -0
  140. package/docs/project-refresh.md +77 -0
  141. package/docs/project-refresh.zh-CN.md +59 -0
  142. package/docs/provider-retry.md +70 -0
  143. package/docs/release-workflow.md +39 -0
  144. package/docs/release-workflow.zh-CN.md +29 -0
  145. package/docs/sqlite-control-plane-design.md +223 -1
  146. package/docs/task-delivery.md +133 -13
  147. package/docs/task-delivery.zh-CN.md +99 -10
  148. package/docs/task-discovery.md +102 -0
  149. package/docs/task-discovery.zh-CN.md +86 -0
  150. package/docs/testing/verification-levels.md +40 -0
  151. package/docs/testing/verification-levels.zh-CN.md +23 -0
  152. package/i18n/README.zh-CN.md +13 -7
  153. package/package.json +1 -1
  154. package/skills/yui-leader/references/execution.md +154 -51
  155. package/skills/yui-leader/references/integration.md +52 -2
  156. package/skills/yui-operator/SKILL.md +19 -3
  157. package/skills/yui-reviewer/SKILL.md +4 -0
  158. package/skills/yui-runtime/SKILL.md +42 -0
  159. package/skills/yui-runtime/references/publication.md +42 -0
  160. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -3,7 +3,18 @@ import { createHash, randomBytes } from "node:crypto";
3
3
  import { lstat, mkdir, readFile, realpath, rm } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { cleanupFailure } from "../workspace/cleanupInspection.js";
7
+ import { externalProgramConfigViolations } from "../artifacts/managedGit.js";
6
8
  const executeFile = promisify(execFile);
9
+ /** Observed partial effects, not a rollback or permission to replay a refresh. */
10
+ export class GitWorkspaceRefreshError extends Error {
11
+ result;
12
+ constructor(message, result, options) {
13
+ super(message, options);
14
+ this.result = result;
15
+ this.name = "GitWorkspaceRefreshError";
16
+ }
17
+ }
7
18
  /** A remote baseline merge stopped for an explicit Leader resolution. */
8
19
  export class RemoteBaselineConflictError extends Error {
9
20
  affectedPaths;
@@ -86,6 +97,7 @@ export class NodeGitWorkspace {
86
97
  return git([
87
98
  "-C", input.repositoryPath,
88
99
  "diff", "--no-color", "--no-ext-diff",
100
+ ...(input.exact ? ["--no-textconv", "--binary", "--full-index", "--no-renames"] : []),
89
101
  input.fromCommit, input.toCommit
90
102
  ]);
91
103
  }
@@ -172,47 +184,117 @@ export class NodeGitWorkspace {
172
184
  const initial = await this.inspect(input.repositoryPath, "HEAD");
173
185
  await this.#assertRefreshCheckout(initial.root, configuredStableRef, initial.baseCommit);
174
186
  const stableBranch = configuredStableRef === "HEAD"
175
- ? await resolveRemoteHeadBranch(remote)
187
+ ? await resolveRemoteHeadBranch(remote, initial.root)
176
188
  : configuredStableRef;
177
189
  if (stableBranch !== configuredStableRef) {
178
190
  await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
179
191
  }
180
- const stableRemoteRef = `refs/heads/${stableBranch}`;
181
- await git(["-C", initial.root, "fetch", "--no-tags", remote, stableRemoteRef]);
182
- const fetchedCommit = (await gitLine([
183
- "-C", initial.root,
184
- "rev-parse", "--verify", "--end-of-options", "FETCH_HEAD^{commit}"
185
- ])).toLowerCase();
186
- // The advertised SHA must match what was fetched. A network race, a ref
187
- // that moved mid-fetch, or any inconsistency fails closed: the stable
188
- // checkout is never advanced to an unverified commit.
189
- const advertisedCommit = await resolveRemoteBranchCommit(remote, stableBranch);
190
- if (fetchedCommit !== advertisedCommit) {
191
- throw new Error(`Project remote stable branch changed while it was fetched: ${stableBranch}.`);
192
- }
193
- await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
194
- if (fetchedCommit === initial.baseCommit) {
195
- return {
196
- fromCommit: initial.baseCommit,
197
- toCommit: fetchedCommit,
198
- changed: false
199
- };
192
+ const mapping = await resolveFetchTracking(initial.root, remote, stableBranch);
193
+ const previous = mapping.status === "managed"
194
+ ? await readDirectTrackingRef(initial.root, mapping.ref)
195
+ : null;
196
+ let tracking = mapping.status === "managed"
197
+ ? { status: "current", remoteName: mapping.remoteName, ref: mapping.ref,
198
+ fromCommit: previous, toCommit: previous }
199
+ : { status: "unmanaged", reason: mapping.reason };
200
+ const temporaryRef = `refs/yui/project-refresh/${randomBytes(16).toString("hex")}`;
201
+ let fetchedCommit;
202
+ let verifiedCommit;
203
+ let failure;
204
+ let cleanupFailed = false;
205
+ const assertMapping = async () => {
206
+ const current = await resolveFetchTracking(initial.root, remote, stableBranch);
207
+ if (JSON.stringify(current) !== JSON.stringify(mapping)) {
208
+ throw new Error("Project fetch URL or tracking mapping changed during refresh.");
209
+ }
210
+ };
211
+ try {
212
+ // An empty refmap disables opportunistic tracking updates even when the
213
+ // Project URL is a named remote. Ignore configured prune/submodule effects.
214
+ 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=",
217
+ remote, `refs/heads/${stableBranch}:${temporaryRef}`
218
+ ]);
219
+ fetchedCommit = await resolveFetchedCommit(initial.root, temporaryRef);
220
+ const advertisedCommit = await resolveRemoteBranchCommit(remote, stableBranch, initial.root);
221
+ if (fetchedCommit !== advertisedCommit) {
222
+ throw new Error(`Project remote stable branch changed while it was fetched: ${stableBranch}.`);
223
+ }
224
+ verifiedCommit = fetchedCommit;
225
+ await assertMapping();
226
+ await this.#assertRefreshCheckout(initial.root, stableBranch, initial.baseCommit);
227
+ if (fetchedCommit !== initial.baseCommit) {
228
+ if (!await this.isAncestor(initial.root, initial.baseCommit, fetchedCommit)) {
229
+ throw new Error(`Project checkout cannot be fast-forwarded from ${initial.baseCommit} to ${fetchedCommit}.`);
230
+ }
231
+ await git(["-C", initial.root, "merge", "--ff-only", "--no-edit", fetchedCommit]);
232
+ }
233
+ await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
234
+ await assertMapping();
235
+ if (mapping.status === "managed") {
236
+ // Do not follow a symbolic tracking ref into somebody else's branch.
237
+ await readDirectTrackingRef(initial.root, mapping.ref);
238
+ const old = previous ?? "0".repeat(fetchedCommit.length);
239
+ // Verify the stable branch and CAS the one configured destination in
240
+ // the same Git transaction. HEAD/index/worktree still use normal ff.
241
+ await git(["-C", initial.root, "update-ref", "--stdin"], { input: [
242
+ "start", "option no-deref",
243
+ `verify refs/heads/${stableBranch} ${fetchedCommit}`,
244
+ "option no-deref",
245
+ `update ${mapping.ref} ${fetchedCommit} ${old}`,
246
+ "prepare", "commit", ""
247
+ ].join("\n") });
248
+ tracking = {
249
+ status: previous === fetchedCommit ? "current" : "updated",
250
+ remoteName: mapping.remoteName, ref: mapping.ref,
251
+ fromCommit: previous, toCommit: fetchedCommit
252
+ };
253
+ }
254
+ await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
255
+ await assertMapping();
256
+ if (mapping.status === "managed"
257
+ && await readDirectTrackingRef(initial.root, mapping.ref) !== fetchedCommit) {
258
+ throw new Error(`Project tracking ref changed after refresh: ${mapping.ref}.`);
259
+ }
200
260
  }
201
- if (!await gitSucceeds([
202
- "-C", initial.root,
203
- "merge-base", "--is-ancestor", initial.baseCommit, fetchedCommit
204
- ])) {
205
- throw new Error(`Project checkout cannot be fast-forwarded from ${initial.baseCommit} to ${fetchedCommit}.`);
261
+ catch (error) {
262
+ failure = error;
263
+ }
264
+ finally {
265
+ if (fetchedCommit !== undefined) {
266
+ cleanupFailed = !await gitSucceeds([
267
+ "-C", initial.root, "update-ref", "--no-deref", "-d", temporaryRef, fetchedCommit
268
+ ]);
269
+ if (cleanupFailed && failure === undefined) {
270
+ failure = new Error(`Project refresh temporary ref cleanup failed: ${temporaryRef}.`);
271
+ }
272
+ }
273
+ }
274
+ if (failure !== undefined) {
275
+ const head = await this.inspect(initial.root, "HEAD").then((value) => value.baseCommit, () => null);
276
+ const detail = failure instanceof Error ? failure.message : String(failure);
277
+ if (mapping.status === "managed") {
278
+ tracking = {
279
+ status: "failed", remoteName: mapping.remoteName, ref: mapping.ref,
280
+ fromCommit: previous,
281
+ toCommit: await readDirectTrackingRef(initial.root, mapping.ref).catch(() => null),
282
+ reason: detail
283
+ };
284
+ }
285
+ throw new GitWorkspaceRefreshError(`Project refresh failed: ${detail} HEAD ${initial.baseCommit} -> ${head ?? "unknown"}; ` +
286
+ (tracking.status === "unmanaged" ? `tracking unmanaged: ${tracking.reason}` :
287
+ `tracking ${tracking.ref}: ${tracking.fromCommit ?? "absent"} -> ${tracking.toCommit ?? "absent or unreadable"}`) +
288
+ (cleanupFailed ? `; temporary ref retained: ${temporaryRef}` : ""), { fromCommit: initial.baseCommit, toCommit: head,
289
+ changed: head === null ? null : head !== initial.baseCommit,
290
+ ...(verifiedCommit === undefined ? {} : { verifiedCommit }), tracking,
291
+ ...(cleanupFailed || fetchedCommit === undefined ? { temporaryRef } : {}) }, { cause: failure });
206
292
  }
207
- await git([
208
- "-C", initial.root,
209
- "merge", "--ff-only", "--no-edit", fetchedCommit
210
- ]);
211
- await this.#assertRefreshCheckout(initial.root, stableBranch, fetchedCommit);
212
293
  return {
213
294
  fromCommit: initial.baseCommit,
214
- toCommit: fetchedCommit,
215
- changed: true
295
+ toCommit: verifiedCommit,
296
+ changed: initial.baseCommit !== verifiedCommit,
297
+ tracking
216
298
  };
217
299
  }
218
300
  /**
@@ -513,47 +595,17 @@ export class NodeGitWorkspace {
513
595
  }
514
596
  async inspectRemoteTracking(input) {
515
597
  const root = (await this.inspect(input.repositoryPath)).root;
516
- const wantedUrl = requireText(input.remoteUrl, "Project remote URL");
517
- let output;
518
- try {
519
- output = await git([
520
- "-C", root,
521
- "config", "--get-regexp", "--null",
522
- "^remote\\..*\\.url$"
523
- ]);
524
- }
525
- catch {
526
- return null;
527
- }
528
- const records = output.split("\0").filter((record) => record.length > 0);
529
- const matches = [];
530
- for (const record of records) {
531
- const separator = record.indexOf("\n");
532
- if (separator < 0)
533
- continue;
534
- const key = record.slice(0, separator).trim();
535
- const url = record.slice(separator + 1).trim();
536
- const prefix = "remote.";
537
- const suffix = ".url";
538
- if (!key.startsWith(prefix) || !key.endsWith(suffix) || url !== wantedUrl)
539
- continue;
540
- matches.push({ remoteName: key.slice(prefix.length, -suffix.length), url });
541
- }
542
- if (matches.length !== 1)
543
- return null;
544
- const remoteName = requireText(matches[0].remoteName, "Git remote name");
545
598
  const branch = await safeFetchBranch(input.branch);
546
- const ref = `refs/remotes/${remoteName}/${branch}`;
547
- if (!await gitSucceeds(["check-ref-format", ref])) {
548
- throw new Error("Git remote tracking ref is invalid.");
549
- }
550
- if (!await this.refExists(root, ref))
599
+ const mapping = await resolveFetchTracking(root, safeRemote(input.remoteUrl), branch);
600
+ if (mapping.status !== "managed")
601
+ return null;
602
+ if (!await this.refExists(root, mapping.ref))
551
603
  return null;
552
604
  return {
553
- remoteName,
554
- remoteUrl: matches[0].url,
555
- ref,
556
- commit: (await this.inspect(root, ref)).baseCommit
605
+ remoteName: mapping.remoteName,
606
+ remoteUrl: mapping.remoteUrl,
607
+ ref: mapping.ref,
608
+ commit: (await this.inspect(root, mapping.ref)).baseCommit
557
609
  };
558
610
  }
559
611
  async mergeBase(input) {
@@ -571,9 +623,13 @@ export class NodeGitWorkspace {
571
623
  }
572
624
  async isClean(repositoryPath) {
573
625
  const root = (await this.inspect(repositoryPath)).root;
574
- const status = await git([
575
- "-C", root, "status", "--porcelain=v1", "--untracked-files=all"
576
- ]);
626
+ const status = await git(["--no-optional-locks", "-C", root,
627
+ "status", "--porcelain=v1", "--untracked-files=all"]);
628
+ return status.length === 0;
629
+ }
630
+ async inspectClean(repositoryPath) {
631
+ const root = (await this.inspect(repositoryPath)).root;
632
+ const status = await readWorktreeStatus(root);
577
633
  return status.length === 0;
578
634
  }
579
635
  async mergeWorktree(input) {
@@ -660,6 +716,11 @@ export class NodeGitWorkspace {
660
716
  .filter((line) => line.length > 0);
661
717
  }
662
718
  async #assertRefreshCheckout(repositoryPath, stableRef, expectedCommit) {
719
+ // Status can take time. Re-read branch and HEAD after it so a checkout
720
+ // switched while status was running is not subsequently fast-forwarded.
721
+ if (!await this.isClean(repositoryPath)) {
722
+ throw new Error("Project checkout must be clean before it can be refreshed.");
723
+ }
663
724
  if (stableRef !== "HEAD") {
664
725
  const currentBranch = await this.headRef(repositoryPath);
665
726
  if (currentBranch !== stableRef) {
@@ -671,15 +732,15 @@ export class NodeGitWorkspace {
671
732
  if (head.baseCommit !== expectedCommit) {
672
733
  throw new Error("Project checkout changed while it was being refreshed.");
673
734
  }
674
- if (!await this.isClean(head.root)) {
675
- throw new Error("Project checkout must be clean before it can be refreshed.");
676
- }
677
735
  }
678
736
  async ensureWorktree(input) {
679
737
  return this.#ensureManagedWorktree({
680
738
  repositoryPath: input.repositoryPath,
681
739
  container: input.container,
682
- identity: worktreeIdentity(input.taskSegment, input.roleName),
740
+ identity: {
741
+ directory: input.directory,
742
+ branch: worktreeIdentity(input.taskSegment, input.roleName).branch
743
+ },
683
744
  baseRef: input.baseRef
684
745
  });
685
746
  }
@@ -687,7 +748,10 @@ export class NodeGitWorkspace {
687
748
  return this.#ensureManagedWorktree({
688
749
  repositoryPath: input.repositoryPath,
689
750
  container: input.container,
690
- identity: integrationWorktreeIdentity(input.taskSegment, input.integrationId),
751
+ identity: {
752
+ directory: input.directory,
753
+ branch: integrationWorktreeIdentity(input.taskSegment, input.integrationId).branch
754
+ },
691
755
  baseRef: input.baseRef,
692
756
  allowRebase: true
693
757
  });
@@ -734,19 +798,19 @@ export class NodeGitWorkspace {
734
798
  if (state === "dirty")
735
799
  return state;
736
800
  const container = resolve(input.container);
737
- const identity = worktreeIdentity(input.taskSegment, input.roleName);
738
- const path = managedPath(container, identity.directory);
801
+ const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
802
+ const path = managedPath(container, input.directory);
739
803
  const project = await this.inspect(input.repositoryPath);
740
804
  if (state === "missing") {
741
805
  if (input.deleteBranch === true) {
742
- await git(["-C", project.root, "worktree", "prune"]);
743
- await deleteBranchIfPresent(project.root, identity.branch);
806
+ await removeMissingWorktreeRegistration(project.root, path, branch);
807
+ await deleteBranchIfPresent(project.root, branch);
744
808
  }
745
809
  return state;
746
810
  }
747
811
  await git(["-C", project.root, "worktree", "remove", "--", path]);
748
812
  if (input.deleteBranch === true) {
749
- await deleteBranchIfPresent(project.root, identity.branch);
813
+ await deleteBranchIfPresent(project.root, branch);
750
814
  }
751
815
  return "removed";
752
816
  }
@@ -779,6 +843,42 @@ export class NodeGitWorkspace {
779
843
  }
780
844
  return "removed";
781
845
  }
846
+ async removeTaskClone(input) {
847
+ const state = await this.inspectTaskClone(input);
848
+ if (state !== "clean")
849
+ return state;
850
+ await rm(managedPath(resolve(input.container), input.directory), { recursive: true });
851
+ return "removed";
852
+ }
853
+ async inspectTaskClone(input) {
854
+ const identity = worktreeIdentity(input.taskSegment, "main");
855
+ const expected = managedPath(resolve(input.container), input.directory);
856
+ if (resolve(input.path) !== expected || input.branch !== identity.branch) {
857
+ cleanupFailure("workspace-identity-mismatch", "Task clone does not match its recorded managed identity.", { branch: identity.branch }, { branch: input.branch, pathMatches: resolve(input.path) === expected });
858
+ }
859
+ const kind = await pathKind(expected);
860
+ if (kind === undefined)
861
+ return "missing";
862
+ if (kind === "symlink")
863
+ cleanupFailure("workspace-identity-mismatch", "Task clone must not be a symbolic link.", "real directory", "symbolic link");
864
+ await canonicalContainer(input.container, false);
865
+ if (await realpath(expected) !== expected || await pathKind(join(expected, ".git")) !== "directory") {
866
+ cleanupFailure("workspace-identity-mismatch", "Task clone is not a standalone owned Git directory.", "standalone owned clone", "different Git layout or symbolic ancestor");
867
+ }
868
+ const repository = await this.inspect(expected);
869
+ if (repository.root !== expected || repository.gitDirectory !== join(expected, ".git")) {
870
+ cleanupFailure("workspace-identity-mismatch", "Task clone Git ownership cannot be established.", "exact root and Git directory", "different Git ownership");
871
+ }
872
+ await assertExpectedBranch(expected, input.branch);
873
+ if (!await this.inspectClean(expected))
874
+ return "dirty";
875
+ const worktrees = (await git(["-C", expected, "worktree", "list", "--porcelain", "-z"]))
876
+ .split("\0").filter(line => line.startsWith("worktree "));
877
+ if (worktrees.length !== 1 || worktrees[0] !== `worktree ${expected}`) {
878
+ cleanupFailure("git-worktree-registrations", "Task clone still owns Git worktree registrations; retained.", 1, worktrees.length);
879
+ }
880
+ return "clean";
881
+ }
782
882
  async inspectRecordedWorktree(input) {
783
883
  const inspected = await inspectExactRecordedWorktree(input);
784
884
  return inspected?.state ?? "missing";
@@ -824,23 +924,31 @@ export class NodeGitWorkspace {
824
924
  }
825
925
  async inspectWorktree(input) {
826
926
  const container = resolve(input.container);
827
- const path = managedPath(container, worktreeIdentity(input.taskSegment, input.roleName).directory);
927
+ const path = managedPath(container, input.directory);
828
928
  const kind = await pathKind(path);
829
- if (kind === undefined)
929
+ const branch = input.expectedBranch ?? worktreeIdentity(input.taskSegment, input.roleName).branch;
930
+ if (kind === undefined) {
931
+ const project = await this.inspect(input.repositoryPath);
932
+ await inspectMissingWorktreeRegistration(project.root, path, branch);
830
933
  return "missing";
934
+ }
831
935
  if (kind === "symlink")
832
- throw new Error("Managed worktree path must not be a symbolic link.");
936
+ cleanupFailure("workspace-identity-mismatch", "Managed worktree path must not be a symbolic link.", "real directory", "symbolic link");
833
937
  const canonicalContainerPath = await canonicalContainer(container, false);
834
938
  const project = await this.inspect(input.repositoryPath);
835
939
  await assertOwnedWorktree(project, canonicalContainerPath, path);
836
- const porcelain = await git(["-C", path, "status", "--porcelain=v1", "--untracked-files=all"]);
940
+ await assertExpectedBranch(path, branch);
941
+ const porcelain = await readWorktreeStatus(path);
837
942
  return porcelain.length > 0 ? "dirty" : "clean";
838
943
  }
839
944
  async removeIntegrationWorktree(input) {
840
945
  return this.#removeManagedWorktree({
841
946
  repositoryPath: input.repositoryPath,
842
947
  container: input.container,
843
- identity: integrationWorktreeIdentity(input.taskSegment, input.integrationId),
948
+ identity: {
949
+ directory: input.directory,
950
+ branch: integrationWorktreeIdentity(input.taskSegment, input.integrationId).branch
951
+ },
844
952
  discardChanges: input.discardChanges
845
953
  });
846
954
  }
@@ -851,6 +959,9 @@ export class NodeGitWorkspace {
851
959
  if (kind === "symlink")
852
960
  throw new Error("Managed worktree path must not be a symbolic link.");
853
961
  const repository = await this.inspect(input.repositoryPath);
962
+ if (kind === undefined) {
963
+ await removeMissingWorktreeRegistration(repository.root, path, input.identity.branch);
964
+ }
854
965
  if (kind === "directory") {
855
966
  const canonicalContainerPath = await canonicalContainer(container, false);
856
967
  await assertOwnedWorktree(repository, canonicalContainerPath, path);
@@ -859,9 +970,7 @@ export class NodeGitWorkspace {
859
970
  await git(["-C", repository.root, "worktree", "remove", "--force", "--", path]);
860
971
  }
861
972
  else {
862
- const porcelain = await git([
863
- "-C", path, "status", "--porcelain=v1", "--untracked-files=all"
864
- ]);
973
+ const porcelain = await readWorktreeStatus(path);
865
974
  if (porcelain.length > 0)
866
975
  return "dirty";
867
976
  if (input.expectedBaseCommit !== undefined
@@ -884,6 +993,27 @@ export class NodeGitWorkspace {
884
993
  return kind === "directory" ? "removed" : "missing";
885
994
  }
886
995
  }
996
+ /** Missing paths do not imply missing Git metadata. Never prune another
997
+ * workspace's registration as a side effect of cleaning this exact owner.
998
+ */
999
+ async function removeMissingWorktreeRegistration(repositoryRoot, path, branch) {
1000
+ if (!await inspectMissingWorktreeRegistration(repositoryRoot, path, branch))
1001
+ return;
1002
+ if (await pathKind(path) !== undefined)
1003
+ throw new Error(`Worktree reappeared before metadata cleanup: ${path}.`);
1004
+ await git(["-C", repositoryRoot, "worktree", "remove", "--force", "--", path]);
1005
+ }
1006
+ async function inspectMissingWorktreeRegistration(repositoryRoot, path, branch) {
1007
+ const records = (await git(["-C", repositoryRoot, "worktree", "list", "--porcelain", "-z"])).split("\0\0");
1008
+ const fields = records.map(record => record.split("\0")).find(record => record.includes(`worktree ${path}`));
1009
+ if (fields === undefined)
1010
+ return false;
1011
+ if (!fields.includes(`branch refs/heads/${branch}`) || fields.some(field => field.startsWith("locked"))) {
1012
+ 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,
1013
+ locked: fields.some(field => field.startsWith("locked")) });
1014
+ }
1015
+ return true;
1016
+ }
887
1017
  async function deleteBranchIfPresent(repositoryRoot, branch) {
888
1018
  const branchRef = `refs/heads/${branch}`;
889
1019
  if (!await gitSucceeds([
@@ -942,7 +1072,7 @@ async function assertExpectedBranch(path, expected, allowRebase = false) {
942
1072
  }
943
1073
  const branch = await gitLine(["-C", path, "symbolic-ref", "--short", "HEAD"]);
944
1074
  if (branch !== expected) {
945
- throw new Error(`Managed worktree is on an unexpected branch: ${branch}.`);
1075
+ cleanupFailure("workspace-identity-mismatch", "Managed worktree is on an unexpected branch.", expected, branch);
946
1076
  }
947
1077
  }
948
1078
  async function assertOwnedWorktree(project, container, path) {
@@ -955,14 +1085,14 @@ async function assertOwnedWorktree(project, container, path) {
955
1085
  throw new Error("Managed worktree root does not match its deterministic path.");
956
1086
  const common = await canonicalDirectory(await gitLine(["-C", path, "rev-parse", "--path-format=absolute", "--git-common-dir"]), "Managed Git common directory");
957
1087
  if (common !== project.gitDirectory) {
958
- throw new Error("Managed worktree belongs to another project.");
1088
+ cleanupFailure("workspace-identity-mismatch", "Managed worktree belongs to another project.", "Task main Git common directory", "different Git common directory");
959
1089
  }
960
1090
  }
961
1091
  async function inspectExactRecordedWorktree(input) {
962
1092
  const container = resolve(input.container);
963
- const identity = worktreeIdentity(input.taskSegment, input.roleName);
964
- const expectedPath = managedPath(container, identity.directory);
965
- if (resolve(input.path) !== expectedPath || input.branch !== identity.branch) {
1093
+ const expectedBranch = worktreeIdentity(input.taskSegment, input.roleName).branch;
1094
+ const expectedPath = managedPath(container, input.directory);
1095
+ if (resolve(input.path) !== expectedPath || input.branch !== expectedBranch) {
966
1096
  throw new Error("Recorded managed worktree identity is invalid.");
967
1097
  }
968
1098
  const kind = await pathKind(expectedPath);
@@ -1023,9 +1153,9 @@ async function resolveRefCommit(repositoryPath, ref) {
1023
1153
  async function recordedWorktreeWithoutRepository(input, cause) {
1024
1154
  if (await pathKind(input.repositoryPath) !== undefined)
1025
1155
  throw cause;
1026
- const identity = worktreeIdentity(input.taskSegment, input.roleName);
1027
- const expectedPath = managedPath(resolve(input.container), identity.directory);
1028
- if (resolve(input.path) !== expectedPath || input.branch !== identity.branch) {
1156
+ const branch = worktreeIdentity(input.taskSegment, input.roleName).branch;
1157
+ const expectedPath = managedPath(resolve(input.container), input.directory);
1158
+ if (resolve(input.path) !== expectedPath || input.branch !== branch) {
1029
1159
  throw cause;
1030
1160
  }
1031
1161
  if (await pathKind(expectedPath) === undefined)
@@ -1090,13 +1220,72 @@ async function pathKind(path) {
1090
1220
  throw error;
1091
1221
  }
1092
1222
  }
1093
- async function git(args) {
1223
+ /** Status can execute clean/process filters to compare equal-size changed
1224
+ * files. Preserve native normalization: report unknown instead of disabling
1225
+ * a filter and misclassifying its output as clean/dirty. Include scopes so a
1226
+ * native global/worktree include cannot hide the same execution requirement.
1227
+ */
1228
+ async function readWorktreeStatus(path) {
1229
+ await assertStatusProgramsAbsent(path);
1230
+ return readStatusGit(path, ["status", "--porcelain=v1", "--untracked-files=all"]);
1231
+ }
1232
+ async function readStatusGit(path, args) {
1233
+ return git(["--no-optional-locks", "-c", "core.fsmonitor=false",
1234
+ "-c", "protocol.allow=never", "-c", "protocol.file.allow=never",
1235
+ "-C", path, ...args], { environment: { GIT_NO_LAZY_FETCH: "1" } });
1236
+ }
1237
+ async function assertStatusProgramsAbsent(path) {
1238
+ const configuration = await readStatusGit(path, ["config", "--list", "--null", "--includes"]);
1239
+ const filters = externalProgramConfigViolations(configuration)
1240
+ .filter(key => key.startsWith("filter.") && (key.endsWith(".clean") || key.endsWith(".process")));
1241
+ const files = (await readStatusGit(path, ["ls-files", "--stage", "-z"])).split("\0");
1242
+ if (filters.length > 0) {
1243
+ // Merely having native LFS configured globally is not a violation. Only
1244
+ // filters selected by tracked regular files' effective attributes can run
1245
+ // during status. check-attr reads those selections without executing them.
1246
+ const configured = new Set(filters.map(key => key.slice(7, key.lastIndexOf("."))));
1247
+ const tracked = [...new Set(files.filter(line => line.startsWith("100"))
1248
+ .map(line => line.slice(line.indexOf("\t") + 1)))];
1249
+ for (let offset = 0; offset < tracked.length; offset += 128) {
1250
+ const attributes = (await readStatusGit(path, ["check-attr", "-z", "filter", "--",
1251
+ ...tracked.slice(offset, offset + 128)])).split("\0");
1252
+ for (let index = 2; index < attributes.length; index += 3) {
1253
+ if (!configured.has(attributes[index].toLowerCase()))
1254
+ continue;
1255
+ 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");
1256
+ }
1257
+ }
1258
+ }
1259
+ // Status also examines initialized submodules. Inspect their configuration
1260
+ // before asking Git to recurse; uninitialized gitlinks execute nothing.
1261
+ for (const record of files.filter(line => line.startsWith("160000 "))) {
1262
+ const child = join(path, record.slice(record.indexOf("\t") + 1));
1263
+ assertContained(path, child);
1264
+ const gitEntry = await lstat(join(child, ".git")).catch(error => {
1265
+ if (isErrno(error, "ENOENT"))
1266
+ return undefined;
1267
+ throw error;
1268
+ });
1269
+ if (gitEntry === undefined)
1270
+ continue;
1271
+ if (await realpath(child) !== child
1272
+ || (await readStatusGit(child, ["rev-parse", "--show-toplevel"])).trimEnd() !== child) {
1273
+ cleanupFailure("workspace-identity-mismatch", "Submodule identity cannot be safely inspected.", "owned submodule directory", "different or symbolic root");
1274
+ }
1275
+ await assertStatusProgramsAbsent(child);
1276
+ }
1277
+ }
1278
+ async function git(args, options = {}) {
1094
1279
  try {
1095
- const result = await executeFile("git", [...args], {
1280
+ const execution = executeFile("git", [...args], {
1096
1281
  encoding: "utf8",
1097
1282
  maxBuffer: 1024 * 1024,
1098
- timeout: 30_000
1283
+ timeout: 30_000,
1284
+ ...(options.environment === undefined ? {} : { env: { ...process.env, ...options.environment } })
1099
1285
  });
1286
+ if (options.input !== undefined)
1287
+ execution.child.stdin.end(options.input);
1288
+ const result = await execution;
1100
1289
  return result.stdout;
1101
1290
  }
1102
1291
  catch (error) {
@@ -1124,6 +1313,134 @@ async function gitLine(args) {
1124
1313
  }
1125
1314
  return lines[0];
1126
1315
  }
1316
+ /** Match one exact or one-star Git refspec, in either direction. */
1317
+ function refspecCapture(pattern, ref) {
1318
+ const star = pattern.indexOf("*");
1319
+ if (star < 0)
1320
+ return pattern === ref ? "" : null;
1321
+ const prefix = pattern.slice(0, star);
1322
+ const suffix = pattern.slice(star + 1);
1323
+ return ref.startsWith(prefix) && ref.endsWith(suffix)
1324
+ && ref.length >= prefix.length + suffix.length
1325
+ ? ref.slice(prefix.length, ref.length - suffix.length) : null;
1326
+ }
1327
+ async function parseFetchSpec(value) {
1328
+ const negative = value.startsWith("^");
1329
+ const text = value.replace(/^[+^]/u, "");
1330
+ const parts = text.split(":");
1331
+ const source = parts[0];
1332
+ const destination = parts[1] || undefined;
1333
+ if (parts.length > 2 || (negative && parts.length !== 1)
1334
+ || !source.startsWith("refs/")
1335
+ || !await gitSucceeds(["check-ref-format", "--refspec-pattern", source])
1336
+ || (destination !== undefined && (!destination.startsWith("refs/")
1337
+ || !await gitSucceeds(["check-ref-format", "--refspec-pattern", destination])
1338
+ || source.includes("*") !== destination.includes("*"))))
1339
+ return null;
1340
+ return { source, ...(destination === undefined ? {} : { destination }), negative };
1341
+ }
1342
+ async function gitConfigValues(root, key) {
1343
+ try {
1344
+ return (await git(["-C", root, "config", "--null", "--get-all", key]))
1345
+ .split("\0").filter((value) => value.length > 0);
1346
+ }
1347
+ catch (error) {
1348
+ const cause = error instanceof Error ? error.cause : undefined;
1349
+ if (typeof cause === "object" && cause !== null && "code" in cause && cause.code === 1) {
1350
+ return [];
1351
+ }
1352
+ throw error;
1353
+ }
1354
+ }
1355
+ /**
1356
+ * Prove a unique configured fetch destination, never infer it from a remote
1357
+ * name. Git resolves insteadOf aliases; push URLs are deliberately irrelevant.
1358
+ * Unsafe/ambiguous configurations remain usable for HEAD-only refresh.
1359
+ */
1360
+ async function resolveFetchTracking(root, remote, branch) {
1361
+ const wantedUrl = await gitLine(["-C", root, "ls-remote", "--get-url", "--", remote]);
1362
+ const names = (await git(["-C", root, "remote"])).trim().split("\n").filter(Boolean).sort();
1363
+ const remotes = await Promise.all(names.map(async (name) => ({
1364
+ name,
1365
+ urls: (await git(["-C", root, "remote", "get-url", "--all", "--", name])).trimEnd().split("\n"),
1366
+ specs: await gitConfigValues(root, `remote.${name}.fetch`)
1367
+ })));
1368
+ const evidence = JSON.stringify({ wantedUrl, remotes });
1369
+ const unmanaged = (reason) => ({ status: "unmanaged", reason, evidence });
1370
+ const matching = remotes.filter(({ urls }) => urls[0] === wantedUrl);
1371
+ if (matching.length !== 1 || matching[0].urls.length !== 1) {
1372
+ return unmanaged("Project fetch URL does not identify exactly one single-URL Git remote.");
1373
+ }
1374
+ const selected = matching[0];
1375
+ const candidates = await Promise.all(remotes.map(async (entry) => ({
1376
+ ...entry, parsed: await Promise.all(entry.specs.map(parseFetchSpec))
1377
+ })));
1378
+ if (candidates.some(({ parsed: specs }) => specs.some((spec) => spec === null))) {
1379
+ return unmanaged("A configured fetch refspec is outside the supported full-ref exact/one-star mapping.");
1380
+ }
1381
+ const parsed = candidates.map((entry) => ({
1382
+ ...entry, parsed: entry.parsed.filter((spec) => spec !== null)
1383
+ }));
1384
+ const source = `refs/heads/${branch}`;
1385
+ const excluded = (specs, ref) => specs.some((spec) => spec.negative && refspecCapture(spec.source, ref) !== null);
1386
+ const selectedSpecs = parsed.find(({ name }) => name === selected.name).parsed;
1387
+ if (excluded(selectedSpecs, source))
1388
+ return unmanaged(`Fetch refspec excludes ${source}.`);
1389
+ const destinations = new Set();
1390
+ for (const spec of selectedSpecs) {
1391
+ if (spec.negative || spec.destination === undefined)
1392
+ continue;
1393
+ const capture = refspecCapture(spec.source, source);
1394
+ if (capture !== null)
1395
+ destinations.add(spec.destination.replace("*", capture));
1396
+ }
1397
+ if (destinations.size !== 1) {
1398
+ return unmanaged(`Fetch refspec does not map ${source} to exactly one destination.`);
1399
+ }
1400
+ const ref = [...destinations][0];
1401
+ if (!ref.startsWith("refs/remotes/") || !await gitSucceeds(["check-ref-format", ref])) {
1402
+ return unmanaged(`Fetch destination is not a supported remote-tracking ref: ${ref}.`);
1403
+ }
1404
+ for (const entry of parsed) {
1405
+ for (const spec of entry.parsed) {
1406
+ if (spec.negative || spec.destination === undefined)
1407
+ continue;
1408
+ const capture = refspecCapture(spec.destination, ref);
1409
+ if (capture === null)
1410
+ continue;
1411
+ const otherSource = spec.source.replace("*", capture);
1412
+ if (!excluded(entry.parsed, otherSource)
1413
+ && (entry.name !== selected.name || otherSource !== source)) {
1414
+ return unmanaged(`Another fetch source also manages ${ref}.`);
1415
+ }
1416
+ }
1417
+ }
1418
+ try {
1419
+ await readDirectTrackingRef(root, ref);
1420
+ }
1421
+ catch (error) {
1422
+ return unmanaged(error instanceof Error ? error.message : String(error));
1423
+ }
1424
+ return { status: "managed", remoteName: selected.name, remoteUrl: wantedUrl, ref, evidence };
1425
+ }
1426
+ async function readDirectTrackingRef(root, ref) {
1427
+ const output = await git([
1428
+ "-C", root, "for-each-ref", "--format=%(refname)%09%(objectname)%09%(symref)", "--", ref
1429
+ ]);
1430
+ const record = output.split("\n").find((line) => line.split("\t")[0] === ref);
1431
+ if (record === undefined) {
1432
+ // for-each-ref omits dangling symbolic refs. Never treat those as absent.
1433
+ if (await gitSucceeds(["-C", root, "symbolic-ref", "--quiet", ref])) {
1434
+ throw new Error(`Tracking destination is symbolic: ${ref}.`);
1435
+ }
1436
+ return null;
1437
+ }
1438
+ const [, commit, symbolic] = record.split("\t");
1439
+ if (symbolic !== "" || commit === undefined || !isCommit(commit)) {
1440
+ throw new Error(`Tracking destination is not a direct object ref: ${ref}.`);
1441
+ }
1442
+ return commit.toLowerCase();
1443
+ }
1127
1444
  function safeIdentity(value, label) {
1128
1445
  const identity = requireText(value, label);
1129
1446
  if ([".", "..", "__proto__", "prototype", "constructor"].includes(identity)
@@ -1151,10 +1468,13 @@ async function safeFetchBranch(value) {
1151
1468
  }
1152
1469
  return branch;
1153
1470
  }
1154
- async function resolveRemoteHeadBranch(remote) {
1471
+ async function resolveRemoteHeadBranch(remote, repositoryPath) {
1155
1472
  let output;
1156
1473
  try {
1157
- output = await git(["ls-remote", "--symref", remote, "HEAD"]);
1474
+ output = await git([
1475
+ ...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
1476
+ "ls-remote", "--symref", remote, "HEAD"
1477
+ ]);
1158
1478
  }
1159
1479
  catch (error) {
1160
1480
  const detail = error instanceof Error ? error.message : "Git command failed.";
@@ -1177,11 +1497,14 @@ async function resolveRemoteHeadBranch(remote) {
1177
1497
  throw invalidRemoteHead();
1178
1498
  }
1179
1499
  }
1180
- async function resolveRemoteBranchCommit(remote, branch) {
1500
+ async function resolveRemoteBranchCommit(remote, branch, repositoryPath) {
1181
1501
  const target = `refs/heads/${branch}`;
1182
1502
  let output;
1183
1503
  try {
1184
- output = await git(["ls-remote", "--refs", remote, target]);
1504
+ output = await git([
1505
+ ...(repositoryPath === undefined ? [] : ["-C", repositoryPath]),
1506
+ "ls-remote", "--refs", remote, target
1507
+ ]);
1185
1508
  }
1186
1509
  catch (error) {
1187
1510
  const detail = error instanceof Error ? error.message : "Git command failed.";