@kbediako/codex-orchestrator 0.1.35 → 0.1.36

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.
@@ -13,6 +13,7 @@ const REQUIRED_BUCKET_PASS = new Set(['pass']);
13
13
  const REQUIRED_BUCKET_PENDING = new Set(['pending']);
14
14
  const REQUIRED_BUCKET_FAILED = new Set(['fail', 'cancel', 'skipping']);
15
15
  const MERGEABLE_STATES = new Set(['CLEAN', 'HAS_HOOKS', 'UNSTABLE']);
16
+ const ACTION_REQUIRED_MERGE_STATES = new Set(['BEHIND', 'DIRTY']);
16
17
  const BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED', 'REVIEW_REQUIRED']);
17
18
  const DO_NOT_MERGE_LABEL = /do[\s_-]*not[\s_-]*merge/i;
18
19
  const ACTIONABLE_BOT_LOGINS = new Set([
@@ -31,6 +32,13 @@ const BOT_MENTION_PATTERNS = {
31
32
  };
32
33
  const BOT_IN_PROGRESS_REACTION_CONTENT = new Set(['eyes']);
33
34
  const BOT_COMPLETE_REACTION_CONTENT = new Set(['+1', 'hooray', 'heart', 'rocket', 'laugh', 'confused']);
35
+ class PrWatchMergeExitError extends Error {
36
+ constructor(message, exitCode = 1) {
37
+ super(message);
38
+ this.name = 'PrWatchMergeExitError';
39
+ this.exitCode = exitCode;
40
+ }
41
+ }
34
42
  const PR_QUERY = `
35
43
  query($owner:String!, $repo:String!, $number:Int!) {
36
44
  repository(owner:$owner, name:$repo) {
@@ -238,6 +246,10 @@ export function printPrWatchMergeHelp(options = {}) {
238
246
  const usageCommand = typeof options.usage === 'string' && options.usage.trim().length > 0
239
247
  ? options.usage.trim()
240
248
  : 'codex-orchestrator pr watch-merge';
249
+ const defaultAutoMerge = typeof options.defaultAutoMerge === 'boolean'
250
+ ? options.defaultAutoMerge
251
+ : envFlagEnabled(process.env.PR_MONITOR_AUTO_MERGE, false);
252
+ const defaultExitOnActionRequired = Boolean(options.defaultExitOnActionRequired);
241
253
  console.log(`Usage: ${usageCommand} [options]
242
254
 
243
255
  Monitor PR checks/reviews with polling and optionally merge after a quiet window.
@@ -254,16 +266,21 @@ Options:
254
266
  --no-auto-merge Never merge automatically (monitor only)
255
267
  --delete-branch Delete remote branch when merging
256
268
  --no-delete-branch Keep remote branch after merge
269
+ --exit-on-action-required Exit non-zero when author action is required
270
+ --no-exit-on-action-required Keep monitoring even when author action is required
257
271
  --dry-run Never call gh pr merge (report only)
258
272
  -h, --help Show this help message
259
273
 
260
274
  Environment:
261
- PR_MONITOR_AUTO_MERGE=1 Default auto-merge on
275
+ PR_MONITOR_AUTO_MERGE=1 Default auto-merge on (current default: ${defaultAutoMerge ? 'on' : 'off'})
262
276
  PR_MONITOR_DELETE_BRANCH=1 Default delete branch on merge
263
277
  PR_MONITOR_QUIET_MINUTES=<n> Override quiet window default
264
278
  PR_MONITOR_INTERVAL_SECONDS=<n>
265
279
  PR_MONITOR_TIMEOUT_MINUTES=<n>
266
280
  PR_MONITOR_MERGE_METHOD=<method>`);
281
+ if (defaultExitOnActionRequired) {
282
+ console.log(' resolve-merge default: exit-on-action-required is on');
283
+ }
267
284
  }
268
285
  async function runGh(args, { allowFailure = false } = {}) {
269
286
  return await new Promise((resolve, reject) => {
@@ -303,6 +320,39 @@ async function runGh(args, { allowFailure = false } = {}) {
303
320
  });
304
321
  });
305
322
  }
323
+ async function runGit(args, { allowFailure = false } = {}) {
324
+ return await new Promise((resolve, reject) => {
325
+ const child = spawn('git', args, {
326
+ env: process.env,
327
+ stdio: ['ignore', 'pipe', 'pipe']
328
+ });
329
+ let stdout = '';
330
+ let stderr = '';
331
+ child.stdout?.on('data', (chunk) => {
332
+ stdout += chunk.toString();
333
+ });
334
+ child.stderr?.on('data', (chunk) => {
335
+ stderr += chunk.toString();
336
+ });
337
+ child.once('error', (error) => {
338
+ reject(new Error(`Failed to run git ${args.join(' ')}: ${error.message}`));
339
+ });
340
+ child.once('close', (code) => {
341
+ const exitCode = typeof code === 'number' ? code : 1;
342
+ const result = {
343
+ exitCode,
344
+ stdout: stdout.trim(),
345
+ stderr: stderr.trim()
346
+ };
347
+ if (exitCode === 0 || allowFailure) {
348
+ resolve(result);
349
+ return;
350
+ }
351
+ const detail = result.stderr || result.stdout || `exit code ${exitCode}`;
352
+ reject(new Error(`git ${args.join(' ')} failed: ${detail}`));
353
+ });
354
+ });
355
+ }
306
356
  async function runGhJson(args) {
307
357
  const result = await runGh(args);
308
358
  try {
@@ -327,6 +377,39 @@ async function ensureGhAuth() {
327
377
  throw new Error('GitHub CLI is not authenticated for github.com. Run `gh auth login` and retry.');
328
378
  }
329
379
  }
380
+ export function parseGitHubRepoFromRemoteUrl(rawUrl) {
381
+ if (typeof rawUrl !== 'string' || rawUrl.trim().length === 0) {
382
+ return null;
383
+ }
384
+ const normalized = rawUrl.trim();
385
+ const patterns = [
386
+ /^git@github\.com:(?<owner>[^/\s]+)\/(?<repo>[^/\s]+?)(?:\.git)?$/iu,
387
+ /^https?:\/\/github\.com\/(?<owner>[^/\s]+)\/(?<repo>[^/\s]+?)(?:\.git)?\/?$/iu,
388
+ /^ssh:\/\/git@github\.com\/(?<owner>[^/\s]+)\/(?<repo>[^/\s]+?)(?:\.git)?\/?$/iu
389
+ ];
390
+ for (const pattern of patterns) {
391
+ const match = normalized.match(pattern);
392
+ const owner = match?.groups?.owner?.trim();
393
+ const repo = match?.groups?.repo?.trim();
394
+ if (owner && repo) {
395
+ return { owner, repo };
396
+ }
397
+ }
398
+ return null;
399
+ }
400
+ async function resolveRepoFromGitRemote() {
401
+ let result;
402
+ try {
403
+ result = await runGit(['remote', 'get-url', 'origin'], { allowFailure: true });
404
+ }
405
+ catch {
406
+ return null;
407
+ }
408
+ if (result.exitCode !== 0 || !result.stdout) {
409
+ return null;
410
+ }
411
+ return parseGitHubRepoFromRemoteUrl(result.stdout);
412
+ }
330
413
  async function resolveRepo(ownerArg, repoArg) {
331
414
  if (ownerArg && repoArg) {
332
415
  return { owner: ownerArg, repo: repoArg };
@@ -334,6 +417,10 @@ async function resolveRepo(ownerArg, repoArg) {
334
417
  if (ownerArg || repoArg) {
335
418
  throw new Error('Provide both --owner and --repo, or neither.');
336
419
  }
420
+ const gitRemoteRepo = await resolveRepoFromGitRemote();
421
+ if (gitRemoteRepo) {
422
+ return gitRemoteRepo;
423
+ }
337
424
  const response = await runGhJson(['repo', 'view', '--json', 'nameWithOwner']);
338
425
  const nameWithOwner = response?.nameWithOwner;
339
426
  if (typeof nameWithOwner !== 'string' || !nameWithOwner.includes('/')) {
@@ -342,11 +429,18 @@ async function resolveRepo(ownerArg, repoArg) {
342
429
  const [owner, repo] = nameWithOwner.split('/');
343
430
  return { owner, repo };
344
431
  }
345
- async function resolvePrNumber(prArg) {
432
+ export function buildPrNumberViewArgs(owner, repo) {
433
+ const args = ['pr', 'view', '--json', 'number'];
434
+ if (typeof owner === 'string' && owner.trim().length > 0 && typeof repo === 'string' && repo.trim().length > 0) {
435
+ args.push('--repo', `${owner.trim()}/${repo.trim()}`);
436
+ }
437
+ return args;
438
+ }
439
+ async function resolvePrNumber(prArg, owner, repo) {
346
440
  if (prArg !== undefined) {
347
441
  return parseInteger('pr', prArg, null);
348
442
  }
349
- const response = await runGhJson(['pr', 'view', '--json', 'number']);
443
+ const response = await runGhJson(buildPrNumberViewArgs(owner, repo));
350
444
  const number = response?.number;
351
445
  if (!Number.isInteger(number) || number <= 0) {
352
446
  throw new Error('Unable to infer PR number from current branch.');
@@ -580,6 +674,46 @@ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFe
580
674
  headOid: pr.commits?.nodes?.[0]?.commit?.oid || null
581
675
  };
582
676
  }
677
+ export function resolveActionRequiredReasons(snapshot) {
678
+ if (!snapshot || typeof snapshot !== 'object') {
679
+ return ['snapshot=unknown'];
680
+ }
681
+ const reasons = [];
682
+ const reviewDecision = normalizeEnum(snapshot.reviewDecision);
683
+ const mergeStateStatus = normalizeEnum(snapshot.mergeStateStatus);
684
+ if (Boolean(snapshot.isDraft)) {
685
+ reasons.push('draft');
686
+ }
687
+ if (Boolean(snapshot.hasDoNotMergeLabel)) {
688
+ reasons.push('label:do-not-merge');
689
+ }
690
+ if (BLOCKED_REVIEW_DECISIONS.has(reviewDecision)) {
691
+ reasons.push(`review=${reviewDecision}`);
692
+ }
693
+ if (ACTION_REQUIRED_MERGE_STATES.has(mergeStateStatus)) {
694
+ reasons.push(`merge_state=${mergeStateStatus}`);
695
+ }
696
+ if (typeof snapshot.unresolvedThreadCount === 'number' && snapshot.unresolvedThreadCount > 0) {
697
+ reasons.push(`unresolved_threads=${snapshot.unresolvedThreadCount}`);
698
+ }
699
+ if (typeof snapshot.unacknowledgedBotFeedbackCount === 'number'
700
+ && snapshot.unacknowledgedBotFeedbackCount > 0) {
701
+ reasons.push(`unacknowledged_bot_feedback=${snapshot.unacknowledgedBotFeedbackCount}`);
702
+ }
703
+ const requiredChecks = snapshot.requiredChecks && typeof snapshot.requiredChecks === 'object' ? snapshot.requiredChecks : null;
704
+ const requiredFailedCount = Array.isArray(requiredChecks?.failed) ? requiredChecks.failed.length : 0;
705
+ if (requiredFailedCount > 0 && snapshot.readyToMerge === false) {
706
+ reasons.push(`required_checks_failed=${requiredFailedCount}`);
707
+ }
708
+ else {
709
+ const rollupFailedCount = Array.isArray(snapshot.checks?.failed) ? snapshot.checks.failed.length : 0;
710
+ const rollupPendingCount = Array.isArray(snapshot.checks?.pending) ? snapshot.checks.pending.length : 0;
711
+ if (!requiredChecks && !MERGEABLE_STATES.has(mergeStateStatus) && rollupPendingCount === 0 && rollupFailedCount > 0) {
712
+ reasons.push(`checks_failed=${rollupFailedCount}`);
713
+ }
714
+ }
715
+ return reasons;
716
+ }
583
717
  function formatStatusLine(snapshot, quietRemainingMs) {
584
718
  const requiredChecks = snapshot.requiredChecks;
585
719
  const failedNames = snapshot.checks.failed.map((item) => `${item.name}:${item.state}`).join(', ') || '-';
@@ -1017,15 +1151,19 @@ async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache
1017
1151
  : null
1018
1152
  };
1019
1153
  }
1020
- async function attemptMerge({ prNumber, mergeMethod, deleteBranch, headOid }) {
1154
+ export function buildPrMergeArgs({ owner, repo, prNumber, mergeMethod, deleteBranch, headOid }) {
1021
1155
  // gh pr merge has no --yes flag; rely on non-interactive stdio + explicit merge method.
1022
- const args = ['pr', 'merge', String(prNumber), `--${mergeMethod}`];
1156
+ const args = ['pr', 'merge', String(prNumber), `--${mergeMethod}`, '--repo', `${owner}/${repo}`];
1023
1157
  if (deleteBranch) {
1024
1158
  args.push('--delete-branch');
1025
1159
  }
1026
1160
  if (headOid) {
1027
1161
  args.push('--match-head-commit', headOid);
1028
1162
  }
1163
+ return args;
1164
+ }
1165
+ async function attemptMerge({ owner, repo, prNumber, mergeMethod, deleteBranch, headOid }) {
1166
+ const args = buildPrMergeArgs({ owner, repo, prNumber, mergeMethod, deleteBranch, headOid });
1029
1167
  return await runGh(args, { allowFailure: true });
1030
1168
  }
1031
1169
  async function runPrWatchMergeOrThrow(argv, options) {
@@ -1046,6 +1184,8 @@ async function runPrWatchMergeOrThrow(argv, options) {
1046
1184
  'no-auto-merge',
1047
1185
  'delete-branch',
1048
1186
  'no-delete-branch',
1187
+ 'exit-on-action-required',
1188
+ 'no-exit-on-action-required',
1049
1189
  'dry-run',
1050
1190
  'h',
1051
1191
  'help'
@@ -1067,7 +1207,8 @@ async function runPrWatchMergeOrThrow(argv, options) {
1067
1207
  const mergeMethod = parseMergeMethod(typeof args['merge-method'] === 'string'
1068
1208
  ? args['merge-method']
1069
1209
  : process.env.PR_MONITOR_MERGE_METHOD || DEFAULT_MERGE_METHOD);
1070
- const defaultAutoMerge = envFlagEnabled(process.env.PR_MONITOR_AUTO_MERGE, false);
1210
+ const defaultAutoMergeFallback = typeof options.defaultAutoMerge === 'boolean' ? options.defaultAutoMerge : false;
1211
+ const defaultAutoMerge = envFlagEnabled(process.env.PR_MONITOR_AUTO_MERGE, defaultAutoMergeFallback);
1071
1212
  const defaultDeleteBranch = envFlagEnabled(process.env.PR_MONITOR_DELETE_BRANCH, true);
1072
1213
  let autoMerge = defaultAutoMerge;
1073
1214
  if (hasFlag(args, 'auto-merge')) {
@@ -1083,15 +1224,22 @@ async function runPrWatchMergeOrThrow(argv, options) {
1083
1224
  if (hasFlag(args, 'no-delete-branch')) {
1084
1225
  deleteBranch = false;
1085
1226
  }
1227
+ let exitOnActionRequired = Boolean(options.defaultExitOnActionRequired);
1228
+ if (hasFlag(args, 'exit-on-action-required')) {
1229
+ exitOnActionRequired = true;
1230
+ }
1231
+ if (hasFlag(args, 'no-exit-on-action-required')) {
1232
+ exitOnActionRequired = false;
1233
+ }
1086
1234
  const dryRun = hasFlag(args, 'dry-run');
1087
1235
  await ensureGhAuth();
1088
1236
  const { owner, repo } = await resolveRepo(typeof args.owner === 'string' ? args.owner : undefined, typeof args.repo === 'string' ? args.repo : undefined);
1089
- const prNumber = await resolvePrNumber(args.pr);
1237
+ const prNumber = await resolvePrNumber(args.pr, owner, repo);
1090
1238
  const intervalMs = Math.round(intervalSeconds * 1000);
1091
1239
  const quietMs = Math.round(quietMinutes * 60 * 1000);
1092
1240
  const timeoutMs = Math.round(timeoutMinutes * 60 * 1000);
1093
1241
  const deadline = Date.now() + timeoutMs;
1094
- log(`Monitoring ${owner}/${repo}#${prNumber} every ${intervalSeconds}s (quiet window ${quietMinutes}m, timeout ${timeoutMinutes}m, auto_merge=${autoMerge ? 'on' : 'off'}, dry_run=${dryRun ? 'on' : 'off'}).`);
1242
+ log(`Monitoring ${owner}/${repo}#${prNumber} every ${intervalSeconds}s (quiet window ${quietMinutes}m, timeout ${timeoutMinutes}m, auto_merge=${autoMerge ? 'on' : 'off'}, exit_on_action_required=${exitOnActionRequired ? 'on' : 'off'}, dry_run=${dryRun ? 'on' : 'off'}).`);
1095
1243
  let quietWindowStartedAt = null;
1096
1244
  let quietWindowAnchorUpdatedAt = null;
1097
1245
  let quietWindowAnchorHeadOid = null;
@@ -1142,6 +1290,13 @@ async function runPrWatchMergeOrThrow(argv, options) {
1142
1290
  const quietElapsedMs = quietWindowStartedAt ? Date.now() - quietWindowStartedAt : 0;
1143
1291
  const quietRemainingMs = quietWindowStartedAt ? Math.max(quietMs - quietElapsedMs, 0) : quietMs;
1144
1292
  log(formatStatusLine(snapshot, quietRemainingMs));
1293
+ if (exitOnActionRequired) {
1294
+ const actionRequiredReasons = resolveActionRequiredReasons(snapshot);
1295
+ if (actionRequiredReasons.length > 0) {
1296
+ const details = actionRequiredReasons.join(', ');
1297
+ throw new PrWatchMergeExitError(`Action required before merge: ${details}${snapshot.url ? ` (${snapshot.url})` : ''}`, 2);
1298
+ }
1299
+ }
1145
1300
  if (snapshot.readyToMerge && quietWindowStartedAt !== null && quietElapsedMs >= quietMs) {
1146
1301
  if (!autoMerge || dryRun) {
1147
1302
  log(dryRun
@@ -1159,6 +1314,8 @@ async function runPrWatchMergeOrThrow(argv, options) {
1159
1314
  lastMergeAttemptHeadOid = snapshot.headOid;
1160
1315
  log(`Attempting merge via gh pr merge --${mergeMethod}${deleteBranch ? ' --delete-branch' : ''}.`);
1161
1316
  const mergeResult = await attemptMerge({
1317
+ owner,
1318
+ repo,
1162
1319
  prNumber,
1163
1320
  mergeMethod,
1164
1321
  deleteBranch,
@@ -1178,7 +1335,7 @@ async function runPrWatchMergeOrThrow(argv, options) {
1178
1335
  }
1179
1336
  await sleep(Math.min(intervalMs, remainingTimeMs));
1180
1337
  }
1181
- throw new Error(`Timed out after ${timeoutMinutes} minute(s) while monitoring PR #${prNumber}.`);
1338
+ throw new PrWatchMergeExitError(`Timed out after ${timeoutMinutes} minute(s) while monitoring PR #${prNumber}.`, 3);
1182
1339
  }
1183
1340
  export async function runPrWatchMerge(argv, options = {}) {
1184
1341
  try {
@@ -1188,6 +1345,10 @@ export async function runPrWatchMerge(argv, options = {}) {
1188
1345
  catch (error) {
1189
1346
  const message = error instanceof Error ? error.message : String(error);
1190
1347
  console.error(message);
1348
+ const exitCodeCandidate = Number(error?.exitCode);
1349
+ if (Number.isInteger(exitCodeCandidate) && exitCodeCandidate > 0) {
1350
+ return exitCodeCandidate;
1351
+ }
1191
1352
  return 1;
1192
1353
  }
1193
1354
  }