@jonit-dev/night-watch-cli 1.8.8-beta.2 → 1.8.8-beta.8

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.
package/dist/cli.js CHANGED
@@ -201,6 +201,35 @@ var init_job_registry = __esm({
201
201
  maxRuntime: 3600
202
202
  }
203
203
  },
204
+ {
205
+ id: "pr-resolver",
206
+ name: "PR Conflict Solver",
207
+ description: "Resolves merge conflicts via AI rebase; optionally addresses review comments and labels PRs ready-to-merge",
208
+ cliCommand: "resolve",
209
+ logName: "pr-resolver",
210
+ lockSuffix: "-pr-resolver.lock",
211
+ queuePriority: 35,
212
+ envPrefix: "NW_PR_RESOLVER",
213
+ extraFields: [
214
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
215
+ { name: "maxPrsPerRun", type: "number", defaultValue: 0 },
216
+ { name: "perPrTimeout", type: "number", defaultValue: 600 },
217
+ { name: "aiConflictResolution", type: "boolean", defaultValue: true },
218
+ { name: "aiReviewResolution", type: "boolean", defaultValue: false },
219
+ { name: "readyLabel", type: "string", defaultValue: "ready-to-merge" }
220
+ ],
221
+ defaultConfig: {
222
+ enabled: true,
223
+ schedule: "15 6,14,22 * * *",
224
+ maxRuntime: 3600,
225
+ branchPatterns: [],
226
+ maxPrsPerRun: 0,
227
+ perPrTimeout: 600,
228
+ aiConflictResolution: true,
229
+ aiReviewResolution: false,
230
+ readyLabel: "ready-to-merge"
231
+ }
232
+ },
204
233
  {
205
234
  id: "slicer",
206
235
  name: "Slicer",
@@ -234,7 +263,8 @@ var init_job_registry = __esm({
234
263
  defaultValue: "both"
235
264
  },
236
265
  { name: "skipLabel", type: "string", defaultValue: "skip-qa" },
237
- { name: "autoInstallPlaywright", type: "boolean", defaultValue: true }
266
+ { name: "autoInstallPlaywright", type: "boolean", defaultValue: true },
267
+ { name: "validatedLabel", type: "string", defaultValue: "e2e-validated" }
238
268
  ],
239
269
  defaultConfig: {
240
270
  enabled: true,
@@ -243,7 +273,8 @@ var init_job_registry = __esm({
243
273
  branchPatterns: [],
244
274
  artifacts: "both",
245
275
  skipLabel: "skip-qa",
246
- autoInstallPlaywright: true
276
+ autoInstallPlaywright: true,
277
+ validatedLabel: "e2e-validated"
247
278
  }
248
279
  },
249
280
  {
@@ -308,13 +339,14 @@ function resolveProviderBucketKey(provider, providerEnv) {
308
339
  return `claude-proxy:${baseUrl}`;
309
340
  }
310
341
  }
311
- var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, DEFAULT_PROVIDER_SCHEDULE_OVERRIDES, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, GLOBAL_NOTIFICATIONS_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY;
342
+ var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_SUMMARY_WINDOW_HOURS, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA_VALIDATED_LABEL, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, DEFAULT_PR_RESOLVER_ENABLED, DEFAULT_PR_RESOLVER_SCHEDULE, DEFAULT_PR_RESOLVER_MAX_RUNTIME, DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN, DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT, DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION, DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION, DEFAULT_PR_RESOLVER_READY_LABEL, DEFAULT_PR_RESOLVER, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, PR_RESOLVER_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, DEFAULT_PROVIDER_SCHEDULE_OVERRIDES, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, GLOBAL_NOTIFICATIONS_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY;
312
343
  var init_constants = __esm({
313
344
  "../core/dist/constants.js"() {
314
345
  "use strict";
315
346
  init_job_registry();
316
347
  DEFAULT_DEFAULT_BRANCH = "";
317
348
  DEFAULT_PRD_DIR = "docs/prds";
349
+ DEFAULT_SUMMARY_WINDOW_HOURS = 12;
318
350
  DEFAULT_MAX_RUNTIME = 7200;
319
351
  DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
320
352
  DEFAULT_CRON_SCHEDULE = "5 * * * *";
@@ -369,6 +401,7 @@ var init_constants = __esm({
369
401
  DEFAULT_QA_ARTIFACTS = "both";
370
402
  DEFAULT_QA_SKIP_LABEL = "skip-qa";
371
403
  DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT = true;
404
+ DEFAULT_QA_VALIDATED_LABEL = "e2e-validated";
372
405
  DEFAULT_QA = {
373
406
  enabled: DEFAULT_QA_ENABLED,
374
407
  schedule: DEFAULT_QA_SCHEDULE,
@@ -376,7 +409,8 @@ var init_constants = __esm({
376
409
  branchPatterns: [],
377
410
  artifacts: DEFAULT_QA_ARTIFACTS,
378
411
  skipLabel: DEFAULT_QA_SKIP_LABEL,
379
- autoInstallPlaywright: DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT
412
+ autoInstallPlaywright: DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT,
413
+ validatedLabel: DEFAULT_QA_VALIDATED_LABEL
380
414
  };
381
415
  QA_LOG_NAME = "night-watch-qa";
382
416
  DEFAULT_AUDIT_ENABLED = true;
@@ -405,9 +439,29 @@ If no issues are warranted, output an empty array: []`;
405
439
  targetColumn: DEFAULT_ANALYTICS_TARGET_COLUMN,
406
440
  analysisPrompt: DEFAULT_ANALYTICS_PROMPT
407
441
  };
442
+ DEFAULT_PR_RESOLVER_ENABLED = true;
443
+ DEFAULT_PR_RESOLVER_SCHEDULE = "15 6,14,22 * * *";
444
+ DEFAULT_PR_RESOLVER_MAX_RUNTIME = 3600;
445
+ DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN = 0;
446
+ DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT = 600;
447
+ DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION = true;
448
+ DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION = false;
449
+ DEFAULT_PR_RESOLVER_READY_LABEL = "ready-to-merge";
450
+ DEFAULT_PR_RESOLVER = {
451
+ enabled: DEFAULT_PR_RESOLVER_ENABLED,
452
+ schedule: DEFAULT_PR_RESOLVER_SCHEDULE,
453
+ maxRuntime: DEFAULT_PR_RESOLVER_MAX_RUNTIME,
454
+ branchPatterns: [],
455
+ maxPrsPerRun: DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN,
456
+ perPrTimeout: DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT,
457
+ aiConflictResolution: DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION,
458
+ aiReviewResolution: DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION,
459
+ readyLabel: DEFAULT_PR_RESOLVER_READY_LABEL
460
+ };
408
461
  AUDIT_LOG_NAME = "audit";
409
462
  PLANNER_LOG_NAME = "slicer";
410
463
  ANALYTICS_LOG_NAME = "analytics";
464
+ PR_RESOLVER_LOG_NAME = "pr-resolver";
411
465
  VALID_PROVIDERS = ["claude", "codex"];
412
466
  VALID_JOB_TYPES = getValidJobTypes();
413
467
  DEFAULT_JOB_PROVIDERS = {};
@@ -689,6 +743,13 @@ function normalizeConfig(rawConfig) {
689
743
  normalized[jobId] = normalizeJobConfig(rawJob, jobDef);
690
744
  }
691
745
  }
746
+ const prResolverDef = getJobDef("pr-resolver");
747
+ if (prResolverDef) {
748
+ const rawJob = readObject(rawConfig.prResolver);
749
+ if (rawJob) {
750
+ normalized.prResolver = normalizeJobConfig(rawJob, prResolverDef);
751
+ }
752
+ }
692
753
  const rawJobProviders = readObject(rawConfig.jobProviders);
693
754
  if (rawJobProviders) {
694
755
  const jobProviders = {};
@@ -996,6 +1057,17 @@ function buildEnvOverrideConfig(fileConfig) {
996
1057
  env[jobId] = overrides;
997
1058
  }
998
1059
  }
1060
+ const prResolverDef = getJobDef("pr-resolver");
1061
+ if (prResolverDef) {
1062
+ const currentBase = (
1063
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1064
+ env.prResolver ?? fileConfig?.prResolver ?? prResolverDef.defaultConfig
1065
+ );
1066
+ const overrides = buildJobEnvOverrides(prResolverDef.envPrefix, currentBase, prResolverDef.extraFields);
1067
+ if (overrides) {
1068
+ env.prResolver = overrides;
1069
+ }
1070
+ }
999
1071
  const jobProvidersEnv = {};
1000
1072
  for (const jobType of VALID_JOB_TYPES) {
1001
1073
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -1090,6 +1162,7 @@ function getDefaultConfig() {
1090
1162
  qa: { ...DEFAULT_QA },
1091
1163
  audit: { ...DEFAULT_AUDIT },
1092
1164
  analytics: { ...DEFAULT_ANALYTICS },
1165
+ prResolver: { ...DEFAULT_PR_RESOLVER },
1093
1166
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
1094
1167
  providerScheduleOverrides: [...DEFAULT_PROVIDER_SCHEDULE_OVERRIDES],
1095
1168
  queue: { ...DEFAULT_QUEUE }
@@ -1157,7 +1230,7 @@ function mergeConfigLayer(base, layer) {
1157
1230
  ...layerQueue,
1158
1231
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
1159
1232
  };
1160
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
1233
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "prResolver") {
1161
1234
  base[_key] = {
1162
1235
  ...base[_key],
1163
1236
  ...value
@@ -2936,6 +3009,11 @@ var init_labels = __esm({
2936
3009
  name: "analytics",
2937
3010
  description: "Created by the analytics job for Amplitude findings",
2938
3011
  color: "1d76db"
3012
+ },
3013
+ {
3014
+ name: "e2e-validated",
3015
+ description: "PR acceptance requirements validated by e2e/integration tests",
3016
+ color: "0e8a16"
2939
3017
  }
2940
3018
  ];
2941
3019
  }
@@ -3509,6 +3587,9 @@ function plannerLockPath(projectDir) {
3509
3587
  function analyticsLockPath(projectDir) {
3510
3588
  return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
3511
3589
  }
3590
+ function prResolverLockPath(projectDir) {
3591
+ return `${LOCK_FILE_PREFIX}pr-resolver-${projectRuntimeKey(projectDir)}.lock`;
3592
+ }
3512
3593
  function isProcessRunning(pid) {
3513
3594
  try {
3514
3595
  process.kill(pid, 0);
@@ -3809,7 +3890,7 @@ async function collectPrInfo(projectDir, branchPatterns) {
3809
3890
  } catch {
3810
3891
  return [];
3811
3892
  }
3812
- const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision", {
3893
+ const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision,labels", {
3813
3894
  cwd: projectDir,
3814
3895
  encoding: "utf-8"
3815
3896
  });
@@ -3833,7 +3914,8 @@ async function collectPrInfo(projectDir, branchPatterns) {
3833
3914
  branch: pr.headRefName,
3834
3915
  url: pr.url,
3835
3916
  ciStatus: deriveCiStatus(pr.statusCheckRollup),
3836
- reviewScore: deriveReviewScore(pr.reviewDecision)
3917
+ reviewScore: deriveReviewScore(pr.reviewDecision),
3918
+ labels: (pr.labels ?? []).map((l) => l.name)
3837
3919
  };
3838
3920
  });
3839
3921
  } catch {
@@ -4812,6 +4894,12 @@ function getEventEmoji(event) {
4812
4894
  return "\u{1F500}";
4813
4895
  case "qa_completed":
4814
4896
  return "\u{1F9EA}";
4897
+ case "pr_resolver_completed":
4898
+ return "\u{1F527}";
4899
+ case "pr_resolver_conflict_resolved":
4900
+ return "\u2705";
4901
+ case "pr_resolver_failed":
4902
+ return "\u274C";
4815
4903
  }
4816
4904
  }
4817
4905
  function getEventTitle(event) {
@@ -4836,6 +4924,12 @@ function getEventTitle(event) {
4836
4924
  return "PR Auto-Merged";
4837
4925
  case "qa_completed":
4838
4926
  return "QA Completed";
4927
+ case "pr_resolver_completed":
4928
+ return "PR Resolver Completed";
4929
+ case "pr_resolver_conflict_resolved":
4930
+ return "PR Conflict Resolved";
4931
+ case "pr_resolver_failed":
4932
+ return "PR Resolver Failed";
4839
4933
  }
4840
4934
  }
4841
4935
  function getEventColor(event) {
@@ -4860,6 +4954,12 @@ function getEventColor(event) {
4860
4954
  return 10181046;
4861
4955
  case "qa_completed":
4862
4956
  return 3066993;
4957
+ case "pr_resolver_completed":
4958
+ return 51283;
4959
+ case "pr_resolver_conflict_resolved":
4960
+ return 65280;
4961
+ case "pr_resolver_failed":
4962
+ return 16711680;
4863
4963
  }
4864
4964
  }
4865
4965
  function buildDescription(ctx) {
@@ -6178,6 +6278,15 @@ function isJobTypeEnabled(config, jobType) {
6178
6278
  return true;
6179
6279
  }
6180
6280
  }
6281
+ function getJobSchedule(config, jobType) {
6282
+ switch (jobType) {
6283
+ case "reviewer":
6284
+ return config.reviewerSchedule ?? "";
6285
+ case "executor":
6286
+ default:
6287
+ return config.cronSchedule ?? "";
6288
+ }
6289
+ }
6181
6290
  function loadPeerConfig(projectPath) {
6182
6291
  if (!fs17.existsSync(projectPath) || !fs17.existsSync(path16.join(projectPath, CONFIG_FILE_NAME))) {
6183
6292
  return null;
@@ -6191,11 +6300,15 @@ function loadPeerConfig(projectPath) {
6191
6300
  function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6192
6301
  const peers = /* @__PURE__ */ new Map();
6193
6302
  const currentPath = path16.resolve(currentProjectDir);
6303
+ const currentSchedule = getJobSchedule(currentConfig, jobType);
6194
6304
  const addPeer = (projectPath, config) => {
6195
6305
  const resolvedPath = path16.resolve(projectPath);
6196
6306
  if (!isJobTypeEnabled(config, jobType)) {
6197
6307
  return;
6198
6308
  }
6309
+ if (getJobSchedule(config, jobType) !== currentSchedule) {
6310
+ return;
6311
+ }
6199
6312
  peers.set(resolvedPath, {
6200
6313
  path: resolvedPath,
6201
6314
  config,
@@ -6532,6 +6645,8 @@ function getLockPathForJob(projectPath, jobType) {
6532
6645
  return plannerLockPath(projectPath);
6533
6646
  case "analytics":
6534
6647
  return analyticsLockPath(projectPath);
6648
+ case "pr-resolver":
6649
+ return prResolverLockPath(projectPath);
6535
6650
  }
6536
6651
  }
6537
6652
  function reconcileStaleRunningJobs(db) {
@@ -6621,6 +6736,15 @@ function getJobPriority(jobType, config) {
6621
6736
  function enqueueJob(projectPath, projectName, jobType, envVars, config, providerKey) {
6622
6737
  const db = openDb();
6623
6738
  try {
6739
+ const existing = db.prepare(`SELECT id FROM job_queue WHERE project_path = ? AND job_type = ? AND status IN ('pending', 'dispatched', 'running')`).get(projectPath, jobType);
6740
+ if (existing) {
6741
+ logger2.info("Skipping duplicate enqueue \u2014 active entry already exists", {
6742
+ id: existing.id,
6743
+ jobType,
6744
+ project: projectName
6745
+ });
6746
+ return existing.id;
6747
+ }
6624
6748
  const priority = getJobPriority(jobType, config);
6625
6749
  const now = Math.floor(Date.now() / 1e3);
6626
6750
  const envJson = JSON.stringify(envVars);
@@ -7086,6 +7210,88 @@ var init_job_queue = __esm({
7086
7210
  }
7087
7211
  });
7088
7212
 
7213
+ // ../core/dist/utils/summary.js
7214
+ function computeCounts(runs) {
7215
+ const counts = {
7216
+ total: runs.length,
7217
+ succeeded: 0,
7218
+ failed: 0,
7219
+ timedOut: 0,
7220
+ rateLimited: 0,
7221
+ skipped: 0
7222
+ };
7223
+ for (const run2 of runs) {
7224
+ switch (run2.status) {
7225
+ case "success":
7226
+ counts.succeeded++;
7227
+ break;
7228
+ case "failure":
7229
+ counts.failed++;
7230
+ break;
7231
+ case "timeout":
7232
+ counts.timedOut++;
7233
+ break;
7234
+ case "rate_limited":
7235
+ counts.rateLimited++;
7236
+ break;
7237
+ case "skipped":
7238
+ counts.skipped++;
7239
+ break;
7240
+ }
7241
+ }
7242
+ return counts;
7243
+ }
7244
+ function buildActionItems(counts, prs, pendingItems) {
7245
+ const items = [];
7246
+ if (counts.failed > 0) {
7247
+ items.push(`${counts.failed} failed job${counts.failed > 1 ? "s" : ""} \u2014 run \`night-watch logs\` to investigate`);
7248
+ }
7249
+ if (counts.timedOut > 0) {
7250
+ items.push(`${counts.timedOut} timed out job${counts.timedOut > 1 ? "s" : ""} \u2014 check logs for details`);
7251
+ }
7252
+ if (counts.rateLimited > 0) {
7253
+ items.push(`${counts.rateLimited} rate-limited job${counts.rateLimited > 1 ? "s" : ""} \u2014 consider adjusting schedule`);
7254
+ }
7255
+ const failingCiPrs = prs.filter((pr) => pr.ciStatus === "fail");
7256
+ for (const pr of failingCiPrs) {
7257
+ items.push(`PR #${pr.number} has failing CI \u2014 check ${pr.url}`);
7258
+ }
7259
+ const readyToMergePrs = prs.filter((pr) => pr.labels.includes("ready-to-merge"));
7260
+ if (readyToMergePrs.length > 0) {
7261
+ items.push(`${readyToMergePrs.length} PR${readyToMergePrs.length > 1 ? "s" : ""} marked ready-to-merge \u2014 review and merge`);
7262
+ }
7263
+ if (pendingItems.length > 0) {
7264
+ const jobTypes = [...new Set(pendingItems.map((item) => item.jobType))];
7265
+ items.push(`${pendingItems.length} job${pendingItems.length > 1 ? "s" : ""} pending in queue (${jobTypes.join(", ")})`);
7266
+ }
7267
+ return items;
7268
+ }
7269
+ async function getSummaryData(projectDir, windowHours = DEFAULT_SUMMARY_WINDOW_HOURS, branchPatterns = []) {
7270
+ const analytics = getJobRunsAnalytics(windowHours);
7271
+ const jobRuns = analytics.recentRuns;
7272
+ const counts = computeCounts(jobRuns);
7273
+ const openPrs = await collectPrInfo(projectDir, branchPatterns);
7274
+ const queueStatus = getQueueStatus();
7275
+ const pendingQueueItems = queueStatus.items.filter((item) => item.status === "pending");
7276
+ const actionItems = buildActionItems(counts, openPrs, pendingQueueItems);
7277
+ return {
7278
+ windowHours,
7279
+ jobRuns,
7280
+ counts,
7281
+ openPrs,
7282
+ pendingQueueItems,
7283
+ actionItems
7284
+ };
7285
+ }
7286
+ var init_summary = __esm({
7287
+ "../core/dist/utils/summary.js"() {
7288
+ "use strict";
7289
+ init_constants();
7290
+ init_status_data();
7291
+ init_job_queue();
7292
+ }
7293
+ });
7294
+
7089
7295
  // ../core/dist/analytics/amplitude-client.js
7090
7296
  function buildAuthHeader(apiKey, secretKey) {
7091
7297
  return `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString("base64")}`;
@@ -7516,6 +7722,15 @@ __export(dist_exports, {
7516
7722
  DEFAULT_PROVIDER: () => DEFAULT_PROVIDER,
7517
7723
  DEFAULT_PROVIDER_ENV: () => DEFAULT_PROVIDER_ENV,
7518
7724
  DEFAULT_PROVIDER_SCHEDULE_OVERRIDES: () => DEFAULT_PROVIDER_SCHEDULE_OVERRIDES,
7725
+ DEFAULT_PR_RESOLVER: () => DEFAULT_PR_RESOLVER,
7726
+ DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION: () => DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION,
7727
+ DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION: () => DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION,
7728
+ DEFAULT_PR_RESOLVER_ENABLED: () => DEFAULT_PR_RESOLVER_ENABLED,
7729
+ DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN: () => DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN,
7730
+ DEFAULT_PR_RESOLVER_MAX_RUNTIME: () => DEFAULT_PR_RESOLVER_MAX_RUNTIME,
7731
+ DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT: () => DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT,
7732
+ DEFAULT_PR_RESOLVER_READY_LABEL: () => DEFAULT_PR_RESOLVER_READY_LABEL,
7733
+ DEFAULT_PR_RESOLVER_SCHEDULE: () => DEFAULT_PR_RESOLVER_SCHEDULE,
7519
7734
  DEFAULT_QA: () => DEFAULT_QA,
7520
7735
  DEFAULT_QA_ARTIFACTS: () => DEFAULT_QA_ARTIFACTS,
7521
7736
  DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT: () => DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT,
@@ -7523,6 +7738,7 @@ __export(dist_exports, {
7523
7738
  DEFAULT_QA_MAX_RUNTIME: () => DEFAULT_QA_MAX_RUNTIME,
7524
7739
  DEFAULT_QA_SCHEDULE: () => DEFAULT_QA_SCHEDULE,
7525
7740
  DEFAULT_QA_SKIP_LABEL: () => DEFAULT_QA_SKIP_LABEL,
7741
+ DEFAULT_QA_VALIDATED_LABEL: () => DEFAULT_QA_VALIDATED_LABEL,
7526
7742
  DEFAULT_QUEUE: () => DEFAULT_QUEUE,
7527
7743
  DEFAULT_QUEUE_ENABLED: () => DEFAULT_QUEUE_ENABLED,
7528
7744
  DEFAULT_QUEUE_MAX_CONCURRENCY: () => DEFAULT_QUEUE_MAX_CONCURRENCY,
@@ -7540,6 +7756,7 @@ __export(dist_exports, {
7540
7756
  DEFAULT_SECONDARY_FALLBACK_MODEL: () => DEFAULT_SECONDARY_FALLBACK_MODEL,
7541
7757
  DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
7542
7758
  DEFAULT_SLICER_SCHEDULE: () => DEFAULT_SLICER_SCHEDULE,
7759
+ DEFAULT_SUMMARY_WINDOW_HOURS: () => DEFAULT_SUMMARY_WINDOW_HOURS,
7543
7760
  DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
7544
7761
  EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
7545
7762
  EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
@@ -7562,6 +7779,7 @@ __export(dist_exports, {
7562
7779
  PRIORITY_LABELS: () => PRIORITY_LABELS,
7563
7780
  PRIORITY_LABEL_INFO: () => PRIORITY_LABEL_INFO,
7564
7781
  PROVIDER_COMMANDS: () => PROVIDER_COMMANDS,
7782
+ PR_RESOLVER_LOG_NAME: () => PR_RESOLVER_LOG_NAME,
7565
7783
  QA_LOG_NAME: () => QA_LOG_NAME,
7566
7784
  REGISTRY_FILE_NAME: () => REGISTRY_FILE_NAME,
7567
7785
  REVIEWER_LOG_FILE: () => REVIEWER_LOG_FILE,
@@ -7690,6 +7908,7 @@ __export(dist_exports, {
7690
7908
  getScriptPath: () => getScriptPath,
7691
7909
  getStateFilePath: () => getStateFilePath,
7692
7910
  getStateItem: () => getStateItem,
7911
+ getSummaryData: () => getSummaryData,
7693
7912
  getUncheckedItems: () => getUncheckedItems,
7694
7913
  getValidJobTypes: () => getValidJobTypes,
7695
7914
  groupBySection: () => groupBySection2,
@@ -7726,6 +7945,7 @@ __export(dist_exports, {
7726
7945
  parseTimeToMinutes: () => parseTimeToMinutes,
7727
7946
  performCancel: () => performCancel,
7728
7947
  plannerLockPath: () => plannerLockPath,
7948
+ prResolverLockPath: () => prResolverLockPath,
7729
7949
  prepareBranchWorktree: () => prepareBranchWorktree,
7730
7950
  prepareDetachedWorktree: () => prepareDetachedWorktree,
7731
7951
  projectRuntimeKey: () => projectRuntimeKey,
@@ -7822,6 +8042,7 @@ var init_dist = __esm({
7822
8042
  init_webhook_validator();
7823
8043
  init_worktree_manager();
7824
8044
  init_job_queue();
8045
+ init_summary();
7825
8046
  init_analytics();
7826
8047
  init_prd_template();
7827
8048
  init_slicer_prompt();
@@ -8098,6 +8319,7 @@ function buildInitConfig(params) {
8098
8319
  },
8099
8320
  audit: { ...defaults.audit },
8100
8321
  analytics: { ...defaults.analytics },
8322
+ prResolver: { ...defaults.prResolver },
8101
8323
  jobProviders: { ...defaults.jobProviders },
8102
8324
  queue: {
8103
8325
  ...defaults.queue,
@@ -8221,7 +8443,7 @@ function initCommand(program2) {
8221
8443
  const cwd = process.cwd();
8222
8444
  const force = options.force || false;
8223
8445
  const prdDir = options.prdDir || DEFAULT_PRD_DIR;
8224
- const totalSteps = 13;
8446
+ const totalSteps = 14;
8225
8447
  const interactive = isInteractiveInitSession();
8226
8448
  console.log();
8227
8449
  header("Night Watch CLI - Initializing");
@@ -8476,7 +8698,35 @@ function initCommand(program2) {
8476
8698
  }
8477
8699
  }
8478
8700
  }
8479
- step(11, totalSteps, "Registering project in global registry...");
8701
+ step(11, totalSteps, "Syncing Night Watch labels to GitHub...");
8702
+ let labelSyncStatus = "Skipped";
8703
+ if (!remoteStatus.hasGitHubRemote || !ghAuthenticated) {
8704
+ labelSyncStatus = !remoteStatus.hasGitHubRemote ? "Skipped (no GitHub remote)" : "Skipped (gh auth required)";
8705
+ info("Skipping label sync (no GitHub remote or gh not authenticated).");
8706
+ } else {
8707
+ try {
8708
+ const { NIGHT_WATCH_LABELS: NIGHT_WATCH_LABELS2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
8709
+ let created = 0;
8710
+ for (const label2 of NIGHT_WATCH_LABELS2) {
8711
+ try {
8712
+ execSync3(
8713
+ `gh label create "${label2.name}" --description "${label2.description}" --color "${label2.color}" --force`,
8714
+ { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
8715
+ );
8716
+ created++;
8717
+ } catch {
8718
+ }
8719
+ }
8720
+ labelSyncStatus = `Synced ${created}/${NIGHT_WATCH_LABELS2.length} labels`;
8721
+ success(`Synced ${created}/${NIGHT_WATCH_LABELS2.length} labels to GitHub`);
8722
+ } catch (labelErr) {
8723
+ labelSyncStatus = "Failed";
8724
+ warn(
8725
+ `Could not sync labels: ${labelErr instanceof Error ? labelErr.message : String(labelErr)}`
8726
+ );
8727
+ }
8728
+ }
8729
+ step(12, totalSteps, "Registering project in global registry...");
8480
8730
  try {
8481
8731
  const { registerProject: registerProject2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
8482
8732
  const entry = registerProject2(cwd);
@@ -8486,7 +8736,7 @@ function initCommand(program2) {
8486
8736
  ` Warning: Could not register in global registry: ${regErr instanceof Error ? regErr.message : String(regErr)}`
8487
8737
  );
8488
8738
  }
8489
- step(12, totalSteps, "Installing Night Watch skills...");
8739
+ step(13, totalSteps, "Installing Night Watch skills...");
8490
8740
  const skillsResult = installSkills(cwd, selectedProvider, force, TEMPLATES_DIR);
8491
8741
  if (skillsResult.installed > 0) {
8492
8742
  success(`Installed ${skillsResult.installed} skills to ${skillsResult.location}`);
@@ -8498,7 +8748,7 @@ function initCommand(program2) {
8498
8748
  } else if (skillsResult.type === "none") {
8499
8749
  info("No compatible AI skills directory detected \u2014 skipping.");
8500
8750
  }
8501
- step(13, totalSteps, "Initialization complete!");
8751
+ step(14, totalSteps, "Initialization complete!");
8502
8752
  header("Initialization Complete");
8503
8753
  const filesTable = createTable({ head: ["Created Files", ""] });
8504
8754
  filesTable.push(["PRD Directory", `${prdDir}/done/`]);
@@ -8511,6 +8761,7 @@ function initCommand(program2) {
8511
8761
  filesTable.push(["", `instructions/prd-creator.md (${templateSources[5].source})`]);
8512
8762
  filesTable.push(["Config File", CONFIG_FILE_NAME]);
8513
8763
  filesTable.push(["Board Setup", boardSetupStatus]);
8764
+ filesTable.push(["Label Sync", labelSyncStatus]);
8514
8765
  filesTable.push(["Global Registry", "~/.night-watch/projects.json"]);
8515
8766
  let skillsSummary;
8516
8767
  if (skillsResult.installed > 0) {
@@ -9453,6 +9704,7 @@ function buildEnvVars3(config, options) {
9453
9704
  const branchPatterns = config.qa.branchPatterns.length > 0 ? config.qa.branchPatterns : config.branchPatterns;
9454
9705
  env.NW_BRANCH_PATTERNS = branchPatterns.join(",");
9455
9706
  env.NW_QA_SKIP_LABEL = config.qa.skipLabel;
9707
+ env.NW_QA_VALIDATED_LABEL = config.qa.validatedLabel;
9456
9708
  env.NW_QA_ARTIFACTS = config.qa.artifacts;
9457
9709
  env.NW_QA_AUTO_INSTALL_PLAYWRIGHT = config.qa.autoInstallPlaywright ? "1" : "0";
9458
9710
  env.NW_CLAUDE_MODEL_ID = CLAUDE_MODEL_IDS[config.primaryFallbackModel ?? config.claudeModel ?? "sonnet"];
@@ -9502,6 +9754,7 @@ function qaCommand(program2) {
9502
9754
  const branchPatterns = config.qa.branchPatterns.length > 0 ? config.qa.branchPatterns : config.branchPatterns;
9503
9755
  configTable.push(["Branch Patterns", branchPatterns.join(", ")]);
9504
9756
  configTable.push(["Skip Label", config.qa.skipLabel]);
9757
+ configTable.push(["Validated Label", config.qa.validatedLabel]);
9505
9758
  configTable.push(["Artifacts", config.qa.artifacts]);
9506
9759
  configTable.push([
9507
9760
  "Auto-install Playwright",
@@ -9866,6 +10119,14 @@ function performInstall(projectDir, config, options) {
9866
10119
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9867
10120
  entries.push(analyticsEntry);
9868
10121
  }
10122
+ const disablePrResolver = options?.noPrResolver === true || options?.prResolver === false;
10123
+ const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10124
+ if (installPrResolver) {
10125
+ const prResolverSchedule = config.prResolver.schedule;
10126
+ const prResolverLog = path25.join(logDir, "pr-resolver.log");
10127
+ const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10128
+ entries.push(prResolverEntry);
10129
+ }
9869
10130
  const existingEntries = new Set(
9870
10131
  Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
9871
10132
  );
@@ -9884,7 +10145,7 @@ function performInstall(projectDir, config, options) {
9884
10145
  }
9885
10146
  }
9886
10147
  function installCommand(program2) {
9887
- program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("--no-analytics", "Skip installing analytics cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
10148
+ program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("--no-analytics", "Skip installing analytics cron").option("--no-pr-resolver", "Skip installing PR resolver cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
9888
10149
  try {
9889
10150
  const projectDir = process.cwd();
9890
10151
  const config = loadConfig(projectDir);
@@ -9966,6 +10227,15 @@ function installCommand(program2) {
9966
10227
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9967
10228
  entries.push(analyticsEntry);
9968
10229
  }
10230
+ const disablePrResolver = options.noPrResolver === true || options.prResolver === false;
10231
+ const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10232
+ let prResolverLog;
10233
+ if (installPrResolver) {
10234
+ prResolverLog = path25.join(logDir, "pr-resolver.log");
10235
+ const prResolverSchedule = config.prResolver.schedule;
10236
+ const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10237
+ entries.push(prResolverEntry);
10238
+ }
9969
10239
  const existingEntrySet = new Set(existingEntries);
9970
10240
  const currentCrontab = readCrontab();
9971
10241
  const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
@@ -9995,6 +10265,9 @@ function installCommand(program2) {
9995
10265
  if (installAnalytics && analyticsLog) {
9996
10266
  dim(` Analytics: ${analyticsLog}`);
9997
10267
  }
10268
+ if (installPrResolver && prResolverLog) {
10269
+ dim(` PR Resolver: ${prResolverLog}`);
10270
+ }
9998
10271
  console.log();
9999
10272
  dim("To uninstall, run: night-watch uninstall");
10000
10273
  dim("To check status, run: night-watch status");
@@ -10027,7 +10300,13 @@ function performUninstall(projectDir, options) {
10027
10300
  if (!options?.keepLogs) {
10028
10301
  const logDir = path26.join(projectDir, "logs");
10029
10302
  if (fs24.existsSync(logDir)) {
10030
- const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
10303
+ const logFiles = [
10304
+ "executor.log",
10305
+ "reviewer.log",
10306
+ "slicer.log",
10307
+ "audit.log",
10308
+ "pr-resolver.log"
10309
+ ];
10031
10310
  logFiles.forEach((logFile) => {
10032
10311
  const logPath = path26.join(logDir, logFile);
10033
10312
  if (fs24.existsSync(logPath)) {
@@ -10072,7 +10351,13 @@ function uninstallCommand(program2) {
10072
10351
  if (!options.keepLogs) {
10073
10352
  const logDir = path26.join(projectDir, "logs");
10074
10353
  if (fs24.existsSync(logDir)) {
10075
- const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
10354
+ const logFiles = [
10355
+ "executor.log",
10356
+ "reviewer.log",
10357
+ "slicer.log",
10358
+ "audit.log",
10359
+ "pr-resolver.log"
10360
+ ];
10076
10361
  let logsRemoved = 0;
10077
10362
  logFiles.forEach((logFile) => {
10078
10363
  const logPath = path26.join(logDir, logFile);
@@ -16742,8 +17027,9 @@ function createQueueCommand() {
16742
17027
  }
16743
17028
  process.exit(0);
16744
17029
  });
16745
- queue.command("dispatch").description("Dispatch the next pending job (used by cron scripts)").option("--log <file>", "Log file to write dispatch output").action((_opts) => {
16746
- const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
17030
+ queue.command("dispatch").description("Dispatch the next pending job (used by cron scripts)").option("--log <file>", "Log file to write dispatch output").option("--project-dir <dir>", "Project directory to load queue config from (defaults to cwd)").action((_opts) => {
17031
+ const configDir = _opts.projectDir ?? process.cwd();
17032
+ const entry = dispatchNextJob(loadConfig(configDir).queue);
16747
17033
  if (!entry) {
16748
17034
  logger5.info("No pending jobs to dispatch");
16749
17035
  return;
@@ -16821,8 +17107,9 @@ function createQueueCommand() {
16821
17107
  }
16822
17108
  removeJob(queueId);
16823
17109
  });
16824
- queue.command("can-start").description("Return a zero exit status when the global queue has an available slot").action(() => {
16825
- const queueConfig = loadConfig(process.cwd()).queue;
17110
+ queue.command("can-start").description("Return a zero exit status when the global queue has an available slot").option("--project-dir <dir>", "Project directory to load queue config from (defaults to cwd)").action((opts) => {
17111
+ const configDir = opts.projectDir ?? process.cwd();
17112
+ const queueConfig = loadConfig(configDir).queue;
16826
17113
  process.exit(canStartJob(queueConfig) ? 0 : 1);
16827
17114
  });
16828
17115
  queue.command("expire").description("Expire stale queued jobs").option(
@@ -16844,7 +17131,26 @@ function createQueueCommand() {
16844
17131
  });
16845
17132
  return queue;
16846
17133
  }
16847
- var QUEUE_MARKER_KEYS = /* @__PURE__ */ new Set(["NW_DRY_RUN", "NW_CRON_TRIGGER", "NW_DEFAULT_BRANCH"]);
17134
+ var QUEUE_MARKER_KEYS = /* @__PURE__ */ new Set([
17135
+ "NW_DRY_RUN",
17136
+ "NW_CRON_TRIGGER",
17137
+ "NW_DEFAULT_BRANCH",
17138
+ "NW_TARGET_PR",
17139
+ "NW_REVIEWER_WORKER_MODE",
17140
+ "NW_REVIEWER_PARALLEL",
17141
+ "NW_REVIEWER_WORKER_STAGGER",
17142
+ "NW_REVIEWER_MAX_RUNTIME",
17143
+ "NW_REVIEWER_MAX_RETRIES",
17144
+ "NW_REVIEWER_RETRY_DELAY",
17145
+ "NW_REVIEWER_MAX_PRS_PER_RUN",
17146
+ "NW_MIN_REVIEW_SCORE",
17147
+ "NW_BRANCH_PATTERNS",
17148
+ "NW_PRD_DIR",
17149
+ "NW_AUTO_MERGE",
17150
+ "NW_AUTO_MERGE_METHOD",
17151
+ "NW_MAX_RUNTIME",
17152
+ "NW_QA_MAX_RUNTIME"
17153
+ ]);
16848
17154
  function filterQueueMarkers(envJson) {
16849
17155
  const result = {};
16850
17156
  for (const [key, value] of Object.entries(envJson)) {
@@ -16913,6 +17219,300 @@ function notifyCommand(program2) {
16913
17219
  );
16914
17220
  }
16915
17221
 
17222
+ // src/commands/summary.ts
17223
+ init_dist();
17224
+ import path42 from "path";
17225
+ import chalk8 from "chalk";
17226
+ function formatDuration2(seconds) {
17227
+ if (seconds === null) return "-";
17228
+ const mins = Math.floor(seconds / 60);
17229
+ const secs = seconds % 60;
17230
+ if (mins === 0) return `${secs}s`;
17231
+ return `${mins}m ${secs}s`;
17232
+ }
17233
+ function formatCiStatus2(status) {
17234
+ if (status === "pass") return chalk8.green("pass");
17235
+ if (status === "fail") return chalk8.red("fail");
17236
+ if (status === "pending") return chalk8.yellow("pending");
17237
+ return chalk8.dim("unknown");
17238
+ }
17239
+ function formatReviewScore2(score) {
17240
+ if (score === null) return chalk8.dim("-");
17241
+ if (score >= 80) return chalk8.green(String(score));
17242
+ if (score >= 60) return chalk8.yellow(String(score));
17243
+ return chalk8.red(String(score));
17244
+ }
17245
+ function formatJobStatus(status) {
17246
+ if (status === "success") return chalk8.green("success");
17247
+ if (status === "failure") return chalk8.red("failure");
17248
+ if (status === "timeout") return chalk8.yellow("timeout");
17249
+ if (status === "rate_limited") return chalk8.magenta("rate_limited");
17250
+ if (status === "skipped") return chalk8.dim("skipped");
17251
+ return chalk8.dim(status);
17252
+ }
17253
+ function getProjectName2(projectPath) {
17254
+ return path42.basename(projectPath) || projectPath;
17255
+ }
17256
+ function formatProvider(providerKey) {
17257
+ return providerKey.split(":")[0] || providerKey;
17258
+ }
17259
+ function summaryCommand(program2) {
17260
+ program2.command("summary").description("Show a summary of recent Night Watch activity").option(
17261
+ "--hours <n>",
17262
+ "Time window in hours (default: 12)",
17263
+ String(DEFAULT_SUMMARY_WINDOW_HOURS)
17264
+ ).option("--json", "Output summary as JSON").action(async (options) => {
17265
+ try {
17266
+ const projectDir = process.cwd();
17267
+ const config = loadConfig(projectDir);
17268
+ const hours = parseInt(options.hours || String(DEFAULT_SUMMARY_WINDOW_HOURS), 10);
17269
+ if (isNaN(hours) || hours <= 0) {
17270
+ console.error("Error: --hours must be a positive integer");
17271
+ process.exit(1);
17272
+ }
17273
+ const data = await getSummaryData(projectDir, hours, config.branchPatterns);
17274
+ if (options.json) {
17275
+ console.log(JSON.stringify(data, null, 2));
17276
+ return;
17277
+ }
17278
+ console.log();
17279
+ console.log(chalk8.bold.cyan(`Night Watch Summary (last ${data.windowHours}h)`));
17280
+ console.log(chalk8.dim("\u2500".repeat(40)));
17281
+ console.log();
17282
+ if (data.jobRuns.length === 0) {
17283
+ info("No recent activity in this time window.");
17284
+ console.log();
17285
+ } else {
17286
+ const countParts = [];
17287
+ if (data.counts.succeeded > 0) {
17288
+ countParts.push(chalk8.green(`\u2713 ${data.counts.succeeded} succeeded`));
17289
+ }
17290
+ if (data.counts.failed > 0) {
17291
+ countParts.push(chalk8.red(`\u2717 ${data.counts.failed} failed`));
17292
+ }
17293
+ if (data.counts.timedOut > 0) {
17294
+ countParts.push(chalk8.yellow(`\u23F1 ${data.counts.timedOut} timed out`));
17295
+ }
17296
+ if (data.counts.rateLimited > 0) {
17297
+ countParts.push(chalk8.magenta(`\u23F3 ${data.counts.rateLimited} rate limited`));
17298
+ }
17299
+ if (data.counts.skipped > 0) {
17300
+ countParts.push(chalk8.dim(`${data.counts.skipped} skipped`));
17301
+ }
17302
+ console.log(`Jobs Executed: ${data.counts.total}`);
17303
+ if (countParts.length > 0) {
17304
+ console.log(` ${countParts.join(" ")}`);
17305
+ }
17306
+ console.log();
17307
+ const table = createTable({
17308
+ head: ["Job", "Status", "Project", "Provider", "Duration"],
17309
+ colWidths: [12, 12, 20, 12, 12]
17310
+ });
17311
+ for (const run2 of data.jobRuns.slice(0, 10)) {
17312
+ table.push([
17313
+ run2.jobType,
17314
+ formatJobStatus(run2.status),
17315
+ getProjectName2(run2.projectPath),
17316
+ formatProvider(run2.providerKey),
17317
+ formatDuration2(run2.durationSeconds)
17318
+ ]);
17319
+ }
17320
+ console.log(table.toString());
17321
+ if (data.jobRuns.length > 10) {
17322
+ dim(` ... and ${data.jobRuns.length - 10} more`);
17323
+ }
17324
+ console.log();
17325
+ }
17326
+ if (data.openPrs.length > 0) {
17327
+ header(`Open PRs (${data.openPrs.length})`);
17328
+ const prTable = createTable({
17329
+ head: ["#", "Title", "CI", "Score"],
17330
+ colWidths: [6, 40, 10, 8]
17331
+ });
17332
+ for (const pr of data.openPrs) {
17333
+ const title = pr.title.length > 37 ? pr.title.substring(0, 34) + "..." : pr.title;
17334
+ prTable.push([
17335
+ String(pr.number),
17336
+ title,
17337
+ formatCiStatus2(pr.ciStatus),
17338
+ formatReviewScore2(pr.reviewScore)
17339
+ ]);
17340
+ }
17341
+ console.log(prTable.toString());
17342
+ console.log();
17343
+ }
17344
+ if (data.pendingQueueItems.length > 0) {
17345
+ const jobTypes = [...new Set(data.pendingQueueItems.map((item) => item.jobType))];
17346
+ const projectNames = [...new Set(data.pendingQueueItems.map((item) => item.projectName))];
17347
+ dim(
17348
+ `Queue: ${data.pendingQueueItems.length} pending (${jobTypes.join(", ")}) for ${projectNames.join(", ")}`
17349
+ );
17350
+ console.log();
17351
+ }
17352
+ if (data.actionItems.length > 0) {
17353
+ console.log(chalk8.yellow("\u26A0 Action needed:"));
17354
+ for (const item of data.actionItems) {
17355
+ console.log(` \u2022 ${item}`);
17356
+ }
17357
+ } else {
17358
+ console.log(chalk8.green("\u2713 No action needed \u2014 all jobs healthy."));
17359
+ }
17360
+ console.log();
17361
+ } catch (error2) {
17362
+ console.error(
17363
+ `Error getting summary: ${error2 instanceof Error ? error2.message : String(error2)}`
17364
+ );
17365
+ process.exit(1);
17366
+ }
17367
+ });
17368
+ }
17369
+
17370
+ // src/commands/resolve.ts
17371
+ init_dist();
17372
+ import { execFileSync as execFileSync7 } from "child_process";
17373
+ import * as path43 from "path";
17374
+ function buildEnvVars6(config, options) {
17375
+ const env = buildBaseEnvVars(config, "pr-resolver", options.dryRun);
17376
+ env.NW_PR_RESOLVER_MAX_RUNTIME = String(config.prResolver.maxRuntime);
17377
+ env.NW_PR_RESOLVER_MAX_PRS_PER_RUN = String(config.prResolver.maxPrsPerRun);
17378
+ env.NW_PR_RESOLVER_PER_PR_TIMEOUT = String(config.prResolver.perPrTimeout);
17379
+ env.NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION = config.prResolver.aiConflictResolution ? "1" : "0";
17380
+ env.NW_PR_RESOLVER_AI_REVIEW_RESOLUTION = config.prResolver.aiReviewResolution ? "1" : "0";
17381
+ env.NW_PR_RESOLVER_READY_LABEL = config.prResolver.readyLabel;
17382
+ env.NW_PR_RESOLVER_BRANCH_PATTERNS = config.prResolver.branchPatterns.join(",");
17383
+ return env;
17384
+ }
17385
+ function applyCliOverrides5(config, options) {
17386
+ const overridden = { ...config, prResolver: { ...config.prResolver } };
17387
+ if (options.timeout) {
17388
+ const timeout = parseInt(options.timeout, 10);
17389
+ if (!isNaN(timeout)) {
17390
+ overridden.prResolver.maxRuntime = timeout;
17391
+ }
17392
+ }
17393
+ if (options.provider) {
17394
+ overridden._cliProviderOverride = options.provider;
17395
+ }
17396
+ return overridden;
17397
+ }
17398
+ function getOpenPrs() {
17399
+ try {
17400
+ const args = ["pr", "list", "--state", "open", "--json", "number,title,headRefName,mergeable"];
17401
+ const result = execFileSync7("gh", args, {
17402
+ encoding: "utf-8",
17403
+ stdio: ["pipe", "pipe", "pipe"]
17404
+ });
17405
+ const prs = JSON.parse(result.trim() || "[]");
17406
+ return prs.map(
17407
+ (pr) => ({
17408
+ number: pr.number,
17409
+ title: pr.title,
17410
+ branch: pr.headRefName,
17411
+ mergeable: pr.mergeable
17412
+ })
17413
+ );
17414
+ } catch {
17415
+ return [];
17416
+ }
17417
+ }
17418
+ function resolveCommand(program2) {
17419
+ program2.command("resolve").description("Run PR conflict resolver now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime").option("--provider <string>", "AI provider to use").action(async (options) => {
17420
+ const projectDir = process.cwd();
17421
+ let config = loadConfig(projectDir);
17422
+ config = applyCliOverrides5(config, options);
17423
+ if (!config.prResolver.enabled && !options.dryRun) {
17424
+ info("PR resolver is disabled in config; skipping.");
17425
+ process.exit(0);
17426
+ }
17427
+ const envVars = buildEnvVars6(config, options);
17428
+ const scriptPath = getScriptPath("night-watch-pr-resolver-cron.sh");
17429
+ if (options.dryRun) {
17430
+ header("Dry Run: PR Resolver");
17431
+ const resolverProvider = resolveJobProvider(config, "pr-resolver");
17432
+ header("Configuration");
17433
+ const configTable = createTable({ head: ["Setting", "Value"] });
17434
+ configTable.push(["Provider", resolverProvider]);
17435
+ configTable.push([
17436
+ "Max Runtime",
17437
+ `${config.prResolver.maxRuntime}s (${Math.floor(config.prResolver.maxRuntime / 60)}min)`
17438
+ ]);
17439
+ configTable.push([
17440
+ "Max PRs Per Run",
17441
+ config.prResolver.maxPrsPerRun === 0 ? "Unlimited" : String(config.prResolver.maxPrsPerRun)
17442
+ ]);
17443
+ configTable.push(["Per-PR Timeout", `${config.prResolver.perPrTimeout}s`]);
17444
+ configTable.push([
17445
+ "AI Conflict Resolution",
17446
+ config.prResolver.aiConflictResolution ? "Enabled" : "Disabled"
17447
+ ]);
17448
+ configTable.push([
17449
+ "AI Review Resolution",
17450
+ config.prResolver.aiReviewResolution ? "Enabled" : "Disabled"
17451
+ ]);
17452
+ configTable.push(["Ready Label", config.prResolver.readyLabel]);
17453
+ configTable.push([
17454
+ "Branch Patterns",
17455
+ config.prResolver.branchPatterns.length > 0 ? config.prResolver.branchPatterns.join(", ") : "(all)"
17456
+ ]);
17457
+ console.log(configTable.toString());
17458
+ header("Open PRs");
17459
+ const openPrs = getOpenPrs();
17460
+ if (openPrs.length === 0) {
17461
+ dim(" (no open PRs found)");
17462
+ } else {
17463
+ for (const pr of openPrs) {
17464
+ const conflictStatus = pr.mergeable === "CONFLICTING" ? " [CONFLICT]" : "";
17465
+ info(`#${pr.number}: ${pr.title}${conflictStatus}`);
17466
+ dim(` Branch: ${pr.branch}`);
17467
+ }
17468
+ }
17469
+ header("Environment Variables");
17470
+ for (const [key, value] of Object.entries(envVars)) {
17471
+ dim(` ${key}=${value}`);
17472
+ }
17473
+ header("Command");
17474
+ dim(` bash ${scriptPath} ${projectDir}`);
17475
+ console.log();
17476
+ process.exit(0);
17477
+ }
17478
+ const spinner = createSpinner("Running PR resolver...");
17479
+ spinner.start();
17480
+ try {
17481
+ await maybeApplyCronSchedulingDelay(config, "pr-resolver", projectDir);
17482
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(
17483
+ scriptPath,
17484
+ [projectDir],
17485
+ envVars
17486
+ );
17487
+ const scriptResult = parseScriptResult(`${stdout}
17488
+ ${stderr}`);
17489
+ if (exitCode === 0) {
17490
+ if (scriptResult?.status === "queued") {
17491
+ spinner.succeed("PR resolver queued \u2014 another job is currently running");
17492
+ } else if (scriptResult?.status?.startsWith("skip_")) {
17493
+ spinner.succeed("PR resolver completed (no PRs needed resolution)");
17494
+ } else {
17495
+ spinner.succeed("PR resolver completed successfully");
17496
+ }
17497
+ } else {
17498
+ spinner.fail(`PR resolver exited with code ${exitCode}`);
17499
+ }
17500
+ const notificationEvent = exitCode === 0 ? "pr_resolver_completed" : "pr_resolver_failed";
17501
+ await sendNotifications(config, {
17502
+ event: notificationEvent,
17503
+ projectName: path43.basename(projectDir),
17504
+ exitCode,
17505
+ provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
17506
+ });
17507
+ process.exit(exitCode);
17508
+ } catch (err) {
17509
+ spinner.fail("Failed to execute resolve command");
17510
+ error(`${err instanceof Error ? err.message : String(err)}`);
17511
+ process.exit(1);
17512
+ }
17513
+ });
17514
+ }
17515
+
16916
17516
  // src/cli.ts
16917
17517
  var __filename5 = fileURLToPath6(import.meta.url);
16918
17518
  var __dirname5 = dirname12(__filename5);
@@ -16954,4 +17554,6 @@ program.addCommand(createStateCommand());
16954
17554
  boardCommand(program);
16955
17555
  queueCommand(program);
16956
17556
  notifyCommand(program);
17557
+ summaryCommand(program);
17558
+ resolveCommand(program);
16957
17559
  program.parse();