@jonit-dev/night-watch-cli 1.8.8-beta.1 → 1.8.8-beta.11

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 (52) hide show
  1. package/dist/cli.js +981 -78
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +39 -6
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.d.ts +8 -0
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +50 -0
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/merge.d.ts +26 -0
  11. package/dist/commands/merge.d.ts.map +1 -0
  12. package/dist/commands/merge.js +159 -0
  13. package/dist/commands/merge.js.map +1 -0
  14. package/dist/commands/qa.d.ts.map +1 -1
  15. package/dist/commands/qa.js +2 -0
  16. package/dist/commands/qa.js.map +1 -1
  17. package/dist/commands/queue.d.ts.map +1 -1
  18. package/dist/commands/queue.js +27 -4
  19. package/dist/commands/queue.js.map +1 -1
  20. package/dist/commands/resolve.d.ts +26 -0
  21. package/dist/commands/resolve.d.ts.map +1 -0
  22. package/dist/commands/resolve.js +186 -0
  23. package/dist/commands/resolve.js.map +1 -0
  24. package/dist/commands/review.d.ts +5 -1
  25. package/dist/commands/review.d.ts.map +1 -1
  26. package/dist/commands/review.js +18 -18
  27. package/dist/commands/review.js.map +1 -1
  28. package/dist/commands/slice.d.ts +1 -0
  29. package/dist/commands/slice.d.ts.map +1 -1
  30. package/dist/commands/slice.js +19 -19
  31. package/dist/commands/slice.js.map +1 -1
  32. package/dist/commands/summary.d.ts +14 -0
  33. package/dist/commands/summary.d.ts.map +1 -0
  34. package/dist/commands/summary.js +193 -0
  35. package/dist/commands/summary.js.map +1 -0
  36. package/dist/commands/uninstall.d.ts.map +1 -1
  37. package/dist/commands/uninstall.js +14 -2
  38. package/dist/commands/uninstall.js.map +1 -1
  39. package/dist/scripts/night-watch-helpers.sh +10 -1
  40. package/dist/scripts/night-watch-merger-cron.sh +321 -0
  41. package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
  42. package/dist/scripts/night-watch-pr-reviewer-cron.sh +23 -142
  43. package/dist/scripts/night-watch-qa-cron.sh +30 -4
  44. package/dist/scripts/test-helpers.bats +45 -0
  45. package/dist/templates/night-watch-pr-reviewer.md +2 -1
  46. package/dist/templates/pr-reviewer.md +2 -1
  47. package/dist/templates/slicer.md +54 -64
  48. package/dist/web/assets/index-CPQbZ1BL.css +1 -0
  49. package/dist/web/assets/index-CiRJZI4z.js +386 -0
  50. package/dist/web/assets/index-ZE5lOeJp.js +386 -0
  51. package/dist/web/index.html +2 -2
  52. package/package.json +1 -1
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
  {
@@ -288,6 +319,38 @@ var init_job_registry = __esm({
288
319
  targetColumn: "Draft",
289
320
  analysisPrompt: ""
290
321
  }
322
+ },
323
+ {
324
+ id: "merger",
325
+ name: "Merge Orchestrator",
326
+ description: "Repo-wide PR merge coordinator \u2014 scans, rebases, and merges in FIFO order",
327
+ cliCommand: "merge",
328
+ logName: "merger",
329
+ lockSuffix: "-merger.lock",
330
+ queuePriority: 45,
331
+ envPrefix: "NW_MERGER",
332
+ extraFields: [
333
+ {
334
+ name: "mergeMethod",
335
+ type: "enum",
336
+ enumValues: ["squash", "merge", "rebase"],
337
+ defaultValue: "squash"
338
+ },
339
+ { name: "minReviewScore", type: "number", defaultValue: 80 },
340
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
341
+ { name: "rebaseBeforeMerge", type: "boolean", defaultValue: true },
342
+ { name: "maxPrsPerRun", type: "number", defaultValue: 0 }
343
+ ],
344
+ defaultConfig: {
345
+ enabled: false,
346
+ schedule: "55 */4 * * *",
347
+ maxRuntime: 1800,
348
+ mergeMethod: "squash",
349
+ minReviewScore: 80,
350
+ branchPatterns: [],
351
+ rebaseBeforeMerge: true,
352
+ maxPrsPerRun: 0
353
+ }
291
354
  }
292
355
  ];
293
356
  JOB_MAP = new Map(JOB_REGISTRY.map((job) => [job.id, job]));
@@ -308,13 +371,14 @@ function resolveProviderBucketKey(provider, providerEnv) {
308
371
  return `claude-proxy:${baseUrl}`;
309
372
  }
310
373
  }
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;
374
+ 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, DEFAULT_MERGER_ENABLED, DEFAULT_MERGER_SCHEDULE, DEFAULT_MERGER_MAX_RUNTIME, DEFAULT_MERGER_MERGE_METHOD, DEFAULT_MERGER_MIN_REVIEW_SCORE, DEFAULT_MERGER_REBASE_BEFORE_MERGE, DEFAULT_MERGER_MAX_PRS_PER_RUN, DEFAULT_MERGER, MERGER_LOG_NAME, 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
375
  var init_constants = __esm({
313
376
  "../core/dist/constants.js"() {
314
377
  "use strict";
315
378
  init_job_registry();
316
379
  DEFAULT_DEFAULT_BRANCH = "";
317
380
  DEFAULT_PRD_DIR = "docs/prds";
381
+ DEFAULT_SUMMARY_WINDOW_HOURS = 12;
318
382
  DEFAULT_MAX_RUNTIME = 7200;
319
383
  DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
320
384
  DEFAULT_CRON_SCHEDULE = "5 * * * *";
@@ -352,7 +416,7 @@ var init_constants = __esm({
352
416
  slicerSchedule: DEFAULT_SLICER_SCHEDULE,
353
417
  slicerMaxRuntime: DEFAULT_SLICER_MAX_RUNTIME,
354
418
  priorityMode: "roadmap-first",
355
- issueColumn: "Draft"
419
+ issueColumn: "Ready"
356
420
  };
357
421
  DEFAULT_TEMPLATES_DIR = ".night-watch/templates";
358
422
  DEFAULT_BOARD_PROVIDER = {
@@ -369,6 +433,7 @@ var init_constants = __esm({
369
433
  DEFAULT_QA_ARTIFACTS = "both";
370
434
  DEFAULT_QA_SKIP_LABEL = "skip-qa";
371
435
  DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT = true;
436
+ DEFAULT_QA_VALIDATED_LABEL = "e2e-validated";
372
437
  DEFAULT_QA = {
373
438
  enabled: DEFAULT_QA_ENABLED,
374
439
  schedule: DEFAULT_QA_SCHEDULE,
@@ -376,7 +441,8 @@ var init_constants = __esm({
376
441
  branchPatterns: [],
377
442
  artifacts: DEFAULT_QA_ARTIFACTS,
378
443
  skipLabel: DEFAULT_QA_SKIP_LABEL,
379
- autoInstallPlaywright: DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT
444
+ autoInstallPlaywright: DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT,
445
+ validatedLabel: DEFAULT_QA_VALIDATED_LABEL
380
446
  };
381
447
  QA_LOG_NAME = "night-watch-qa";
382
448
  DEFAULT_AUDIT_ENABLED = true;
@@ -405,9 +471,47 @@ If no issues are warranted, output an empty array: []`;
405
471
  targetColumn: DEFAULT_ANALYTICS_TARGET_COLUMN,
406
472
  analysisPrompt: DEFAULT_ANALYTICS_PROMPT
407
473
  };
474
+ DEFAULT_PR_RESOLVER_ENABLED = true;
475
+ DEFAULT_PR_RESOLVER_SCHEDULE = "15 6,14,22 * * *";
476
+ DEFAULT_PR_RESOLVER_MAX_RUNTIME = 3600;
477
+ DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN = 0;
478
+ DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT = 600;
479
+ DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION = true;
480
+ DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION = false;
481
+ DEFAULT_PR_RESOLVER_READY_LABEL = "ready-to-merge";
482
+ DEFAULT_PR_RESOLVER = {
483
+ enabled: DEFAULT_PR_RESOLVER_ENABLED,
484
+ schedule: DEFAULT_PR_RESOLVER_SCHEDULE,
485
+ maxRuntime: DEFAULT_PR_RESOLVER_MAX_RUNTIME,
486
+ branchPatterns: [],
487
+ maxPrsPerRun: DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN,
488
+ perPrTimeout: DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT,
489
+ aiConflictResolution: DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION,
490
+ aiReviewResolution: DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION,
491
+ readyLabel: DEFAULT_PR_RESOLVER_READY_LABEL
492
+ };
493
+ DEFAULT_MERGER_ENABLED = false;
494
+ DEFAULT_MERGER_SCHEDULE = "55 */4 * * *";
495
+ DEFAULT_MERGER_MAX_RUNTIME = 1800;
496
+ DEFAULT_MERGER_MERGE_METHOD = "squash";
497
+ DEFAULT_MERGER_MIN_REVIEW_SCORE = 80;
498
+ DEFAULT_MERGER_REBASE_BEFORE_MERGE = true;
499
+ DEFAULT_MERGER_MAX_PRS_PER_RUN = 0;
500
+ DEFAULT_MERGER = {
501
+ enabled: DEFAULT_MERGER_ENABLED,
502
+ schedule: DEFAULT_MERGER_SCHEDULE,
503
+ maxRuntime: DEFAULT_MERGER_MAX_RUNTIME,
504
+ mergeMethod: DEFAULT_MERGER_MERGE_METHOD,
505
+ minReviewScore: DEFAULT_MERGER_MIN_REVIEW_SCORE,
506
+ branchPatterns: [],
507
+ rebaseBeforeMerge: DEFAULT_MERGER_REBASE_BEFORE_MERGE,
508
+ maxPrsPerRun: DEFAULT_MERGER_MAX_PRS_PER_RUN
509
+ };
510
+ MERGER_LOG_NAME = "merger";
408
511
  AUDIT_LOG_NAME = "audit";
409
512
  PLANNER_LOG_NAME = "slicer";
410
513
  ANALYTICS_LOG_NAME = "analytics";
514
+ PR_RESOLVER_LOG_NAME = "pr-resolver";
411
515
  VALID_PROVIDERS = ["claude", "codex"];
412
516
  VALID_JOB_TYPES = getValidJobTypes();
413
517
  DEFAULT_JOB_PROVIDERS = {};
@@ -680,7 +784,7 @@ function normalizeConfig(rawConfig) {
680
784
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
681
785
  normalized.autoMergeMethod = mergeMethod;
682
786
  }
683
- for (const jobId of ["qa", "audit", "analytics"]) {
787
+ for (const jobId of ["qa", "audit", "analytics", "merger"]) {
684
788
  const jobDef = getJobDef(jobId);
685
789
  if (!jobDef)
686
790
  continue;
@@ -689,6 +793,13 @@ function normalizeConfig(rawConfig) {
689
793
  normalized[jobId] = normalizeJobConfig(rawJob, jobDef);
690
794
  }
691
795
  }
796
+ const prResolverDef = getJobDef("pr-resolver");
797
+ if (prResolverDef) {
798
+ const rawJob = readObject(rawConfig.prResolver);
799
+ if (rawJob) {
800
+ normalized.prResolver = normalizeJobConfig(rawJob, prResolverDef);
801
+ }
802
+ }
692
803
  const rawJobProviders = readObject(rawConfig.jobProviders);
693
804
  if (rawJobProviders) {
694
805
  const jobProviders = {};
@@ -996,6 +1107,28 @@ function buildEnvOverrideConfig(fileConfig) {
996
1107
  env[jobId] = overrides;
997
1108
  }
998
1109
  }
1110
+ const prResolverDef = getJobDef("pr-resolver");
1111
+ if (prResolverDef) {
1112
+ const currentBase = (
1113
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1114
+ env.prResolver ?? fileConfig?.prResolver ?? prResolverDef.defaultConfig
1115
+ );
1116
+ const overrides = buildJobEnvOverrides(prResolverDef.envPrefix, currentBase, prResolverDef.extraFields);
1117
+ if (overrides) {
1118
+ env.prResolver = overrides;
1119
+ }
1120
+ }
1121
+ const mergerDef = getJobDef("merger");
1122
+ if (mergerDef) {
1123
+ const currentBase = (
1124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1125
+ env.merger ?? fileConfig?.merger ?? mergerDef.defaultConfig
1126
+ );
1127
+ const overrides = buildJobEnvOverrides(mergerDef.envPrefix, currentBase, mergerDef.extraFields);
1128
+ if (overrides) {
1129
+ env.merger = overrides;
1130
+ }
1131
+ }
999
1132
  const jobProvidersEnv = {};
1000
1133
  for (const jobType of VALID_JOB_TYPES) {
1001
1134
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -1090,6 +1223,8 @@ function getDefaultConfig() {
1090
1223
  qa: { ...DEFAULT_QA },
1091
1224
  audit: { ...DEFAULT_AUDIT },
1092
1225
  analytics: { ...DEFAULT_ANALYTICS },
1226
+ prResolver: { ...DEFAULT_PR_RESOLVER },
1227
+ merger: { ...DEFAULT_MERGER },
1093
1228
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
1094
1229
  providerScheduleOverrides: [...DEFAULT_PROVIDER_SCHEDULE_OVERRIDES],
1095
1230
  queue: { ...DEFAULT_QUEUE }
@@ -1157,7 +1292,7 @@ function mergeConfigLayer(base, layer) {
1157
1292
  ...layerQueue,
1158
1293
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
1159
1294
  };
1160
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
1295
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "prResolver" || _key === "merger") {
1161
1296
  base[_key] = {
1162
1297
  ...base[_key],
1163
1298
  ...value
@@ -1180,6 +1315,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
1180
1315
  if (fileConfig)
1181
1316
  mergeConfigLayer(merged, fileConfig);
1182
1317
  mergeConfigLayer(merged, envConfig);
1318
+ if (merged.autoMerge === true && !fileConfig?.merger) {
1319
+ merged.merger = {
1320
+ ...merged.merger,
1321
+ enabled: true,
1322
+ mergeMethod: merged.autoMergeMethod ?? "squash"
1323
+ };
1324
+ }
1183
1325
  merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
1184
1326
  merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
1185
1327
  merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
@@ -2936,6 +3078,11 @@ var init_labels = __esm({
2936
3078
  name: "analytics",
2937
3079
  description: "Created by the analytics job for Amplitude findings",
2938
3080
  color: "1d76db"
3081
+ },
3082
+ {
3083
+ name: "e2e-validated",
3084
+ description: "PR acceptance requirements validated by e2e/integration tests",
3085
+ color: "0e8a16"
2939
3086
  }
2940
3087
  ];
2941
3088
  }
@@ -3509,6 +3656,12 @@ function plannerLockPath(projectDir) {
3509
3656
  function analyticsLockPath(projectDir) {
3510
3657
  return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
3511
3658
  }
3659
+ function prResolverLockPath(projectDir) {
3660
+ return `${LOCK_FILE_PREFIX}pr-resolver-${projectRuntimeKey(projectDir)}.lock`;
3661
+ }
3662
+ function mergerLockPath(projectDir) {
3663
+ return `${LOCK_FILE_PREFIX}merger-${projectRuntimeKey(projectDir)}.lock`;
3664
+ }
3512
3665
  function isProcessRunning(pid) {
3513
3666
  try {
3514
3667
  process.kill(pid, 0);
@@ -3809,7 +3962,7 @@ async function collectPrInfo(projectDir, branchPatterns) {
3809
3962
  } catch {
3810
3963
  return [];
3811
3964
  }
3812
- const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision", {
3965
+ const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision,labels", {
3813
3966
  cwd: projectDir,
3814
3967
  encoding: "utf-8"
3815
3968
  });
@@ -3833,7 +3986,8 @@ async function collectPrInfo(projectDir, branchPatterns) {
3833
3986
  branch: pr.headRefName,
3834
3987
  url: pr.url,
3835
3988
  ciStatus: deriveCiStatus(pr.statusCheckRollup),
3836
- reviewScore: deriveReviewScore(pr.reviewDecision)
3989
+ reviewScore: deriveReviewScore(pr.reviewDecision),
3990
+ labels: (pr.labels ?? []).map((l) => l.name)
3837
3991
  };
3838
3992
  });
3839
3993
  } catch {
@@ -4812,6 +4966,16 @@ function getEventEmoji(event) {
4812
4966
  return "\u{1F500}";
4813
4967
  case "qa_completed":
4814
4968
  return "\u{1F9EA}";
4969
+ case "pr_resolver_completed":
4970
+ return "\u{1F527}";
4971
+ case "pr_resolver_conflict_resolved":
4972
+ return "\u2705";
4973
+ case "pr_resolver_failed":
4974
+ return "\u274C";
4975
+ case "merge_completed":
4976
+ return "\u{1F500}";
4977
+ case "merge_failed":
4978
+ return "\u274C";
4815
4979
  }
4816
4980
  }
4817
4981
  function getEventTitle(event) {
@@ -4836,6 +5000,16 @@ function getEventTitle(event) {
4836
5000
  return "PR Auto-Merged";
4837
5001
  case "qa_completed":
4838
5002
  return "QA Completed";
5003
+ case "pr_resolver_completed":
5004
+ return "PR Resolver Completed";
5005
+ case "pr_resolver_conflict_resolved":
5006
+ return "PR Conflict Resolved";
5007
+ case "pr_resolver_failed":
5008
+ return "PR Resolver Failed";
5009
+ case "merge_completed":
5010
+ return "PR Merged";
5011
+ case "merge_failed":
5012
+ return "Merge Failed";
4839
5013
  }
4840
5014
  }
4841
5015
  function getEventColor(event) {
@@ -4860,6 +5034,16 @@ function getEventColor(event) {
4860
5034
  return 10181046;
4861
5035
  case "qa_completed":
4862
5036
  return 3066993;
5037
+ case "pr_resolver_completed":
5038
+ return 51283;
5039
+ case "pr_resolver_conflict_resolved":
5040
+ return 65280;
5041
+ case "pr_resolver_failed":
5042
+ return 16711680;
5043
+ case "merge_completed":
5044
+ return 10181046;
5045
+ case "merge_failed":
5046
+ return 16711680;
4863
5047
  }
4864
5048
  }
4865
5049
  function buildDescription(ctx) {
@@ -5600,9 +5784,9 @@ var init_slicer_prompt = __esm({
5600
5784
  "use strict";
5601
5785
  __filename = fileURLToPath2(import.meta.url);
5602
5786
  __dirname = path14.dirname(__filename);
5603
- DEFAULT_SLICER_TEMPLATE = `You are a **PRD Creator Agent**. Your job: analyze the codebase and write a complete Product Requirements Document (PRD) for a feature.
5787
+ DEFAULT_SLICER_TEMPLATE = `You are a **Principal Software Architect**. Your job: analyze the codebase and write a complete Product Requirements Document (PRD) for a feature. The PRD will be used directly as a GitHub issue body, so it must be self-contained and immediately actionable by an engineer.
5604
5788
 
5605
- When this activates: \`PRD Creator: Initializing\`
5789
+ When this activates: \`Planning Mode: Principal Architect\`
5606
5790
 
5607
5791
  ---
5608
5792
 
@@ -5623,22 +5807,16 @@ The PRD directory is: \`{{PRD_DIR}}\`
5623
5807
 
5624
5808
  ## Your Task
5625
5809
 
5626
- 0. **Load Planner Skill** - Read and apply \`.claude/skills/prd-creator/SKILL.md\` before writing the PRD. If unavailable, continue with this template.
5627
-
5628
- 1. **Explore the Codebase** - Read relevant existing files to understand the project structure, patterns, and conventions.
5629
-
5630
- 2. **Assess Complexity** - Score the complexity using the rubric and determine whether this is LOW, MEDIUM, or HIGH complexity.
5631
-
5632
- 3. **Write a Complete PRD** - Create a full PRD following the prd-creator template structure with Context, Solution, Phases, Tests, and Acceptance Criteria.
5633
-
5634
- 4. **Write the PRD File** - Use the Write tool to create the PRD file at the exact path specified in \`{{OUTPUT_FILE_PATH}}\`.
5810
+ 1. **Explore the Codebase** \u2014 Read relevant existing files to understand structure, patterns, and conventions.
5811
+ 2. **Assess Complexity** \u2014 Score using the rubric below and determine LOW / MEDIUM / HIGH.
5812
+ 3. **Write a Complete PRD** \u2014 Follow the exact template structure below. Every section must be filled with concrete information.
5813
+ 4. **Write the PRD File** \u2014 Use the Write tool to create the PRD file at \`{{OUTPUT_FILE_PATH}}\`.
5635
5814
 
5636
5815
  ---
5637
5816
 
5638
5817
  ## Complexity Scoring
5639
5818
 
5640
5819
  \`\`\`
5641
- COMPLEXITY SCORE (sum all that apply):
5642
5820
  +1 Touches 1-5 files
5643
5821
  +2 Touches 6-10 files
5644
5822
  +3 Touches 10+ files
@@ -5650,7 +5828,7 @@ COMPLEXITY SCORE (sum all that apply):
5650
5828
 
5651
5829
  | Score | Level | Template Mode |
5652
5830
  | ----- | ------ | ----------------------------------------------- |
5653
- | 1-3 | LOW | Minimal (skip sections marked with MEDIUM/HIGH) |
5831
+ | 1-3 | LOW | Minimal (skip sections marked MEDIUM/HIGH) |
5654
5832
  | 4-6 | MEDIUM | Standard (all sections) |
5655
5833
  | 7+ | HIGH | Full + mandatory checkpoints every phase |
5656
5834
  \`\`\`
@@ -5659,25 +5837,77 @@ COMPLEXITY SCORE (sum all that apply):
5659
5837
 
5660
5838
  ## PRD Template Structure
5661
5839
 
5662
- Your PRD MUST follow this exact structure with these sections:
5663
- 1. **Context** - Problem, files analyzed, current behavior, integration points
5664
- 2. **Solution** - Approach, architecture diagram, key decisions, data changes
5665
- 3. **Sequence Flow** (MEDIUM/HIGH) - Mermaid sequence diagram
5666
- 4. **Execution Phases** - Concrete phases with files, implementation steps, and tests
5667
- 5. **Acceptance Criteria** - Checklist of completion requirements
5840
+ Your PRD MUST use this structure. Replace every [bracketed placeholder] with real content.
5841
+
5842
+ # PRD: [Title]
5843
+
5844
+ **Complexity: [SCORE] \u2192 [LEVEL] mode**
5845
+
5846
+ ## 1. Context
5847
+
5848
+ **Problem:** [1-2 sentences]
5849
+
5850
+ **Files Analyzed:**
5851
+ - \`path/to/file.ts\` \u2014 [what you found]
5852
+
5853
+ **Current Behavior:**
5854
+ - [3-5 bullets]
5855
+
5856
+ ### Integration Points
5857
+ - Entry point: [cron / CLI / event / route]
5858
+ - Caller file: [file invoking new code]
5859
+ - User flow: User does X \u2192 triggers Y \u2192 result Z
5860
+
5861
+ ## 2. Solution
5862
+
5863
+ **Approach:**
5864
+ - [3-5 bullets]
5865
+
5866
+ **Key Decisions:** [library choices, error handling, reused utilities]
5867
+
5868
+ **Data Changes:** [schema changes, or "None"]
5869
+
5870
+ ## 3. Sequence Flow (MEDIUM/HIGH only)
5871
+
5872
+ [mermaid sequenceDiagram]
5873
+
5874
+ ## 4. Execution Phases
5875
+
5876
+ ### Phase N: [Name] \u2014 [User-visible outcome]
5877
+
5878
+ **Files (max 5):**
5879
+ - \`src/path/file.ts\` \u2014 [what changes]
5880
+
5881
+ **Implementation:**
5882
+ - [ ] Step 1
5883
+
5884
+ **Tests Required:**
5885
+ | Test File | Test Name | Assertion |
5886
+ |-----------|-----------|-----------|
5887
+ | \`src/__tests__/feature.test.ts\` | \`should X when Y\` | \`expect(r).toBe(Z)\` |
5888
+
5889
+ **Checkpoint:** Run \`yarn verify\` and related tests after this phase.
5890
+
5891
+ ## 5. Acceptance Criteria
5892
+
5893
+ - [ ] All phases complete
5894
+ - [ ] All tests pass
5895
+ - [ ] \`yarn verify\` passes
5896
+ - [ ] Feature is reachable (not orphaned code)
5668
5897
 
5669
5898
  ---
5670
5899
 
5671
5900
  ## Critical Instructions
5672
5901
 
5673
- 1. **Read all relevant existing files BEFORE writing any code**
5674
- 2. **Follow existing patterns in the codebase**
5675
- 3. **Write the PRD with concrete file paths and implementation details**
5676
- 4. **Include specific test names and assertions**
5677
- 5. **Use the Write tool to create the PRD file at \`{{OUTPUT_FILE_PATH}}\`**
5678
- 6. **The PRD must be complete and actionable - no TODO placeholders**
5902
+ 1. Read all relevant files BEFORE writing the PRD
5903
+ 2. Follow existing patterns \u2014 use \`@/*\` path aliases, match naming conventions
5904
+ 3. Include concrete file paths and implementation steps
5905
+ 4. Include specific test names and assertions
5906
+ 5. Use the Write tool to create the file at \`{{OUTPUT_FILE_PATH}}\`
5907
+ 6. No placeholder text in the final PRD
5908
+ 7. The PRD is the GitHub issue body \u2014 make it self-contained
5679
5909
 
5680
- DO NOT leave placeholder text like "[Name]" or "[description]" in the final PRD.
5910
+ DO NOT leave [bracketed placeholder] text in the output.
5681
5911
  DO NOT skip any sections.
5682
5912
  DO NOT forget to write the file.
5683
5913
  `;
@@ -6178,6 +6408,15 @@ function isJobTypeEnabled(config, jobType) {
6178
6408
  return true;
6179
6409
  }
6180
6410
  }
6411
+ function getJobSchedule(config, jobType) {
6412
+ switch (jobType) {
6413
+ case "reviewer":
6414
+ return config.reviewerSchedule ?? "";
6415
+ case "executor":
6416
+ default:
6417
+ return config.cronSchedule ?? "";
6418
+ }
6419
+ }
6181
6420
  function loadPeerConfig(projectPath) {
6182
6421
  if (!fs17.existsSync(projectPath) || !fs17.existsSync(path16.join(projectPath, CONFIG_FILE_NAME))) {
6183
6422
  return null;
@@ -6191,11 +6430,15 @@ function loadPeerConfig(projectPath) {
6191
6430
  function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6192
6431
  const peers = /* @__PURE__ */ new Map();
6193
6432
  const currentPath = path16.resolve(currentProjectDir);
6433
+ const currentSchedule = getJobSchedule(currentConfig, jobType);
6194
6434
  const addPeer = (projectPath, config) => {
6195
6435
  const resolvedPath = path16.resolve(projectPath);
6196
6436
  if (!isJobTypeEnabled(config, jobType)) {
6197
6437
  return;
6198
6438
  }
6439
+ if (getJobSchedule(config, jobType) !== currentSchedule) {
6440
+ return;
6441
+ }
6199
6442
  peers.set(resolvedPath, {
6200
6443
  path: resolvedPath,
6201
6444
  config,
@@ -6532,6 +6775,10 @@ function getLockPathForJob(projectPath, jobType) {
6532
6775
  return plannerLockPath(projectPath);
6533
6776
  case "analytics":
6534
6777
  return analyticsLockPath(projectPath);
6778
+ case "pr-resolver":
6779
+ return prResolverLockPath(projectPath);
6780
+ case "merger":
6781
+ return mergerLockPath(projectPath);
6535
6782
  }
6536
6783
  }
6537
6784
  function reconcileStaleRunningJobs(db) {
@@ -6621,6 +6868,15 @@ function getJobPriority(jobType, config) {
6621
6868
  function enqueueJob(projectPath, projectName, jobType, envVars, config, providerKey) {
6622
6869
  const db = openDb();
6623
6870
  try {
6871
+ const existing = db.prepare(`SELECT id FROM job_queue WHERE project_path = ? AND job_type = ? AND status IN ('pending', 'dispatched', 'running')`).get(projectPath, jobType);
6872
+ if (existing) {
6873
+ logger2.info("Skipping duplicate enqueue \u2014 active entry already exists", {
6874
+ id: existing.id,
6875
+ jobType,
6876
+ project: projectName
6877
+ });
6878
+ return existing.id;
6879
+ }
6624
6880
  const priority = getJobPriority(jobType, config);
6625
6881
  const now = Math.floor(Date.now() / 1e3);
6626
6882
  const envJson = JSON.stringify(envVars);
@@ -7086,6 +7342,88 @@ var init_job_queue = __esm({
7086
7342
  }
7087
7343
  });
7088
7344
 
7345
+ // ../core/dist/utils/summary.js
7346
+ function computeCounts(runs) {
7347
+ const counts = {
7348
+ total: runs.length,
7349
+ succeeded: 0,
7350
+ failed: 0,
7351
+ timedOut: 0,
7352
+ rateLimited: 0,
7353
+ skipped: 0
7354
+ };
7355
+ for (const run2 of runs) {
7356
+ switch (run2.status) {
7357
+ case "success":
7358
+ counts.succeeded++;
7359
+ break;
7360
+ case "failure":
7361
+ counts.failed++;
7362
+ break;
7363
+ case "timeout":
7364
+ counts.timedOut++;
7365
+ break;
7366
+ case "rate_limited":
7367
+ counts.rateLimited++;
7368
+ break;
7369
+ case "skipped":
7370
+ counts.skipped++;
7371
+ break;
7372
+ }
7373
+ }
7374
+ return counts;
7375
+ }
7376
+ function buildActionItems(counts, prs, pendingItems) {
7377
+ const items = [];
7378
+ if (counts.failed > 0) {
7379
+ items.push(`${counts.failed} failed job${counts.failed > 1 ? "s" : ""} \u2014 run \`night-watch logs\` to investigate`);
7380
+ }
7381
+ if (counts.timedOut > 0) {
7382
+ items.push(`${counts.timedOut} timed out job${counts.timedOut > 1 ? "s" : ""} \u2014 check logs for details`);
7383
+ }
7384
+ if (counts.rateLimited > 0) {
7385
+ items.push(`${counts.rateLimited} rate-limited job${counts.rateLimited > 1 ? "s" : ""} \u2014 consider adjusting schedule`);
7386
+ }
7387
+ const failingCiPrs = prs.filter((pr) => pr.ciStatus === "fail");
7388
+ for (const pr of failingCiPrs) {
7389
+ items.push(`PR #${pr.number} has failing CI \u2014 check ${pr.url}`);
7390
+ }
7391
+ const readyToMergePrs = prs.filter((pr) => pr.labels.includes("ready-to-merge"));
7392
+ if (readyToMergePrs.length > 0) {
7393
+ items.push(`${readyToMergePrs.length} PR${readyToMergePrs.length > 1 ? "s" : ""} marked ready-to-merge \u2014 review and merge`);
7394
+ }
7395
+ if (pendingItems.length > 0) {
7396
+ const jobTypes = [...new Set(pendingItems.map((item) => item.jobType))];
7397
+ items.push(`${pendingItems.length} job${pendingItems.length > 1 ? "s" : ""} pending in queue (${jobTypes.join(", ")})`);
7398
+ }
7399
+ return items;
7400
+ }
7401
+ async function getSummaryData(projectDir, windowHours = DEFAULT_SUMMARY_WINDOW_HOURS, branchPatterns = []) {
7402
+ const analytics = getJobRunsAnalytics(windowHours);
7403
+ const jobRuns = analytics.recentRuns;
7404
+ const counts = computeCounts(jobRuns);
7405
+ const openPrs = await collectPrInfo(projectDir, branchPatterns);
7406
+ const queueStatus = getQueueStatus();
7407
+ const pendingQueueItems = queueStatus.items.filter((item) => item.status === "pending");
7408
+ const actionItems = buildActionItems(counts, openPrs, pendingQueueItems);
7409
+ return {
7410
+ windowHours,
7411
+ jobRuns,
7412
+ counts,
7413
+ openPrs,
7414
+ pendingQueueItems,
7415
+ actionItems
7416
+ };
7417
+ }
7418
+ var init_summary = __esm({
7419
+ "../core/dist/utils/summary.js"() {
7420
+ "use strict";
7421
+ init_constants();
7422
+ init_status_data();
7423
+ init_job_queue();
7424
+ }
7425
+ });
7426
+
7089
7427
  // ../core/dist/analytics/amplitude-client.js
7090
7428
  function buildAuthHeader(apiKey, secretKey) {
7091
7429
  return `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString("base64")}`;
@@ -7508,6 +7846,14 @@ __export(dist_exports, {
7508
7846
  DEFAULT_MAX_LOG_SIZE: () => DEFAULT_MAX_LOG_SIZE,
7509
7847
  DEFAULT_MAX_RETRIES: () => DEFAULT_MAX_RETRIES,
7510
7848
  DEFAULT_MAX_RUNTIME: () => DEFAULT_MAX_RUNTIME,
7849
+ DEFAULT_MERGER: () => DEFAULT_MERGER,
7850
+ DEFAULT_MERGER_ENABLED: () => DEFAULT_MERGER_ENABLED,
7851
+ DEFAULT_MERGER_MAX_PRS_PER_RUN: () => DEFAULT_MERGER_MAX_PRS_PER_RUN,
7852
+ DEFAULT_MERGER_MAX_RUNTIME: () => DEFAULT_MERGER_MAX_RUNTIME,
7853
+ DEFAULT_MERGER_MERGE_METHOD: () => DEFAULT_MERGER_MERGE_METHOD,
7854
+ DEFAULT_MERGER_MIN_REVIEW_SCORE: () => DEFAULT_MERGER_MIN_REVIEW_SCORE,
7855
+ DEFAULT_MERGER_REBASE_BEFORE_MERGE: () => DEFAULT_MERGER_REBASE_BEFORE_MERGE,
7856
+ DEFAULT_MERGER_SCHEDULE: () => DEFAULT_MERGER_SCHEDULE,
7511
7857
  DEFAULT_MIN_REVIEW_SCORE: () => DEFAULT_MIN_REVIEW_SCORE,
7512
7858
  DEFAULT_NOTIFICATIONS: () => DEFAULT_NOTIFICATIONS,
7513
7859
  DEFAULT_PRD_DIR: () => DEFAULT_PRD_DIR,
@@ -7516,6 +7862,15 @@ __export(dist_exports, {
7516
7862
  DEFAULT_PROVIDER: () => DEFAULT_PROVIDER,
7517
7863
  DEFAULT_PROVIDER_ENV: () => DEFAULT_PROVIDER_ENV,
7518
7864
  DEFAULT_PROVIDER_SCHEDULE_OVERRIDES: () => DEFAULT_PROVIDER_SCHEDULE_OVERRIDES,
7865
+ DEFAULT_PR_RESOLVER: () => DEFAULT_PR_RESOLVER,
7866
+ DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION: () => DEFAULT_PR_RESOLVER_AI_CONFLICT_RESOLUTION,
7867
+ DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION: () => DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION,
7868
+ DEFAULT_PR_RESOLVER_ENABLED: () => DEFAULT_PR_RESOLVER_ENABLED,
7869
+ DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN: () => DEFAULT_PR_RESOLVER_MAX_PRS_PER_RUN,
7870
+ DEFAULT_PR_RESOLVER_MAX_RUNTIME: () => DEFAULT_PR_RESOLVER_MAX_RUNTIME,
7871
+ DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT: () => DEFAULT_PR_RESOLVER_PER_PR_TIMEOUT,
7872
+ DEFAULT_PR_RESOLVER_READY_LABEL: () => DEFAULT_PR_RESOLVER_READY_LABEL,
7873
+ DEFAULT_PR_RESOLVER_SCHEDULE: () => DEFAULT_PR_RESOLVER_SCHEDULE,
7519
7874
  DEFAULT_QA: () => DEFAULT_QA,
7520
7875
  DEFAULT_QA_ARTIFACTS: () => DEFAULT_QA_ARTIFACTS,
7521
7876
  DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT: () => DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT,
@@ -7523,6 +7878,7 @@ __export(dist_exports, {
7523
7878
  DEFAULT_QA_MAX_RUNTIME: () => DEFAULT_QA_MAX_RUNTIME,
7524
7879
  DEFAULT_QA_SCHEDULE: () => DEFAULT_QA_SCHEDULE,
7525
7880
  DEFAULT_QA_SKIP_LABEL: () => DEFAULT_QA_SKIP_LABEL,
7881
+ DEFAULT_QA_VALIDATED_LABEL: () => DEFAULT_QA_VALIDATED_LABEL,
7526
7882
  DEFAULT_QUEUE: () => DEFAULT_QUEUE,
7527
7883
  DEFAULT_QUEUE_ENABLED: () => DEFAULT_QUEUE_ENABLED,
7528
7884
  DEFAULT_QUEUE_MAX_CONCURRENCY: () => DEFAULT_QUEUE_MAX_CONCURRENCY,
@@ -7540,6 +7896,7 @@ __export(dist_exports, {
7540
7896
  DEFAULT_SECONDARY_FALLBACK_MODEL: () => DEFAULT_SECONDARY_FALLBACK_MODEL,
7541
7897
  DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
7542
7898
  DEFAULT_SLICER_SCHEDULE: () => DEFAULT_SLICER_SCHEDULE,
7899
+ DEFAULT_SUMMARY_WINDOW_HOURS: () => DEFAULT_SUMMARY_WINDOW_HOURS,
7543
7900
  DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
7544
7901
  EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
7545
7902
  EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
@@ -7555,6 +7912,7 @@ __export(dist_exports, {
7555
7912
  LocalKanbanProvider: () => LocalKanbanProvider,
7556
7913
  Logger: () => Logger,
7557
7914
  MAX_HISTORY_RECORDS_PER_PRD: () => MAX_HISTORY_RECORDS_PER_PRD,
7915
+ MERGER_LOG_NAME: () => MERGER_LOG_NAME,
7558
7916
  NIGHT_WATCH_LABELS: () => NIGHT_WATCH_LABELS,
7559
7917
  PLANNER_LOG_NAME: () => PLANNER_LOG_NAME,
7560
7918
  PRD_STATES_FILE_NAME: () => PRD_STATES_FILE_NAME,
@@ -7562,6 +7920,7 @@ __export(dist_exports, {
7562
7920
  PRIORITY_LABELS: () => PRIORITY_LABELS,
7563
7921
  PRIORITY_LABEL_INFO: () => PRIORITY_LABEL_INFO,
7564
7922
  PROVIDER_COMMANDS: () => PROVIDER_COMMANDS,
7923
+ PR_RESOLVER_LOG_NAME: () => PR_RESOLVER_LOG_NAME,
7565
7924
  QA_LOG_NAME: () => QA_LOG_NAME,
7566
7925
  REGISTRY_FILE_NAME: () => REGISTRY_FILE_NAME,
7567
7926
  REVIEWER_LOG_FILE: () => REVIEWER_LOG_FILE,
@@ -7690,6 +8049,7 @@ __export(dist_exports, {
7690
8049
  getScriptPath: () => getScriptPath,
7691
8050
  getStateFilePath: () => getStateFilePath,
7692
8051
  getStateItem: () => getStateItem,
8052
+ getSummaryData: () => getSummaryData,
7693
8053
  getUncheckedItems: () => getUncheckedItems,
7694
8054
  getValidJobTypes: () => getValidJobTypes,
7695
8055
  groupBySection: () => groupBySection2,
@@ -7717,6 +8077,7 @@ __export(dist_exports, {
7717
8077
  markItemProcessed: () => markItemProcessed,
7718
8078
  markJobRunning: () => markJobRunning,
7719
8079
  markPrdDone: () => markPrdDone,
8080
+ mergerLockPath: () => mergerLockPath,
7720
8081
  migrateJsonToSqlite: () => migrateJsonToSqlite,
7721
8082
  normalizeJobConfig: () => normalizeJobConfig,
7722
8083
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
@@ -7726,6 +8087,7 @@ __export(dist_exports, {
7726
8087
  parseTimeToMinutes: () => parseTimeToMinutes,
7727
8088
  performCancel: () => performCancel,
7728
8089
  plannerLockPath: () => plannerLockPath,
8090
+ prResolverLockPath: () => prResolverLockPath,
7729
8091
  prepareBranchWorktree: () => prepareBranchWorktree,
7730
8092
  prepareDetachedWorktree: () => prepareDetachedWorktree,
7731
8093
  projectRuntimeKey: () => projectRuntimeKey,
@@ -7822,6 +8184,7 @@ var init_dist = __esm({
7822
8184
  init_webhook_validator();
7823
8185
  init_worktree_manager();
7824
8186
  init_job_queue();
8187
+ init_summary();
7825
8188
  init_analytics();
7826
8189
  init_prd_template();
7827
8190
  init_slicer_prompt();
@@ -8098,6 +8461,8 @@ function buildInitConfig(params) {
8098
8461
  },
8099
8462
  audit: { ...defaults.audit },
8100
8463
  analytics: { ...defaults.analytics },
8464
+ merger: { ...defaults.merger },
8465
+ prResolver: { ...defaults.prResolver },
8101
8466
  jobProviders: { ...defaults.jobProviders },
8102
8467
  queue: {
8103
8468
  ...defaults.queue,
@@ -8221,7 +8586,7 @@ function initCommand(program2) {
8221
8586
  const cwd = process.cwd();
8222
8587
  const force = options.force || false;
8223
8588
  const prdDir = options.prdDir || DEFAULT_PRD_DIR;
8224
- const totalSteps = 13;
8589
+ const totalSteps = 14;
8225
8590
  const interactive = isInteractiveInitSession();
8226
8591
  console.log();
8227
8592
  header("Night Watch CLI - Initializing");
@@ -8476,7 +8841,35 @@ function initCommand(program2) {
8476
8841
  }
8477
8842
  }
8478
8843
  }
8479
- step(11, totalSteps, "Registering project in global registry...");
8844
+ step(11, totalSteps, "Syncing Night Watch labels to GitHub...");
8845
+ let labelSyncStatus = "Skipped";
8846
+ if (!remoteStatus.hasGitHubRemote || !ghAuthenticated) {
8847
+ labelSyncStatus = !remoteStatus.hasGitHubRemote ? "Skipped (no GitHub remote)" : "Skipped (gh auth required)";
8848
+ info("Skipping label sync (no GitHub remote or gh not authenticated).");
8849
+ } else {
8850
+ try {
8851
+ const { NIGHT_WATCH_LABELS: NIGHT_WATCH_LABELS2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
8852
+ let created = 0;
8853
+ for (const label2 of NIGHT_WATCH_LABELS2) {
8854
+ try {
8855
+ execSync3(
8856
+ `gh label create "${label2.name}" --description "${label2.description}" --color "${label2.color}" --force`,
8857
+ { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
8858
+ );
8859
+ created++;
8860
+ } catch {
8861
+ }
8862
+ }
8863
+ labelSyncStatus = `Synced ${created}/${NIGHT_WATCH_LABELS2.length} labels`;
8864
+ success(`Synced ${created}/${NIGHT_WATCH_LABELS2.length} labels to GitHub`);
8865
+ } catch (labelErr) {
8866
+ labelSyncStatus = "Failed";
8867
+ warn(
8868
+ `Could not sync labels: ${labelErr instanceof Error ? labelErr.message : String(labelErr)}`
8869
+ );
8870
+ }
8871
+ }
8872
+ step(12, totalSteps, "Registering project in global registry...");
8480
8873
  try {
8481
8874
  const { registerProject: registerProject2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
8482
8875
  const entry = registerProject2(cwd);
@@ -8486,7 +8879,7 @@ function initCommand(program2) {
8486
8879
  ` Warning: Could not register in global registry: ${regErr instanceof Error ? regErr.message : String(regErr)}`
8487
8880
  );
8488
8881
  }
8489
- step(12, totalSteps, "Installing Night Watch skills...");
8882
+ step(13, totalSteps, "Installing Night Watch skills...");
8490
8883
  const skillsResult = installSkills(cwd, selectedProvider, force, TEMPLATES_DIR);
8491
8884
  if (skillsResult.installed > 0) {
8492
8885
  success(`Installed ${skillsResult.installed} skills to ${skillsResult.location}`);
@@ -8498,7 +8891,7 @@ function initCommand(program2) {
8498
8891
  } else if (skillsResult.type === "none") {
8499
8892
  info("No compatible AI skills directory detected \u2014 skipping.");
8500
8893
  }
8501
- step(13, totalSteps, "Initialization complete!");
8894
+ step(14, totalSteps, "Initialization complete!");
8502
8895
  header("Initialization Complete");
8503
8896
  const filesTable = createTable({ head: ["Created Files", ""] });
8504
8897
  filesTable.push(["PRD Directory", `${prdDir}/done/`]);
@@ -8511,6 +8904,7 @@ function initCommand(program2) {
8511
8904
  filesTable.push(["", `instructions/prd-creator.md (${templateSources[5].source})`]);
8512
8905
  filesTable.push(["Config File", CONFIG_FILE_NAME]);
8513
8906
  filesTable.push(["Board Setup", boardSetupStatus]);
8907
+ filesTable.push(["Label Sync", labelSyncStatus]);
8514
8908
  filesTable.push(["Global Registry", "~/.night-watch/projects.json"]);
8515
8909
  let skillsSummary;
8516
8910
  if (skillsResult.installed > 0) {
@@ -9058,6 +9452,15 @@ function shouldSendReviewNotification(scriptStatus) {
9058
9452
  }
9059
9453
  return !scriptStatus.startsWith("skip_");
9060
9454
  }
9455
+ function shouldSendReviewCompletionNotification(exitCode, scriptStatus) {
9456
+ if (exitCode !== 0) {
9457
+ return false;
9458
+ }
9459
+ if (scriptStatus === "failure" || scriptStatus === "timeout") {
9460
+ return false;
9461
+ }
9462
+ return shouldSendReviewNotification(scriptStatus);
9463
+ }
9061
9464
  function parseAutoMergedPrNumbers(raw) {
9062
9465
  if (!raw || raw.trim().length === 0) {
9063
9466
  return [];
@@ -9139,10 +9542,6 @@ function buildEnvVars2(config, options) {
9139
9542
  env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
9140
9543
  env.NW_PRD_DIR = config.prdDir;
9141
9544
  env.NW_CLAUDE_MODEL_ID = CLAUDE_MODEL_IDS[config.primaryFallbackModel ?? config.claudeModel ?? "sonnet"];
9142
- if (config.autoMerge) {
9143
- env.NW_AUTO_MERGE = "1";
9144
- }
9145
- env.NW_AUTO_MERGE_METHOD = config.autoMergeMethod;
9146
9545
  return env;
9147
9546
  }
9148
9547
  function applyCliOverrides2(config, options) {
@@ -9156,9 +9555,6 @@ function applyCliOverrides2(config, options) {
9156
9555
  if (options.provider) {
9157
9556
  overridden._cliProviderOverride = options.provider;
9158
9557
  }
9159
- if (options.autoMerge !== void 0) {
9160
- overridden.autoMerge = options.autoMerge;
9161
- }
9162
9558
  return overridden;
9163
9559
  }
9164
9560
  function isFailingCheck(check) {
@@ -9219,7 +9615,7 @@ function getOpenPrsNeedingWork(branchPatterns) {
9219
9615
  }
9220
9616
  }
9221
9617
  function reviewCommand(program2) {
9222
- program2.command("review").description("Run PR reviewer now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds for reviewer").option("--provider <string>", "AI provider to use (claude or codex)").option("--auto-merge", "Enable auto-merge for this run").action(async (options) => {
9618
+ program2.command("review").description("Run PR reviewer now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds for reviewer").option("--provider <string>", "AI provider to use (claude or codex)").action(async (options) => {
9223
9619
  const projectDir = process.cwd();
9224
9620
  let config = loadConfig(projectDir);
9225
9621
  config = applyCliOverrides2(config, options);
@@ -9242,10 +9638,6 @@ function reviewCommand(program2) {
9242
9638
  ]);
9243
9639
  configTable.push(["Min Review Score", `${config.minReviewScore}/100`]);
9244
9640
  configTable.push(["Branch Patterns", config.branchPatterns.join(", ")]);
9245
- configTable.push([
9246
- "Auto-merge",
9247
- config.autoMerge ? `Enabled (${config.autoMergeMethod})` : "Disabled"
9248
- ]);
9249
9641
  configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
9250
9642
  configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
9251
9643
  configTable.push([
@@ -9316,12 +9708,15 @@ ${stderr}`);
9316
9708
  spinner.fail(`PR reviewer exited with code ${exitCode}`);
9317
9709
  }
9318
9710
  if (!options.dryRun) {
9319
- const skipNotification = !shouldSendReviewNotification(scriptResult?.status);
9320
- if (skipNotification) {
9321
- info("Skipping review notification (no actionable review result)");
9711
+ const shouldNotifyCompletion = shouldSendReviewCompletionNotification(
9712
+ exitCode,
9713
+ scriptResult?.status
9714
+ );
9715
+ if (!shouldNotifyCompletion) {
9716
+ info("Skipping review completion notification (review did not complete successfully)");
9322
9717
  }
9323
9718
  let fallbackPrDetails = null;
9324
- if (!skipNotification && exitCode === 0) {
9719
+ if (shouldNotifyCompletion) {
9325
9720
  const reviewedPrNumbers = parseReviewedPrNumbers(scriptResult?.data.prs);
9326
9721
  const firstReviewedPrNumber = reviewedPrNumbers[0];
9327
9722
  if (firstReviewedPrNumber !== void 0) {
@@ -9331,7 +9726,7 @@ ${stderr}`);
9331
9726
  fallbackPrDetails = fetchReviewedPrDetails(config.branchPatterns, projectDir);
9332
9727
  }
9333
9728
  }
9334
- if (!skipNotification) {
9729
+ if (shouldNotifyCompletion) {
9335
9730
  const attempts = parseRetryAttempts(scriptResult?.data.attempts);
9336
9731
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
9337
9732
  const legacyNoChangesNeeded = scriptResult?.data.no_changes_needed === "1";
@@ -9453,6 +9848,7 @@ function buildEnvVars3(config, options) {
9453
9848
  const branchPatterns = config.qa.branchPatterns.length > 0 ? config.qa.branchPatterns : config.branchPatterns;
9454
9849
  env.NW_BRANCH_PATTERNS = branchPatterns.join(",");
9455
9850
  env.NW_QA_SKIP_LABEL = config.qa.skipLabel;
9851
+ env.NW_QA_VALIDATED_LABEL = config.qa.validatedLabel;
9456
9852
  env.NW_QA_ARTIFACTS = config.qa.artifacts;
9457
9853
  env.NW_QA_AUTO_INSTALL_PLAYWRIGHT = config.qa.autoInstallPlaywright ? "1" : "0";
9458
9854
  env.NW_CLAUDE_MODEL_ID = CLAUDE_MODEL_IDS[config.primaryFallbackModel ?? config.claudeModel ?? "sonnet"];
@@ -9502,6 +9898,7 @@ function qaCommand(program2) {
9502
9898
  const branchPatterns = config.qa.branchPatterns.length > 0 ? config.qa.branchPatterns : config.branchPatterns;
9503
9899
  configTable.push(["Branch Patterns", branchPatterns.join(", ")]);
9504
9900
  configTable.push(["Skip Label", config.qa.skipLabel]);
9901
+ configTable.push(["Validated Label", config.qa.validatedLabel]);
9505
9902
  configTable.push(["Artifacts", config.qa.artifacts]);
9506
9903
  configTable.push([
9507
9904
  "Auto-install Playwright",
@@ -9866,6 +10263,22 @@ function performInstall(projectDir, config, options) {
9866
10263
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9867
10264
  entries.push(analyticsEntry);
9868
10265
  }
10266
+ const disablePrResolver = options?.noPrResolver === true || options?.prResolver === false;
10267
+ const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10268
+ if (installPrResolver) {
10269
+ const prResolverSchedule = config.prResolver.schedule;
10270
+ const prResolverLog = path25.join(logDir, "pr-resolver.log");
10271
+ const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10272
+ entries.push(prResolverEntry);
10273
+ }
10274
+ const disableMerger = options?.noMerger === true || options?.merger === false;
10275
+ const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
10276
+ if (installMerger) {
10277
+ const mergerSchedule = config.merger.schedule;
10278
+ const mergerLog = path25.join(logDir, "merger.log");
10279
+ const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
10280
+ entries.push(mergerEntry);
10281
+ }
9869
10282
  const existingEntries = new Set(
9870
10283
  Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
9871
10284
  );
@@ -9884,7 +10297,7 @@ function performInstall(projectDir, config, options) {
9884
10297
  }
9885
10298
  }
9886
10299
  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) => {
10300
+ 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("--no-merger", "Skip installing merger cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
9888
10301
  try {
9889
10302
  const projectDir = process.cwd();
9890
10303
  const config = loadConfig(projectDir);
@@ -9966,6 +10379,24 @@ function installCommand(program2) {
9966
10379
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9967
10380
  entries.push(analyticsEntry);
9968
10381
  }
10382
+ const disablePrResolver = options.noPrResolver === true || options.prResolver === false;
10383
+ const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10384
+ let prResolverLog;
10385
+ if (installPrResolver) {
10386
+ prResolverLog = path25.join(logDir, "pr-resolver.log");
10387
+ const prResolverSchedule = config.prResolver.schedule;
10388
+ const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10389
+ entries.push(prResolverEntry);
10390
+ }
10391
+ const disableMerger = options.noMerger === true || options.merger === false;
10392
+ const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
10393
+ let mergerLog;
10394
+ if (installMerger) {
10395
+ mergerLog = path25.join(logDir, "merger.log");
10396
+ const mergerSchedule = config.merger.schedule;
10397
+ const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
10398
+ entries.push(mergerEntry);
10399
+ }
9969
10400
  const existingEntrySet = new Set(existingEntries);
9970
10401
  const currentCrontab = readCrontab();
9971
10402
  const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
@@ -9995,6 +10426,12 @@ function installCommand(program2) {
9995
10426
  if (installAnalytics && analyticsLog) {
9996
10427
  dim(` Analytics: ${analyticsLog}`);
9997
10428
  }
10429
+ if (installPrResolver && prResolverLog) {
10430
+ dim(` PR Resolver: ${prResolverLog}`);
10431
+ }
10432
+ if (installMerger && mergerLog) {
10433
+ dim(` Merger: ${mergerLog}`);
10434
+ }
9998
10435
  console.log();
9999
10436
  dim("To uninstall, run: night-watch uninstall");
10000
10437
  dim("To check status, run: night-watch status");
@@ -10027,7 +10464,13 @@ function performUninstall(projectDir, options) {
10027
10464
  if (!options?.keepLogs) {
10028
10465
  const logDir = path26.join(projectDir, "logs");
10029
10466
  if (fs24.existsSync(logDir)) {
10030
- const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
10467
+ const logFiles = [
10468
+ "executor.log",
10469
+ "reviewer.log",
10470
+ "slicer.log",
10471
+ "audit.log",
10472
+ "pr-resolver.log"
10473
+ ];
10031
10474
  logFiles.forEach((logFile) => {
10032
10475
  const logPath = path26.join(logDir, logFile);
10033
10476
  if (fs24.existsSync(logPath)) {
@@ -10072,7 +10515,13 @@ function uninstallCommand(program2) {
10072
10515
  if (!options.keepLogs) {
10073
10516
  const logDir = path26.join(projectDir, "logs");
10074
10517
  if (fs24.existsSync(logDir)) {
10075
- const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
10518
+ const logFiles = [
10519
+ "executor.log",
10520
+ "reviewer.log",
10521
+ "slicer.log",
10522
+ "audit.log",
10523
+ "pr-resolver.log"
10524
+ ];
10076
10525
  let logsRemoved = 0;
10077
10526
  logFiles.forEach((logFile) => {
10078
10527
  const logPath = path26.join(logDir, logFile);
@@ -14546,12 +14995,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
14546
14995
  const auditPlan = getSchedulingPlan(projectDir, config, "audit");
14547
14996
  const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
14548
14997
  const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
14998
+ const mergerPlan = getSchedulingPlan(projectDir, config, "merger");
14549
14999
  const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
14550
15000
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
14551
15001
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
14552
15002
  const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
14553
15003
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
14554
15004
  const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
15005
+ const mergerInstalled = installed && (config.merger?.enabled ?? false) && hasScheduledCommand(entries, "merge");
14555
15006
  return {
14556
15007
  executor: {
14557
15008
  schedule: config.cronSchedule,
@@ -14601,6 +15052,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
14601
15052
  manualDelayMinutes: analyticsPlan.manualDelayMinutes,
14602
15053
  balancedDelayMinutes: analyticsPlan.balancedDelayMinutes
14603
15054
  },
15055
+ merger: {
15056
+ schedule: config.merger?.schedule ?? "55 */4 * * *",
15057
+ installed: mergerInstalled,
15058
+ nextRun: mergerInstalled ? addDelayToIsoString(computeNextRun(config.merger?.schedule ?? "55 */4 * * *"), mergerPlan.totalDelayMinutes) : null,
15059
+ delayMinutes: mergerPlan.totalDelayMinutes,
15060
+ manualDelayMinutes: mergerPlan.manualDelayMinutes,
15061
+ balancedDelayMinutes: mergerPlan.balancedDelayMinutes
15062
+ },
14604
15063
  paused: !installed,
14605
15064
  schedulingPriority: config.schedulingPriority,
14606
15065
  entries
@@ -15710,7 +16169,7 @@ function releasePlannerLock(lockFile) {
15710
16169
  }
15711
16170
  }
15712
16171
  function resolvePlannerIssueColumn(config) {
15713
- return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
16172
+ return config.roadmapScanner.issueColumn === "Draft" ? "Draft" : "Ready";
15714
16173
  }
15715
16174
  function buildPlannerIssueBody(projectDir, config, result) {
15716
16175
  const relativePrdPath = path38.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
@@ -15724,23 +16183,25 @@ function buildPlannerIssueBody(projectDir, config, result) {
15724
16183
  }
15725
16184
  const maxBodyChars = 6e4;
15726
16185
  const truncated = prdContent.length > maxBodyChars;
15727
- const prdPreview = truncated ? `${prdContent.slice(0, maxBodyChars)}
16186
+ const prdBody = truncated ? `${prdContent.slice(0, maxBodyChars)}
15728
16187
 
15729
16188
  ...[truncated]` : prdContent;
15730
- const sourceLines = sourceItem ? [
15731
- `- Source section: ${sourceItem.section}`,
15732
- `- Source item: ${sourceItem.title}`,
15733
- sourceItem.description ? `- Source summary: ${sourceItem.description}` : ""
15734
- ].filter((line) => line.length > 0) : [];
16189
+ const metaLines = [`- PRD file: \`${relativePrdPath}\``];
16190
+ if (sourceItem) {
16191
+ metaLines.push(`- Source section: ${sourceItem.section}`);
16192
+ if (sourceItem.description) {
16193
+ metaLines.push(`- Source summary: ${sourceItem.description}`);
16194
+ }
16195
+ }
15735
16196
  return [
15736
- "## Planner Generated PRD",
16197
+ prdBody,
15737
16198
  "",
15738
- `- PRD file: \`${relativePrdPath}\``,
15739
- ...sourceLines,
16199
+ "<details>",
16200
+ "<summary>Source metadata</summary>",
15740
16201
  "",
15741
- "---",
16202
+ ...metaLines,
15742
16203
  "",
15743
- prdPreview
16204
+ "</details>"
15744
16205
  ].join("\n");
15745
16206
  }
15746
16207
  async function createPlannerIssue(projectDir, config, result) {
@@ -15755,9 +16216,11 @@ async function createPlannerIssue(projectDir, config, result) {
15755
16216
  if (!board) {
15756
16217
  return { created: false, skippedReason: "board-not-configured" };
15757
16218
  }
16219
+ const issueTitle = `PRD: ${result.item.title}`;
16220
+ const normalizeTitle = (t) => t.replace(/^PRD:\s*/i, "").trim().toLowerCase();
15758
16221
  const existingIssues = await provider.getAllIssues();
15759
16222
  const existing = existingIssues.find(
15760
- (issue2) => issue2.title.trim().toLowerCase() === result.item.title.trim().toLowerCase()
16223
+ (issue2) => normalizeTitle(issue2.title) === normalizeTitle(result.item.title)
15761
16224
  );
15762
16225
  if (existing) {
15763
16226
  return {
@@ -15768,7 +16231,7 @@ async function createPlannerIssue(projectDir, config, result) {
15768
16231
  };
15769
16232
  }
15770
16233
  const issue = await provider.createIssue({
15771
- title: result.item.title,
16234
+ title: issueTitle,
15772
16235
  body: buildPlannerIssueBody(projectDir, config, result),
15773
16236
  column: resolvePlannerIssueColumn(config)
15774
16237
  });
@@ -16742,8 +17205,9 @@ function createQueueCommand() {
16742
17205
  }
16743
17206
  process.exit(0);
16744
17207
  });
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);
17208
+ 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) => {
17209
+ const configDir = _opts.projectDir ?? process.cwd();
17210
+ const entry = dispatchNextJob(loadConfig(configDir).queue);
16747
17211
  if (!entry) {
16748
17212
  logger5.info("No pending jobs to dispatch");
16749
17213
  return;
@@ -16821,8 +17285,9 @@ function createQueueCommand() {
16821
17285
  }
16822
17286
  removeJob(queueId);
16823
17287
  });
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;
17288
+ 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) => {
17289
+ const configDir = opts.projectDir ?? process.cwd();
17290
+ const queueConfig = loadConfig(configDir).queue;
16826
17291
  process.exit(canStartJob(queueConfig) ? 0 : 1);
16827
17292
  });
16828
17293
  queue.command("expire").description("Expire stale queued jobs").option(
@@ -16844,7 +17309,26 @@ function createQueueCommand() {
16844
17309
  });
16845
17310
  return queue;
16846
17311
  }
16847
- var QUEUE_MARKER_KEYS = /* @__PURE__ */ new Set(["NW_DRY_RUN", "NW_CRON_TRIGGER", "NW_DEFAULT_BRANCH"]);
17312
+ var QUEUE_MARKER_KEYS = /* @__PURE__ */ new Set([
17313
+ "NW_DRY_RUN",
17314
+ "NW_CRON_TRIGGER",
17315
+ "NW_DEFAULT_BRANCH",
17316
+ "NW_TARGET_PR",
17317
+ "NW_REVIEWER_WORKER_MODE",
17318
+ "NW_REVIEWER_PARALLEL",
17319
+ "NW_REVIEWER_WORKER_STAGGER",
17320
+ "NW_REVIEWER_MAX_RUNTIME",
17321
+ "NW_REVIEWER_MAX_RETRIES",
17322
+ "NW_REVIEWER_RETRY_DELAY",
17323
+ "NW_REVIEWER_MAX_PRS_PER_RUN",
17324
+ "NW_MIN_REVIEW_SCORE",
17325
+ "NW_BRANCH_PATTERNS",
17326
+ "NW_PRD_DIR",
17327
+ "NW_AUTO_MERGE",
17328
+ "NW_AUTO_MERGE_METHOD",
17329
+ "NW_MAX_RUNTIME",
17330
+ "NW_QA_MAX_RUNTIME"
17331
+ ]);
16848
17332
  function filterQueueMarkers(envJson) {
16849
17333
  const result = {};
16850
17334
  for (const [key, value] of Object.entries(envJson)) {
@@ -16913,6 +17397,422 @@ function notifyCommand(program2) {
16913
17397
  );
16914
17398
  }
16915
17399
 
17400
+ // src/commands/summary.ts
17401
+ init_dist();
17402
+ import path42 from "path";
17403
+ import chalk8 from "chalk";
17404
+ function formatDuration2(seconds) {
17405
+ if (seconds === null) return "-";
17406
+ const mins = Math.floor(seconds / 60);
17407
+ const secs = seconds % 60;
17408
+ if (mins === 0) return `${secs}s`;
17409
+ return `${mins}m ${secs}s`;
17410
+ }
17411
+ function formatCiStatus2(status) {
17412
+ if (status === "pass") return chalk8.green("pass");
17413
+ if (status === "fail") return chalk8.red("fail");
17414
+ if (status === "pending") return chalk8.yellow("pending");
17415
+ return chalk8.dim("unknown");
17416
+ }
17417
+ function formatReviewScore2(score) {
17418
+ if (score === null) return chalk8.dim("-");
17419
+ if (score >= 80) return chalk8.green(String(score));
17420
+ if (score >= 60) return chalk8.yellow(String(score));
17421
+ return chalk8.red(String(score));
17422
+ }
17423
+ function formatJobStatus(status) {
17424
+ if (status === "success") return chalk8.green("success");
17425
+ if (status === "failure") return chalk8.red("failure");
17426
+ if (status === "timeout") return chalk8.yellow("timeout");
17427
+ if (status === "rate_limited") return chalk8.magenta("rate_limited");
17428
+ if (status === "skipped") return chalk8.dim("skipped");
17429
+ return chalk8.dim(status);
17430
+ }
17431
+ function getProjectName2(projectPath) {
17432
+ return path42.basename(projectPath) || projectPath;
17433
+ }
17434
+ function formatProvider(providerKey) {
17435
+ return providerKey.split(":")[0] || providerKey;
17436
+ }
17437
+ function summaryCommand(program2) {
17438
+ program2.command("summary").description("Show a summary of recent Night Watch activity").option(
17439
+ "--hours <n>",
17440
+ "Time window in hours (default: 12)",
17441
+ String(DEFAULT_SUMMARY_WINDOW_HOURS)
17442
+ ).option("--json", "Output summary as JSON").action(async (options) => {
17443
+ try {
17444
+ const projectDir = process.cwd();
17445
+ const config = loadConfig(projectDir);
17446
+ const hours = parseInt(options.hours || String(DEFAULT_SUMMARY_WINDOW_HOURS), 10);
17447
+ if (isNaN(hours) || hours <= 0) {
17448
+ console.error("Error: --hours must be a positive integer");
17449
+ process.exit(1);
17450
+ }
17451
+ const data = await getSummaryData(projectDir, hours, config.branchPatterns);
17452
+ if (options.json) {
17453
+ console.log(JSON.stringify(data, null, 2));
17454
+ return;
17455
+ }
17456
+ console.log();
17457
+ console.log(chalk8.bold.cyan(`Night Watch Summary (last ${data.windowHours}h)`));
17458
+ console.log(chalk8.dim("\u2500".repeat(40)));
17459
+ console.log();
17460
+ if (data.jobRuns.length === 0) {
17461
+ info("No recent activity in this time window.");
17462
+ console.log();
17463
+ } else {
17464
+ const countParts = [];
17465
+ if (data.counts.succeeded > 0) {
17466
+ countParts.push(chalk8.green(`\u2713 ${data.counts.succeeded} succeeded`));
17467
+ }
17468
+ if (data.counts.failed > 0) {
17469
+ countParts.push(chalk8.red(`\u2717 ${data.counts.failed} failed`));
17470
+ }
17471
+ if (data.counts.timedOut > 0) {
17472
+ countParts.push(chalk8.yellow(`\u23F1 ${data.counts.timedOut} timed out`));
17473
+ }
17474
+ if (data.counts.rateLimited > 0) {
17475
+ countParts.push(chalk8.magenta(`\u23F3 ${data.counts.rateLimited} rate limited`));
17476
+ }
17477
+ if (data.counts.skipped > 0) {
17478
+ countParts.push(chalk8.dim(`${data.counts.skipped} skipped`));
17479
+ }
17480
+ console.log(`Jobs Executed: ${data.counts.total}`);
17481
+ if (countParts.length > 0) {
17482
+ console.log(` ${countParts.join(" ")}`);
17483
+ }
17484
+ console.log();
17485
+ const table = createTable({
17486
+ head: ["Job", "Status", "Project", "Provider", "Duration"],
17487
+ colWidths: [12, 12, 20, 12, 12]
17488
+ });
17489
+ for (const run2 of data.jobRuns.slice(0, 10)) {
17490
+ table.push([
17491
+ run2.jobType,
17492
+ formatJobStatus(run2.status),
17493
+ getProjectName2(run2.projectPath),
17494
+ formatProvider(run2.providerKey),
17495
+ formatDuration2(run2.durationSeconds)
17496
+ ]);
17497
+ }
17498
+ console.log(table.toString());
17499
+ if (data.jobRuns.length > 10) {
17500
+ dim(` ... and ${data.jobRuns.length - 10} more`);
17501
+ }
17502
+ console.log();
17503
+ }
17504
+ if (data.openPrs.length > 0) {
17505
+ header(`Open PRs (${data.openPrs.length})`);
17506
+ const prTable = createTable({
17507
+ head: ["#", "Title", "CI", "Score"],
17508
+ colWidths: [6, 40, 10, 8]
17509
+ });
17510
+ for (const pr of data.openPrs) {
17511
+ const title = pr.title.length > 37 ? pr.title.substring(0, 34) + "..." : pr.title;
17512
+ prTable.push([
17513
+ String(pr.number),
17514
+ title,
17515
+ formatCiStatus2(pr.ciStatus),
17516
+ formatReviewScore2(pr.reviewScore)
17517
+ ]);
17518
+ }
17519
+ console.log(prTable.toString());
17520
+ console.log();
17521
+ }
17522
+ if (data.pendingQueueItems.length > 0) {
17523
+ const jobTypes = [...new Set(data.pendingQueueItems.map((item) => item.jobType))];
17524
+ const projectNames = [...new Set(data.pendingQueueItems.map((item) => item.projectName))];
17525
+ dim(
17526
+ `Queue: ${data.pendingQueueItems.length} pending (${jobTypes.join(", ")}) for ${projectNames.join(", ")}`
17527
+ );
17528
+ console.log();
17529
+ }
17530
+ if (data.actionItems.length > 0) {
17531
+ console.log(chalk8.yellow("\u26A0 Action needed:"));
17532
+ for (const item of data.actionItems) {
17533
+ console.log(` \u2022 ${item}`);
17534
+ }
17535
+ } else {
17536
+ console.log(chalk8.green("\u2713 No action needed \u2014 all jobs healthy."));
17537
+ }
17538
+ console.log();
17539
+ } catch (error2) {
17540
+ console.error(
17541
+ `Error getting summary: ${error2 instanceof Error ? error2.message : String(error2)}`
17542
+ );
17543
+ process.exit(1);
17544
+ }
17545
+ });
17546
+ }
17547
+
17548
+ // src/commands/resolve.ts
17549
+ init_dist();
17550
+ import { execFileSync as execFileSync7 } from "child_process";
17551
+ import * as path43 from "path";
17552
+ function buildEnvVars6(config, options) {
17553
+ const env = buildBaseEnvVars(config, "pr-resolver", options.dryRun);
17554
+ env.NW_PR_RESOLVER_MAX_RUNTIME = String(config.prResolver.maxRuntime);
17555
+ env.NW_PR_RESOLVER_MAX_PRS_PER_RUN = String(config.prResolver.maxPrsPerRun);
17556
+ env.NW_PR_RESOLVER_PER_PR_TIMEOUT = String(config.prResolver.perPrTimeout);
17557
+ env.NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION = config.prResolver.aiConflictResolution ? "1" : "0";
17558
+ env.NW_PR_RESOLVER_AI_REVIEW_RESOLUTION = config.prResolver.aiReviewResolution ? "1" : "0";
17559
+ env.NW_PR_RESOLVER_READY_LABEL = config.prResolver.readyLabel;
17560
+ env.NW_PR_RESOLVER_BRANCH_PATTERNS = config.prResolver.branchPatterns.join(",");
17561
+ return env;
17562
+ }
17563
+ function applyCliOverrides5(config, options) {
17564
+ const overridden = { ...config, prResolver: { ...config.prResolver } };
17565
+ if (options.timeout) {
17566
+ const timeout = parseInt(options.timeout, 10);
17567
+ if (!isNaN(timeout)) {
17568
+ overridden.prResolver.maxRuntime = timeout;
17569
+ }
17570
+ }
17571
+ if (options.provider) {
17572
+ overridden._cliProviderOverride = options.provider;
17573
+ }
17574
+ return overridden;
17575
+ }
17576
+ function getOpenPrs() {
17577
+ try {
17578
+ const args = ["pr", "list", "--state", "open", "--json", "number,title,headRefName,mergeable"];
17579
+ const result = execFileSync7("gh", args, {
17580
+ encoding: "utf-8",
17581
+ stdio: ["pipe", "pipe", "pipe"]
17582
+ });
17583
+ const prs = JSON.parse(result.trim() || "[]");
17584
+ return prs.map(
17585
+ (pr) => ({
17586
+ number: pr.number,
17587
+ title: pr.title,
17588
+ branch: pr.headRefName,
17589
+ mergeable: pr.mergeable
17590
+ })
17591
+ );
17592
+ } catch {
17593
+ return [];
17594
+ }
17595
+ }
17596
+ function resolveCommand(program2) {
17597
+ 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) => {
17598
+ const projectDir = process.cwd();
17599
+ let config = loadConfig(projectDir);
17600
+ config = applyCliOverrides5(config, options);
17601
+ if (!config.prResolver.enabled && !options.dryRun) {
17602
+ info("PR resolver is disabled in config; skipping.");
17603
+ process.exit(0);
17604
+ }
17605
+ const envVars = buildEnvVars6(config, options);
17606
+ const scriptPath = getScriptPath("night-watch-pr-resolver-cron.sh");
17607
+ if (options.dryRun) {
17608
+ header("Dry Run: PR Resolver");
17609
+ const resolverProvider = resolveJobProvider(config, "pr-resolver");
17610
+ header("Configuration");
17611
+ const configTable = createTable({ head: ["Setting", "Value"] });
17612
+ configTable.push(["Provider", resolverProvider]);
17613
+ configTable.push([
17614
+ "Max Runtime",
17615
+ `${config.prResolver.maxRuntime}s (${Math.floor(config.prResolver.maxRuntime / 60)}min)`
17616
+ ]);
17617
+ configTable.push([
17618
+ "Max PRs Per Run",
17619
+ config.prResolver.maxPrsPerRun === 0 ? "Unlimited" : String(config.prResolver.maxPrsPerRun)
17620
+ ]);
17621
+ configTable.push(["Per-PR Timeout", `${config.prResolver.perPrTimeout}s`]);
17622
+ configTable.push([
17623
+ "AI Conflict Resolution",
17624
+ config.prResolver.aiConflictResolution ? "Enabled" : "Disabled"
17625
+ ]);
17626
+ configTable.push([
17627
+ "AI Review Resolution",
17628
+ config.prResolver.aiReviewResolution ? "Enabled" : "Disabled"
17629
+ ]);
17630
+ configTable.push(["Ready Label", config.prResolver.readyLabel]);
17631
+ configTable.push([
17632
+ "Branch Patterns",
17633
+ config.prResolver.branchPatterns.length > 0 ? config.prResolver.branchPatterns.join(", ") : "(all)"
17634
+ ]);
17635
+ console.log(configTable.toString());
17636
+ header("Open PRs");
17637
+ const openPrs = getOpenPrs();
17638
+ if (openPrs.length === 0) {
17639
+ dim(" (no open PRs found)");
17640
+ } else {
17641
+ for (const pr of openPrs) {
17642
+ const conflictStatus = pr.mergeable === "CONFLICTING" ? " [CONFLICT]" : "";
17643
+ info(`#${pr.number}: ${pr.title}${conflictStatus}`);
17644
+ dim(` Branch: ${pr.branch}`);
17645
+ }
17646
+ }
17647
+ header("Environment Variables");
17648
+ for (const [key, value] of Object.entries(envVars)) {
17649
+ dim(` ${key}=${value}`);
17650
+ }
17651
+ header("Command");
17652
+ dim(` bash ${scriptPath} ${projectDir}`);
17653
+ console.log();
17654
+ process.exit(0);
17655
+ }
17656
+ const spinner = createSpinner("Running PR resolver...");
17657
+ spinner.start();
17658
+ try {
17659
+ await maybeApplyCronSchedulingDelay(config, "pr-resolver", projectDir);
17660
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(
17661
+ scriptPath,
17662
+ [projectDir],
17663
+ envVars
17664
+ );
17665
+ const scriptResult = parseScriptResult(`${stdout}
17666
+ ${stderr}`);
17667
+ if (exitCode === 0) {
17668
+ if (scriptResult?.status === "queued") {
17669
+ spinner.succeed("PR resolver queued \u2014 another job is currently running");
17670
+ } else if (scriptResult?.status?.startsWith("skip_")) {
17671
+ spinner.succeed("PR resolver completed (no PRs needed resolution)");
17672
+ } else {
17673
+ spinner.succeed("PR resolver completed successfully");
17674
+ }
17675
+ } else {
17676
+ spinner.fail(`PR resolver exited with code ${exitCode}`);
17677
+ }
17678
+ const notificationEvent = exitCode === 0 ? "pr_resolver_completed" : "pr_resolver_failed";
17679
+ await sendNotifications(config, {
17680
+ event: notificationEvent,
17681
+ projectName: path43.basename(projectDir),
17682
+ exitCode,
17683
+ provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
17684
+ });
17685
+ process.exit(exitCode);
17686
+ } catch (err) {
17687
+ spinner.fail("Failed to execute resolve command");
17688
+ error(`${err instanceof Error ? err.message : String(err)}`);
17689
+ process.exit(1);
17690
+ }
17691
+ });
17692
+ }
17693
+
17694
+ // src/commands/merge.ts
17695
+ init_dist();
17696
+ import * as path44 from "path";
17697
+ function buildEnvVars7(config, options) {
17698
+ const env = buildBaseEnvVars(config, "merger", options.dryRun);
17699
+ env.NW_MERGER_MAX_RUNTIME = String(config.merger.maxRuntime);
17700
+ env.NW_MERGER_MERGE_METHOD = config.merger.mergeMethod;
17701
+ env.NW_MERGER_MIN_REVIEW_SCORE = String(config.merger.minReviewScore);
17702
+ env.NW_MERGER_BRANCH_PATTERNS = (config.merger.branchPatterns.length > 0 ? config.merger.branchPatterns : config.branchPatterns).join(",");
17703
+ env.NW_MERGER_REBASE_BEFORE_MERGE = config.merger.rebaseBeforeMerge ? "1" : "0";
17704
+ env.NW_MERGER_MAX_PRS_PER_RUN = String(config.merger.maxPrsPerRun);
17705
+ return env;
17706
+ }
17707
+ function applyCliOverrides6(config, options) {
17708
+ const overridden = { ...config, merger: { ...config.merger } };
17709
+ if (options.timeout) {
17710
+ const timeout = parseInt(options.timeout, 10);
17711
+ if (!isNaN(timeout)) {
17712
+ overridden.merger.maxRuntime = timeout;
17713
+ }
17714
+ }
17715
+ if (options.provider) {
17716
+ overridden._cliProviderOverride = options.provider;
17717
+ }
17718
+ return overridden;
17719
+ }
17720
+ function resolveMergeNotificationEvent(exitCode, mergedCount, failedCount) {
17721
+ if (exitCode === 0 && mergedCount > 0) {
17722
+ return "merge_completed";
17723
+ }
17724
+ if (exitCode !== 0 || failedCount > 0) {
17725
+ return "merge_failed";
17726
+ }
17727
+ return null;
17728
+ }
17729
+ function printDryRun(config, envVars, scriptPath, projectDir) {
17730
+ header("Dry Run: Merge Orchestrator");
17731
+ const mergerProvider = resolveJobProvider(config, "merger");
17732
+ header("Configuration");
17733
+ const configTable = createTable({ head: ["Setting", "Value"] });
17734
+ configTable.push(["Provider", mergerProvider]);
17735
+ configTable.push([
17736
+ "Max Runtime",
17737
+ `${config.merger.maxRuntime}s (${Math.floor(config.merger.maxRuntime / 60)}min)`
17738
+ ]);
17739
+ configTable.push(["Merge Method", config.merger.mergeMethod]);
17740
+ configTable.push(["Min Review Score", `${config.merger.minReviewScore}/100`]);
17741
+ configTable.push([
17742
+ "Branch Patterns",
17743
+ config.merger.branchPatterns.length > 0 ? config.merger.branchPatterns.join(", ") : "(top-level)"
17744
+ ]);
17745
+ configTable.push(["Rebase Before Merge", config.merger.rebaseBeforeMerge ? "Yes" : "No"]);
17746
+ configTable.push([
17747
+ "Max PRs Per Run",
17748
+ config.merger.maxPrsPerRun === 0 ? "Unlimited" : String(config.merger.maxPrsPerRun)
17749
+ ]);
17750
+ console.log(configTable.toString());
17751
+ header("Environment Variables");
17752
+ for (const [key, value] of Object.entries(envVars)) {
17753
+ dim(` ${key}=${value}`);
17754
+ }
17755
+ header("Command");
17756
+ dim(` bash ${scriptPath} ${projectDir}`);
17757
+ console.log();
17758
+ }
17759
+ function mergeCommand(program2) {
17760
+ program2.command("merge").description("Merge eligible PRs in FIFO order").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) => {
17761
+ const projectDir = process.cwd();
17762
+ let config = loadConfig(projectDir);
17763
+ config = applyCliOverrides6(config, options);
17764
+ if (!config.merger.enabled && !options.dryRun) {
17765
+ info("Merge orchestrator is disabled in config; skipping.");
17766
+ process.exit(0);
17767
+ }
17768
+ const envVars = buildEnvVars7(config, options);
17769
+ const scriptPath = getScriptPath("night-watch-merger-cron.sh");
17770
+ if (options.dryRun) {
17771
+ printDryRun(config, envVars, scriptPath, projectDir);
17772
+ process.exit(0);
17773
+ }
17774
+ const spinner = createSpinner("Running merge orchestrator...");
17775
+ spinner.start();
17776
+ try {
17777
+ await maybeApplyCronSchedulingDelay(config, "merger", projectDir);
17778
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(
17779
+ scriptPath,
17780
+ [projectDir],
17781
+ envVars
17782
+ );
17783
+ const scriptResult = parseScriptResult(`${stdout}
17784
+ ${stderr}`);
17785
+ if (exitCode === 0) {
17786
+ if (scriptResult?.status === "queued") {
17787
+ spinner.succeed("Merge orchestrator queued \u2014 another job is currently running");
17788
+ } else if (scriptResult?.status?.startsWith("skip_")) {
17789
+ spinner.succeed("Merge orchestrator completed (no eligible PRs)");
17790
+ } else {
17791
+ spinner.succeed("Merge orchestrator completed successfully");
17792
+ }
17793
+ } else {
17794
+ spinner.fail(`Merge orchestrator exited with code ${exitCode}`);
17795
+ }
17796
+ const mergedCount = parseInt(scriptResult?.data?.merged ?? "0", 10);
17797
+ const failedCount = parseInt(scriptResult?.data?.failed ?? "0", 10);
17798
+ const notificationEvent = resolveMergeNotificationEvent(exitCode, mergedCount, failedCount);
17799
+ if (notificationEvent) {
17800
+ await sendNotifications(config, {
17801
+ event: notificationEvent,
17802
+ projectName: path44.basename(projectDir),
17803
+ exitCode,
17804
+ provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
17805
+ });
17806
+ }
17807
+ process.exit(exitCode);
17808
+ } catch (err) {
17809
+ spinner.fail("Failed to execute merge command");
17810
+ error(`${err instanceof Error ? err.message : String(err)}`);
17811
+ process.exit(1);
17812
+ }
17813
+ });
17814
+ }
17815
+
16916
17816
  // src/cli.ts
16917
17817
  var __filename5 = fileURLToPath6(import.meta.url);
16918
17818
  var __dirname5 = dirname12(__filename5);
@@ -16954,4 +17854,7 @@ program.addCommand(createStateCommand());
16954
17854
  boardCommand(program);
16955
17855
  queueCommand(program);
16956
17856
  notifyCommand(program);
17857
+ summaryCommand(program);
17858
+ resolveCommand(program);
17859
+ mergeCommand(program);
16957
17860
  program.parse();