@zq-silk/yui 0.6.13 → 0.6.15

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 (90) hide show
  1. package/README.md +34 -5
  2. package/dist/cli/commandCatalog.js +172 -62
  3. package/dist/cli/helpRenderer.js +3 -1
  4. package/dist/cli.js +102 -11
  5. package/dist/commands/configCommands.js +521 -171
  6. package/dist/commands/deliveryGuardPreflight.js +2 -2
  7. package/dist/commands/executionAuditCommands.js +56 -3
  8. package/dist/commands/projectCommands.js +518 -55
  9. package/dist/commands/releaseCommands.js +0 -1
  10. package/dist/commands/taskActor.js +17 -0
  11. package/dist/commands/taskBaseCommands.js +29 -0
  12. package/dist/commands/taskCommands.js +577 -126
  13. package/dist/commands/taskContextCommand.js +36 -2
  14. package/dist/commands/taskNextActionCommand.js +48 -3
  15. package/dist/commands/taskPublicationCommands.js +319 -0
  16. package/dist/commands/taskRoleRuntimeStatus.js +83 -59
  17. package/dist/commands/telemetryCommands.js +14 -13
  18. package/dist/config/yuiConfig.js +161 -8
  19. package/dist/context/sessionContextBudget.js +71 -0
  20. package/dist/context/wakeNotification.js +65 -0
  21. package/dist/controller/clientRuntime.js +2 -1
  22. package/dist/controller/ephemeralResourceReaper.js +2 -1
  23. package/dist/controller/fileSchedulerStoreAdapter.js +191 -16
  24. package/dist/controller/jobSupervisor.js +5 -4
  25. package/dist/controller/resourceCleanupLinux.js +6 -6
  26. package/dist/controller/resourceInventoryLinux.js +3 -3
  27. package/dist/controller/runtime.js +88 -10
  28. package/dist/controller/updateReconciliation.js +4 -3
  29. package/dist/doctor/doctor.js +26 -7
  30. package/dist/executor/agentConfigurationCatalog.js +18 -0
  31. package/dist/executor/fileRoleLaunchPlanner.js +3 -3
  32. package/dist/lifecycle/contextBudgetRollover.js +81 -0
  33. package/dist/lifecycle/exactRunTerminalization.js +16 -2
  34. package/dist/lifecycle/providerErrorClass.js +33 -12
  35. package/dist/observability/executionAudit.js +214 -6
  36. package/dist/output/table.js +18 -0
  37. package/dist/repository/gitWorkspace.js +92 -0
  38. package/dist/repository/project.js +218 -4
  39. package/dist/repository/taskBaseFreshness.js +318 -0
  40. package/dist/repository/taskWorkspacePreparer.js +16 -2
  41. package/dist/review/deltaRecheck.js +253 -0
  42. package/dist/review/reviewConfig.js +31 -0
  43. package/dist/review/reviewFindingLedger.js +5 -1
  44. package/dist/review/reviewRound.js +156 -1
  45. package/dist/run/providerRetry.js +21 -3
  46. package/dist/run/providerRetryConfig.js +13 -60
  47. package/dist/run/recoveryProjection.js +198 -0
  48. package/dist/runtime/builtinAgentDrivers.js +3 -0
  49. package/dist/runtime/builtinTranscriptUsage.js +76 -32
  50. package/dist/runtime/continuationManager.js +17 -0
  51. package/dist/runtime/index.js +2 -0
  52. package/dist/runtime/launchDiagnostics.js +154 -0
  53. package/dist/runtime/lifecycleReservation.js +13 -0
  54. package/dist/runtime/providerContinuation.js +38 -0
  55. package/dist/runtime/providerContinuationReconciliationService.js +1 -0
  56. package/dist/runtime/providerErrorCodes.js +278 -0
  57. package/dist/runtime/runtimeHealthPolicy.js +20 -0
  58. package/dist/runtime/runtimeObservation.js +7 -1
  59. package/dist/runtime/runtimeProjection.js +115 -23
  60. package/dist/runtime/tmuxAdapters.js +242 -48
  61. package/dist/scheduler/activeRoleRunDelivery.js +139 -3
  62. package/dist/scheduler/activeTaskProgress.js +4 -3
  63. package/dist/scheduler/leaderWakeupProcessor.js +105 -15
  64. package/dist/scheduler/roleRunLiveness.js +2 -1
  65. package/dist/scheduler/roleRunStall.js +3 -2
  66. package/dist/scheduler/taskWake.js +72 -0
  67. package/dist/scheduler/wakeReason.js +64 -0
  68. package/dist/scheduler/wakeupQueue.js +2 -1
  69. package/dist/setup/setupCommand.js +1 -1
  70. package/dist/storage/migration/productionRegistry.js +325 -1
  71. package/dist/storage/sqliteSchema.js +61 -2
  72. package/dist/storage/sqliteStore.js +129 -2
  73. package/dist/storage/storeRpc.js +1 -0
  74. package/dist/storage/taskStore.js +262 -5
  75. package/dist/storage/upgrade/recordVersions.js +6 -1
  76. package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
  77. package/dist/task/completionReadiness.js +289 -0
  78. package/dist/task/publicationReference.js +123 -0
  79. package/dist/task/taskRecordReference.js +3 -1
  80. package/dist/telemetry/telemetryConfig.js +23 -18
  81. package/dist/telemetry/telemetryWiring.js +8 -8
  82. package/dist/tmux/tmuxManager.js +50 -9
  83. package/dist/web/assets/client/i18n.js +4 -0
  84. package/dist/web/assets/client/view.js +18 -0
  85. package/dist/web/webSnapshot.js +100 -10
  86. package/i18n/README.zh-CN.md +5 -5
  87. package/package.json +1 -1
  88. package/skills/yui-leader/SKILL.md +49 -10
  89. package/skills/yui-operator/SKILL.md +17 -3
  90. package/skills/yui-worker/SKILL.md +8 -0
@@ -16,6 +16,7 @@ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
16
16
  import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
17
17
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
18
18
  import { classifyAgentRunFailure, classifyIntegrationAttempt, classifyReviewRound, classifyWakeReasons, countFaultClasses } from "./faultClassification.js";
19
+ import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
19
20
  import { UNSUPPORTED } from "./runtimeIdentity.js";
20
21
  export function createProductionExecutionAuditPorts() {
21
22
  return {
@@ -84,11 +85,86 @@ function roleBucket(roleName) {
84
85
  }
85
86
  return "other";
86
87
  }
88
+ /** Maps a provider-retry lifecycle event to its last recorded decision. */
89
+ function providerRetryDecision(event) {
90
+ if (event.type === "runtime.provider-retry-waiting")
91
+ return "waiting";
92
+ if (event.type === "runtime.provider-retry-resume")
93
+ return "resume";
94
+ if (event.type === "runtime.provider-retry-session-dead")
95
+ return "session-dead";
96
+ if (event.type === "runtime.provider-retry-budget-exhausted")
97
+ return "budget-exhausted";
98
+ if (event.payload.note === "native-turn-completion-durable")
99
+ return "suppressed:completion-durable";
100
+ if (event.payload.note === "retry-budget-exhausted")
101
+ return "budget-exhausted";
102
+ if (event.payload.wouldRetry === "true")
103
+ return "retrying";
104
+ if (event.payload.shadow === "true")
105
+ return "shadow";
106
+ return "not-retried";
107
+ }
87
108
  function durationMs(run) {
88
109
  if (run.endedAt === undefined)
89
110
  return 0;
90
111
  return Math.max(0, Date.parse(run.endedAt) - Date.parse(run.createdAt));
91
112
  }
113
+ function emptyLaunchFailureCounts() {
114
+ const counts = {
115
+ total: 0,
116
+ byPhase: Object.fromEntries(RUNTIME_LAUNCH_PHASES.map((phase) => [phase, 0])),
117
+ byKind: Object.fromEntries(RUNTIME_LAUNCH_KINDS.map((kind) => [kind, 0]))
118
+ };
119
+ return counts;
120
+ }
121
+ function addLaunchFailureCounts(counts, summary) {
122
+ if (summary === undefined)
123
+ return;
124
+ const phase = /failurePhase=([a-z-]+)/u.exec(summary)?.[1];
125
+ const kind = /failureKind=([a-z-]+)/u.exec(summary)?.[1];
126
+ if (phase === undefined
127
+ || kind === undefined
128
+ || !RUNTIME_LAUNCH_PHASES.includes(phase)
129
+ || !RUNTIME_LAUNCH_KINDS.includes(kind)) {
130
+ return;
131
+ }
132
+ counts.byPhase[phase] += 1;
133
+ counts.byKind[kind] += 1;
134
+ counts.total += 1;
135
+ }
136
+ /**
137
+ * Issue 09: classify a terminal Session (stopped/broken) by its relationship
138
+ * to the last Run it carried. Runs and Sessions are separate axes: a Session
139
+ * that stops after its Run yielded is a post-completion stop, not a Run
140
+ * failure. Correlation is by Role + Agent + Adapter (the durable identity a
141
+ * Run and its Session share) and the Session's terminal update timestamp.
142
+ */
143
+ function classifyTerminalSessionRunRelation(runs, roleName, session, counts) {
144
+ const terminalAt = Date.parse(session.updatedAt);
145
+ const carried = runs
146
+ .filter((run) => (run.roleName === roleName
147
+ && run.effective.agentId === session.agentId
148
+ && run.effective.adapterId === session.adapterId
149
+ && Date.parse(run.createdAt) <= terminalAt))
150
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt));
151
+ const lastRun = carried[0];
152
+ if (lastRun === undefined) {
153
+ counts.noRun += 1;
154
+ return;
155
+ }
156
+ if (lastRun.status === "yielded"
157
+ && lastRun.endedAt !== undefined
158
+ && Date.parse(lastRun.endedAt) <= terminalAt) {
159
+ counts.postRunYielded += 1;
160
+ return;
161
+ }
162
+ if (lastRun.status === "failed") {
163
+ counts.runFailed += 1;
164
+ return;
165
+ }
166
+ counts.activeRun += 1;
167
+ }
92
168
  function ok(data) {
93
169
  return { status: "ok", data };
94
170
  }
@@ -126,7 +202,9 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
126
202
  sessions: section,
127
203
  reviews: section,
128
204
  integrations: section,
205
+ publications: section,
129
206
  events: section,
207
+ providerRetries: section,
130
208
  workItems: section,
131
209
  storage: section,
132
210
  topLongRunning: section
@@ -174,6 +252,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
174
252
  const byRole = { leader: 0, reviewer: 0, implementer: 0, other: 0 };
175
253
  const byPurpose = { execution: 0, review: 0 };
176
254
  const failures = [];
255
+ const launchFailures = emptyLaunchFailureCounts();
177
256
  for (const taskId of taskIds) {
178
257
  for (const run of store.listAgentRuns(taskId)) {
179
258
  if (!inWindow(run.createdAt, options))
@@ -197,6 +276,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
197
276
  failedCount += 1;
198
277
  failedDurationMs += duration;
199
278
  cumulativeDurationMs += duration;
279
+ addLaunchFailureCounts(launchFailures, run.summary);
200
280
  failures.push(classifyAgentRunFailure(run));
201
281
  }
202
282
  }
@@ -211,7 +291,8 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
211
291
  failedDurationMs,
212
292
  byRole,
213
293
  byPurpose,
214
- faultClasses: countFaultClasses(failures)
294
+ faultClasses: countFaultClasses(failures),
295
+ launchFailures
215
296
  });
216
297
  }
217
298
  catch (error) {
@@ -224,6 +305,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
224
305
  let withWakeReasons = 0;
225
306
  let orphanWakes = 0;
226
307
  let orphanYieldOnly = 0;
308
+ let suppressedWakes = 0;
227
309
  const byReason = new Map();
228
310
  for (const taskId of taskIds) {
229
311
  for (const run of store.listAgentRuns(taskId)) {
@@ -247,6 +329,13 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
247
329
  orphanYieldOnly += 1;
248
330
  }
249
331
  }
332
+ for (const event of store.listEvents(taskId)) {
333
+ if (event.type !== "wake.suppressed")
334
+ continue;
335
+ if (!inWindow(event.createdAt, options))
336
+ continue;
337
+ suppressedWakes += 1;
338
+ }
250
339
  }
251
340
  return ok({
252
341
  leaderRuns,
@@ -254,9 +343,10 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
254
343
  orphanWakes,
255
344
  orphanYieldOnly,
256
345
  byReason: Object.fromEntries([...byReason.entries()].sort((left, right) => right[1] - left[1])),
257
- // The quiescence/suppression counter is owned by the scheduler Issue;
258
- // this build has no producer, so report unsupported rather than 0.
259
- suppressedWakes: { status: "unsupported" }
346
+ // Scheduler single-flight suppression: wakes that were coalesced
347
+ // because the Role runtime lifecycle lane was busy. These are
348
+ // scheduler outcomes, never failed Runs.
349
+ suppressedWakes: { status: "ok", data: suppressedWakes }
260
350
  });
261
351
  }
262
352
  catch (error) {
@@ -272,7 +362,14 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
272
362
  let resets = 0;
273
363
  let lifecycleEvents = 0;
274
364
  let stopFailures = 0;
365
+ const terminalByRunRelation = {
366
+ postRunYielded: 0,
367
+ runFailed: 0,
368
+ activeRun: 0,
369
+ noRun: 0
370
+ };
275
371
  for (const taskId of taskIds) {
372
+ const runs = store.listAgentRuns(taskId);
276
373
  for (const set of store.listRoleSessionSets(taskId)) {
277
374
  const history = Array.isArray(set.history) ? set.history : [];
278
375
  for (const session of [...history, ...Object.values(set.sessions)]) {
@@ -283,6 +380,9 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
283
380
  stopped += 1;
284
381
  else
285
382
  other += 1;
383
+ if (session.status === "broken" || session.status === "stopped") {
384
+ classifyTerminalSessionRunRelation(runs, set.owner.roleName, session, terminalByRunRelation);
385
+ }
286
386
  }
287
387
  }
288
388
  for (const event of store.listEvents(taskId)) {
@@ -304,7 +404,8 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
304
404
  other,
305
405
  resets,
306
406
  lifecycleEvents,
307
- stopFailures
407
+ stopFailures,
408
+ terminalByRunRelation
308
409
  });
309
410
  }
310
411
  catch (error) {
@@ -318,6 +419,10 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
318
419
  let failedCount = 0;
319
420
  let infraFailed = 0;
320
421
  let semanticNegative = 0;
422
+ let deltaTotal = 0;
423
+ let deltaAccepted = 0;
424
+ let deltaFinding = 0;
425
+ let deltaEscalated = 0;
321
426
  const classes = [];
322
427
  for (const taskId of taskIds) {
323
428
  for (const round of store.listReviewRounds(taskId)) {
@@ -328,6 +433,15 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
328
433
  completed += 1;
329
434
  else if (round.status === "failed")
330
435
  failedCount += 1;
436
+ if (round.deltaRecheck !== undefined) {
437
+ deltaTotal += 1;
438
+ if (round.deltaRecheck.disposition === "equivalent-and-accepted")
439
+ deltaAccepted += 1;
440
+ else if (round.deltaRecheck.disposition === "finding")
441
+ deltaFinding += 1;
442
+ else if (round.deltaRecheck.disposition === "requires-full-review")
443
+ deltaEscalated += 1;
444
+ }
331
445
  const classification = classifyReviewRound(round);
332
446
  if (classification.faultClass === "review-infra")
333
447
  infraFailed += 1;
@@ -344,7 +458,13 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
344
458
  failed: failedCount,
345
459
  infraFailed,
346
460
  semanticNegative,
347
- faultClasses: countFaultClasses(classes)
461
+ faultClasses: countFaultClasses(classes),
462
+ deltaRechecks: {
463
+ total: deltaTotal,
464
+ equivalentAndAccepted: deltaAccepted,
465
+ finding: deltaFinding,
466
+ requiresFullReview: deltaEscalated
467
+ }
348
468
  });
349
469
  }
350
470
  catch (error) {
@@ -411,6 +531,41 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
411
531
  return failed(error);
412
532
  }
413
533
  })();
534
+ const publications = (() => {
535
+ try {
536
+ let total = 0;
537
+ let merged = 0;
538
+ let verified = 0;
539
+ let open = 0;
540
+ let closed = 0;
541
+ let superseded = 0;
542
+ for (const taskId of taskIds) {
543
+ const references = store.listPublicationReferences(taskId);
544
+ const supersededIds = new Set(references
545
+ .map((reference) => reference.supersedes)
546
+ .filter((id) => id !== undefined));
547
+ for (const reference of references) {
548
+ if (!inWindow(reference.createdAt, options))
549
+ continue;
550
+ total += 1;
551
+ if (reference.state === "merged")
552
+ merged += 1;
553
+ if (reference.state === "open")
554
+ open += 1;
555
+ if (reference.state === "closed")
556
+ closed += 1;
557
+ if (reference.verification === "verified")
558
+ verified += 1;
559
+ if (supersededIds.has(reference.id))
560
+ superseded += 1;
561
+ }
562
+ }
563
+ return ok({ total, merged, verified, open, closed, superseded });
564
+ }
565
+ catch (error) {
566
+ return failed(error);
567
+ }
568
+ })();
414
569
  const events = (() => {
415
570
  try {
416
571
  let total = 0;
@@ -445,6 +600,57 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
445
600
  return failed(error);
446
601
  }
447
602
  })();
603
+ const providerRetries = (() => {
604
+ try {
605
+ const entries = new Map();
606
+ for (const taskId of taskIds) {
607
+ for (const event of store.listEvents(taskId)) {
608
+ if (!inWindow(event.createdAt, options))
609
+ continue;
610
+ if (!event.type.startsWith("runtime.provider-retry-"))
611
+ continue;
612
+ const runId = event.payload.runId;
613
+ if (typeof runId !== "string")
614
+ continue;
615
+ const key = `${taskId}/${runId}`;
616
+ const previous = entries.get(key);
617
+ const attempt = Number(event.payload.attempt ?? "0");
618
+ const decision = providerRetryDecision(event);
619
+ const terminal = decision === "session-dead" || decision === "budget-exhausted";
620
+ const next = {
621
+ taskId,
622
+ runId,
623
+ roleName: typeof event.payload.roleName === "string"
624
+ ? event.payload.roleName
625
+ : previous?.roleName ?? "",
626
+ attempts: Number.isFinite(attempt) ? Math.max(previous?.attempts ?? 0, attempt) : (previous?.attempts ?? 0),
627
+ errorClass: typeof event.payload.errorClass === "string"
628
+ ? event.payload.errorClass
629
+ : previous?.errorClass ?? "",
630
+ firstFailureAt: previous?.firstFailureAt ?? event.createdAt,
631
+ lastFailureAt: event.createdAt,
632
+ ...(typeof event.payload.nextAttemptAt === "string"
633
+ ? { lastBackoffAt: event.payload.nextAttemptAt }
634
+ : previous?.lastBackoffAt !== undefined
635
+ ? { lastBackoffAt: previous.lastBackoffAt }
636
+ : {}),
637
+ decision,
638
+ terminal: (previous?.terminal ?? false) || terminal
639
+ };
640
+ entries.set(key, next);
641
+ }
642
+ }
643
+ const list = [...entries.values()];
644
+ return ok({
645
+ total: list.length,
646
+ terminal: list.filter((entry) => entry.terminal).length,
647
+ entries: list.map(({ terminal: _terminal, ...entry }) => entry)
648
+ });
649
+ }
650
+ catch (error) {
651
+ return failed(error);
652
+ }
653
+ })();
448
654
  const workItems = (() => {
449
655
  try {
450
656
  let total = 0;
@@ -540,7 +746,9 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
540
746
  sessions,
541
747
  reviews,
542
748
  integrations,
749
+ publications,
543
750
  events,
751
+ providerRetries,
544
752
  workItems,
545
753
  storage,
546
754
  topLongRunning
@@ -1,4 +1,22 @@
1
1
  import { padVisibleEnd, visibleWidth, wrapVisibleText } from "./terminal.js";
2
+ /**
3
+ * The shared renderer for every command whose output is row/column data
4
+ * (lists, overviews, settings such as `config show`). The consistent rules:
5
+ *
6
+ * - Column width: each column declares `minWidth`/`maxWidth`; widths start at
7
+ * the widest cell and shrink largest-spare-first until the table fits.
8
+ * - Alignment: cells are left-aligned and padded by visible width, so CJK and
9
+ * other wide characters align correctly.
10
+ * - Empty values: rendered as empty cells (trimmed at line end); the
11
+ * narrow-terminal record fallback skips empty detail values.
12
+ * - Terminal width: callers pass `defaultTableWidth()` (46–140 columns) unless
13
+ * a command has a narrower natural bound.
14
+ * - JSON mode: commands that have structured data pass it as `emit`'s `data`
15
+ * argument; the table text is the human view of the same values.
16
+ *
17
+ * Do not use this for long-form prose, diagnostic detail, or interactive
18
+ * prompts — those keep their own non-tabular formats.
19
+ */
2
20
  export function renderTable(title, columns, rows, maxWidth) {
3
21
  if (columns.length === 0)
4
22
  return title;
@@ -80,6 +80,44 @@ export class NodeGitWorkspace {
80
80
  ]);
81
81
  return output.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
82
82
  }
83
+ /** Issue 07: exact unified diff text between two commits. */
84
+ async diffTextBetween(input) {
85
+ return git([
86
+ "-C", input.repositoryPath,
87
+ "diff", "--no-color", "--no-ext-diff",
88
+ input.fromCommit, input.toCommit
89
+ ]);
90
+ }
91
+ /** Issue 07: sums `git diff --numstat` added/deleted lines. */
92
+ async diffNumstatBetween(input) {
93
+ const output = await git([
94
+ "-C", input.repositoryPath,
95
+ "diff", "--numstat",
96
+ input.fromCommit, input.toCommit
97
+ ]);
98
+ let addedLines = 0;
99
+ let deletedLines = 0;
100
+ for (const line of output.split("\n")) {
101
+ const trimmed = line.trim();
102
+ if (trimmed.length === 0)
103
+ continue;
104
+ const [added, deleted] = trimmed.split("\t");
105
+ // Binary files report "-" for both counts; they change the tree and
106
+ // must not be treated as zero-line edits.
107
+ if (added === "-" || deleted === "-") {
108
+ throw new Error("Delta recheck cannot assess a binary diff.");
109
+ }
110
+ const addedCount = Number(added);
111
+ const deletedCount = Number(deleted);
112
+ if (!Number.isInteger(addedCount) || !Number.isInteger(deletedCount)
113
+ || addedCount < 0 || deletedCount < 0) {
114
+ throw new Error("Git returned invalid numstat output.");
115
+ }
116
+ addedLines += addedCount;
117
+ deletedLines += deletedCount;
118
+ }
119
+ return { addedLines, deletedLines };
120
+ }
83
121
  /**
84
122
  * Files deleted between two commits. Uses `--diff-filter=D` so a rename
85
123
  * reports its source path here (and its destination in `changedFilesBetween`),
@@ -541,6 +579,60 @@ export class NodeGitWorkspace {
541
579
  safeRef(descendant)
542
580
  ]);
543
581
  }
582
+ async inspectRemoteTracking(input) {
583
+ const root = (await this.inspect(input.repositoryPath)).root;
584
+ const wantedUrl = requireText(input.remoteUrl, "Project remote URL");
585
+ let output;
586
+ try {
587
+ output = await git([
588
+ "-C", root,
589
+ "config", "--get-regexp", "--null",
590
+ "^remote\\..*\\.url$"
591
+ ]);
592
+ }
593
+ catch {
594
+ return null;
595
+ }
596
+ const records = output.split("\0").filter((record) => record.length > 0);
597
+ const matches = [];
598
+ for (const record of records) {
599
+ const separator = record.indexOf("\n");
600
+ if (separator < 0)
601
+ continue;
602
+ const key = record.slice(0, separator).trim();
603
+ const url = record.slice(separator + 1).trim();
604
+ const prefix = "remote.";
605
+ const suffix = ".url";
606
+ if (!key.startsWith(prefix) || !key.endsWith(suffix) || url !== wantedUrl)
607
+ continue;
608
+ matches.push({ remoteName: key.slice(prefix.length, -suffix.length), url });
609
+ }
610
+ if (matches.length !== 1)
611
+ return null;
612
+ const remoteName = requireText(matches[0].remoteName, "Git remote name");
613
+ const branch = await safeFetchBranch(input.branch);
614
+ const ref = `refs/remotes/${remoteName}/${branch}`;
615
+ if (!await gitSucceeds(["check-ref-format", ref])) {
616
+ throw new Error("Git remote tracking ref is invalid.");
617
+ }
618
+ if (!await this.refExists(root, ref))
619
+ return null;
620
+ return {
621
+ remoteName,
622
+ remoteUrl: matches[0].url,
623
+ ref,
624
+ commit: (await this.inspect(root, ref)).baseCommit
625
+ };
626
+ }
627
+ async mergeBase(input) {
628
+ const root = (await this.inspect(input.repositoryPath)).root;
629
+ return gitLine([
630
+ "-C", root,
631
+ "merge-base",
632
+ safeRef(input.leftCommit),
633
+ safeRef(input.rightCommit)
634
+ ]);
635
+ }
544
636
  async headRef(repositoryPath) {
545
637
  const requested = await canonicalDirectory(repositoryPath, "Project");
546
638
  return gitLine(["-C", requested, "rev-parse", "--abbrev-ref", "HEAD"]);