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

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 (42) hide show
  1. package/dist/cli.js +1090 -524
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/audit.d.ts.map +1 -1
  4. package/dist/commands/audit.js +10 -2
  5. package/dist/commands/audit.js.map +1 -1
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +1 -0
  8. package/dist/commands/init.js.map +1 -1
  9. package/dist/commands/install.d.ts +4 -0
  10. package/dist/commands/install.d.ts.map +1 -1
  11. package/dist/commands/install.js +25 -0
  12. package/dist/commands/install.js.map +1 -1
  13. package/dist/commands/logs.d.ts +1 -1
  14. package/dist/commands/logs.d.ts.map +1 -1
  15. package/dist/commands/logs.js +25 -5
  16. package/dist/commands/logs.js.map +1 -1
  17. package/dist/commands/merge.d.ts +26 -0
  18. package/dist/commands/merge.d.ts.map +1 -0
  19. package/dist/commands/merge.js +159 -0
  20. package/dist/commands/merge.js.map +1 -0
  21. package/dist/commands/review.d.ts +5 -1
  22. package/dist/commands/review.d.ts.map +1 -1
  23. package/dist/commands/review.js +18 -18
  24. package/dist/commands/review.js.map +1 -1
  25. package/dist/commands/slice.d.ts +1 -0
  26. package/dist/commands/slice.d.ts.map +1 -1
  27. package/dist/commands/slice.js +19 -19
  28. package/dist/commands/slice.js.map +1 -1
  29. package/dist/commands/status.d.ts.map +1 -1
  30. package/dist/commands/status.js +54 -0
  31. package/dist/commands/status.js.map +1 -1
  32. package/dist/scripts/night-watch-merger-cron.sh +321 -0
  33. package/dist/scripts/night-watch-pr-reviewer-cron.sh +2 -137
  34. package/dist/templates/slicer.md +54 -64
  35. package/dist/web/assets/index-B1BnOpiO.css +1 -0
  36. package/dist/web/assets/index-CPQbZ1BL.css +1 -0
  37. package/dist/web/assets/index-CiRJZI4z.js +386 -0
  38. package/dist/web/assets/index-DcgNAi4A.js +386 -0
  39. package/dist/web/assets/index-SQlBKu_s.js +386 -0
  40. package/dist/web/assets/index-ZE5lOeJp.js +386 -0
  41. package/dist/web/index.html +2 -2
  42. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -17,6 +17,15 @@ var init_types = __esm({
17
17
  }
18
18
  });
19
19
 
20
+ // ../core/dist/board/types.js
21
+ var BOARD_COLUMNS;
22
+ var init_types2 = __esm({
23
+ "../core/dist/board/types.js"() {
24
+ "use strict";
25
+ BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
26
+ }
27
+ });
28
+
20
29
  // ../core/dist/jobs/job-registry.js
21
30
  function getJobDef(id) {
22
31
  return JOB_MAP.get(id);
@@ -170,6 +179,7 @@ var JOB_REGISTRY, JOB_MAP;
170
179
  var init_job_registry = __esm({
171
180
  "../core/dist/jobs/job-registry.js"() {
172
181
  "use strict";
182
+ init_types2();
173
183
  JOB_REGISTRY = [
174
184
  {
175
185
  id: "executor",
@@ -286,10 +296,19 @@ var init_job_registry = __esm({
286
296
  lockSuffix: "-audit.lock",
287
297
  queuePriority: 10,
288
298
  envPrefix: "NW_AUDIT",
299
+ extraFields: [
300
+ {
301
+ name: "targetColumn",
302
+ type: "enum",
303
+ enumValues: [...BOARD_COLUMNS],
304
+ defaultValue: "Draft"
305
+ }
306
+ ],
289
307
  defaultConfig: {
290
308
  enabled: true,
291
309
  schedule: "50 3 * * 1",
292
- maxRuntime: 1800
310
+ maxRuntime: 1800,
311
+ targetColumn: "Draft"
293
312
  }
294
313
  },
295
314
  {
@@ -306,7 +325,7 @@ var init_job_registry = __esm({
306
325
  {
307
326
  name: "targetColumn",
308
327
  type: "enum",
309
- enumValues: ["Draft", "Ready", "In Progress", "Done", "Closed"],
328
+ enumValues: [...BOARD_COLUMNS],
310
329
  defaultValue: "Draft"
311
330
  },
312
331
  { name: "analysisPrompt", type: "string", defaultValue: "" }
@@ -319,6 +338,38 @@ var init_job_registry = __esm({
319
338
  targetColumn: "Draft",
320
339
  analysisPrompt: ""
321
340
  }
341
+ },
342
+ {
343
+ id: "merger",
344
+ name: "Merge Orchestrator",
345
+ description: "Repo-wide PR merge coordinator \u2014 scans, rebases, and merges in FIFO order",
346
+ cliCommand: "merge",
347
+ logName: "merger",
348
+ lockSuffix: "-merger.lock",
349
+ queuePriority: 45,
350
+ envPrefix: "NW_MERGER",
351
+ extraFields: [
352
+ {
353
+ name: "mergeMethod",
354
+ type: "enum",
355
+ enumValues: ["squash", "merge", "rebase"],
356
+ defaultValue: "squash"
357
+ },
358
+ { name: "minReviewScore", type: "number", defaultValue: 80 },
359
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
360
+ { name: "rebaseBeforeMerge", type: "boolean", defaultValue: true },
361
+ { name: "maxPrsPerRun", type: "number", defaultValue: 0 }
362
+ ],
363
+ defaultConfig: {
364
+ enabled: false,
365
+ schedule: "55 */4 * * *",
366
+ maxRuntime: 1800,
367
+ mergeMethod: "squash",
368
+ minReviewScore: 80,
369
+ branchPatterns: [],
370
+ rebaseBeforeMerge: true,
371
+ maxPrsPerRun: 0
372
+ }
322
373
  }
323
374
  ];
324
375
  JOB_MAP = new Map(JOB_REGISTRY.map((job) => [job.id, job]));
@@ -339,7 +390,7 @@ function resolveProviderBucketKey(provider, providerEnv) {
339
390
  return `claude-proxy:${baseUrl}`;
340
391
  }
341
392
  }
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;
393
+ 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_TARGET_COLUMN, 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;
343
394
  var init_constants = __esm({
344
395
  "../core/dist/constants.js"() {
345
396
  "use strict";
@@ -384,7 +435,7 @@ var init_constants = __esm({
384
435
  slicerSchedule: DEFAULT_SLICER_SCHEDULE,
385
436
  slicerMaxRuntime: DEFAULT_SLICER_MAX_RUNTIME,
386
437
  priorityMode: "roadmap-first",
387
- issueColumn: "Draft"
438
+ issueColumn: "Ready"
388
439
  };
389
440
  DEFAULT_TEMPLATES_DIR = ".night-watch/templates";
390
441
  DEFAULT_BOARD_PROVIDER = {
@@ -416,10 +467,12 @@ var init_constants = __esm({
416
467
  DEFAULT_AUDIT_ENABLED = true;
417
468
  DEFAULT_AUDIT_SCHEDULE = "50 3 * * 1";
418
469
  DEFAULT_AUDIT_MAX_RUNTIME = 1800;
470
+ DEFAULT_AUDIT_TARGET_COLUMN = "Draft";
419
471
  DEFAULT_AUDIT = {
420
472
  enabled: DEFAULT_AUDIT_ENABLED,
421
473
  schedule: DEFAULT_AUDIT_SCHEDULE,
422
- maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
474
+ maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME,
475
+ targetColumn: DEFAULT_AUDIT_TARGET_COLUMN
423
476
  };
424
477
  DEFAULT_ANALYTICS_ENABLED = false;
425
478
  DEFAULT_ANALYTICS_SCHEDULE = "0 6 * * 1";
@@ -458,6 +511,24 @@ If no issues are warranted, output an empty array: []`;
458
511
  aiReviewResolution: DEFAULT_PR_RESOLVER_AI_REVIEW_RESOLUTION,
459
512
  readyLabel: DEFAULT_PR_RESOLVER_READY_LABEL
460
513
  };
514
+ DEFAULT_MERGER_ENABLED = false;
515
+ DEFAULT_MERGER_SCHEDULE = "55 */4 * * *";
516
+ DEFAULT_MERGER_MAX_RUNTIME = 1800;
517
+ DEFAULT_MERGER_MERGE_METHOD = "squash";
518
+ DEFAULT_MERGER_MIN_REVIEW_SCORE = 80;
519
+ DEFAULT_MERGER_REBASE_BEFORE_MERGE = true;
520
+ DEFAULT_MERGER_MAX_PRS_PER_RUN = 0;
521
+ DEFAULT_MERGER = {
522
+ enabled: DEFAULT_MERGER_ENABLED,
523
+ schedule: DEFAULT_MERGER_SCHEDULE,
524
+ maxRuntime: DEFAULT_MERGER_MAX_RUNTIME,
525
+ mergeMethod: DEFAULT_MERGER_MERGE_METHOD,
526
+ minReviewScore: DEFAULT_MERGER_MIN_REVIEW_SCORE,
527
+ branchPatterns: [],
528
+ rebaseBeforeMerge: DEFAULT_MERGER_REBASE_BEFORE_MERGE,
529
+ maxPrsPerRun: DEFAULT_MERGER_MAX_PRS_PER_RUN
530
+ };
531
+ MERGER_LOG_NAME = "merger";
461
532
  AUDIT_LOG_NAME = "audit";
462
533
  PLANNER_LOG_NAME = "slicer";
463
534
  ANALYTICS_LOG_NAME = "analytics";
@@ -734,7 +805,7 @@ function normalizeConfig(rawConfig) {
734
805
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
735
806
  normalized.autoMergeMethod = mergeMethod;
736
807
  }
737
- for (const jobId of ["qa", "audit", "analytics"]) {
808
+ for (const jobId of ["qa", "audit", "analytics", "merger"]) {
738
809
  const jobDef = getJobDef(jobId);
739
810
  if (!jobDef)
740
811
  continue;
@@ -1068,6 +1139,17 @@ function buildEnvOverrideConfig(fileConfig) {
1068
1139
  env.prResolver = overrides;
1069
1140
  }
1070
1141
  }
1142
+ const mergerDef = getJobDef("merger");
1143
+ if (mergerDef) {
1144
+ const currentBase = (
1145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1146
+ env.merger ?? fileConfig?.merger ?? mergerDef.defaultConfig
1147
+ );
1148
+ const overrides = buildJobEnvOverrides(mergerDef.envPrefix, currentBase, mergerDef.extraFields);
1149
+ if (overrides) {
1150
+ env.merger = overrides;
1151
+ }
1152
+ }
1071
1153
  const jobProvidersEnv = {};
1072
1154
  for (const jobType of VALID_JOB_TYPES) {
1073
1155
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -1163,6 +1245,7 @@ function getDefaultConfig() {
1163
1245
  audit: { ...DEFAULT_AUDIT },
1164
1246
  analytics: { ...DEFAULT_ANALYTICS },
1165
1247
  prResolver: { ...DEFAULT_PR_RESOLVER },
1248
+ merger: { ...DEFAULT_MERGER },
1166
1249
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
1167
1250
  providerScheduleOverrides: [...DEFAULT_PROVIDER_SCHEDULE_OVERRIDES],
1168
1251
  queue: { ...DEFAULT_QUEUE }
@@ -1230,7 +1313,7 @@ function mergeConfigLayer(base, layer) {
1230
1313
  ...layerQueue,
1231
1314
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
1232
1315
  };
1233
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "prResolver") {
1316
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "prResolver" || _key === "merger") {
1234
1317
  base[_key] = {
1235
1318
  ...base[_key],
1236
1319
  ...value
@@ -1253,6 +1336,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
1253
1336
  if (fileConfig)
1254
1337
  mergeConfigLayer(merged, fileConfig);
1255
1338
  mergeConfigLayer(merged, envConfig);
1339
+ if (merged.autoMerge === true && !fileConfig?.merger) {
1340
+ merged.merger = {
1341
+ ...merged.merger,
1342
+ enabled: true,
1343
+ mergeMethod: merged.autoMergeMethod ?? "squash"
1344
+ };
1345
+ }
1256
1346
  merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
1257
1347
  merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
1258
1348
  merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
@@ -1377,15 +1467,6 @@ var init_config = __esm({
1377
1467
  }
1378
1468
  });
1379
1469
 
1380
- // ../core/dist/board/types.js
1381
- var BOARD_COLUMNS;
1382
- var init_types2 = __esm({
1383
- "../core/dist/board/types.js"() {
1384
- "use strict";
1385
- BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
1386
- }
1387
- });
1388
-
1389
1470
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
1390
1471
  import Database from "better-sqlite3";
1391
1472
  import { inject, injectable } from "tsyringe";
@@ -3590,6 +3671,9 @@ function analyticsLockPath(projectDir) {
3590
3671
  function prResolverLockPath(projectDir) {
3591
3672
  return `${LOCK_FILE_PREFIX}pr-resolver-${projectRuntimeKey(projectDir)}.lock`;
3592
3673
  }
3674
+ function mergerLockPath(projectDir) {
3675
+ return `${LOCK_FILE_PREFIX}merger-${projectRuntimeKey(projectDir)}.lock`;
3676
+ }
3593
3677
  function isProcessRunning(pid) {
3594
3678
  try {
3595
3679
  process.kill(pid, 0);
@@ -3950,7 +4034,8 @@ function collectLogInfo(projectDir) {
3950
4034
  { name: "qa", fileName: `${QA_LOG_NAME}.log` },
3951
4035
  { name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
3952
4036
  { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
3953
- { name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
4037
+ { name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` },
4038
+ { name: "merger", fileName: `${MERGER_LOG_NAME}.log` }
3954
4039
  ];
3955
4040
  return logEntries.map(({ name, fileName }) => {
3956
4041
  const logPath = path6.join(projectDir, LOG_DIR, fileName);
@@ -3980,13 +4065,15 @@ async function fetchStatusSnapshot(projectDir, config) {
3980
4065
  const auditLock = checkLockFile(auditLockPath(projectDir));
3981
4066
  const plannerLock = checkLockFile(plannerLockPath(projectDir));
3982
4067
  const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
4068
+ const mergerLock = checkLockFile(mergerLockPath(projectDir));
3983
4069
  const processes = [
3984
4070
  { name: "executor", running: executorLock.running, pid: executorLock.pid },
3985
4071
  { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
3986
4072
  { name: "qa", running: qaLock.running, pid: qaLock.pid },
3987
4073
  { name: "audit", running: auditLock.running, pid: auditLock.pid },
3988
4074
  { name: "planner", running: plannerLock.running, pid: plannerLock.pid },
3989
- { name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid }
4075
+ { name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid },
4076
+ { name: "merger", running: mergerLock.running, pid: mergerLock.pid }
3990
4077
  ];
3991
4078
  const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
3992
4079
  const prs = await collectPrInfo(projectDir, config.branchPatterns);
@@ -4900,6 +4987,10 @@ function getEventEmoji(event) {
4900
4987
  return "\u2705";
4901
4988
  case "pr_resolver_failed":
4902
4989
  return "\u274C";
4990
+ case "merge_completed":
4991
+ return "\u{1F500}";
4992
+ case "merge_failed":
4993
+ return "\u274C";
4903
4994
  }
4904
4995
  }
4905
4996
  function getEventTitle(event) {
@@ -4930,6 +5021,10 @@ function getEventTitle(event) {
4930
5021
  return "PR Conflict Resolved";
4931
5022
  case "pr_resolver_failed":
4932
5023
  return "PR Resolver Failed";
5024
+ case "merge_completed":
5025
+ return "PR Merged";
5026
+ case "merge_failed":
5027
+ return "Merge Failed";
4933
5028
  }
4934
5029
  }
4935
5030
  function getEventColor(event) {
@@ -4960,6 +5055,10 @@ function getEventColor(event) {
4960
5055
  return 65280;
4961
5056
  case "pr_resolver_failed":
4962
5057
  return 16711680;
5058
+ case "merge_completed":
5059
+ return 10181046;
5060
+ case "merge_failed":
5061
+ return 16711680;
4963
5062
  }
4964
5063
  }
4965
5064
  function buildDescription(ctx) {
@@ -5562,19 +5661,96 @@ var init_roadmap_parser = __esm({
5562
5661
  }
5563
5662
  });
5564
5663
 
5565
- // ../core/dist/utils/roadmap-state.js
5664
+ // ../core/dist/audit/report.js
5566
5665
  import * as fs14 from "fs";
5567
5666
  import * as path13 from "path";
5667
+ function normalizeAuditSeverity(raw) {
5668
+ const normalized = raw.trim().toLowerCase();
5669
+ if (normalized === "critical")
5670
+ return "critical";
5671
+ if (normalized === "high")
5672
+ return "high";
5673
+ if (normalized === "low")
5674
+ return "low";
5675
+ return "medium";
5676
+ }
5677
+ function extractAuditField(block, field) {
5678
+ const pattern = new RegExp(`- \\*\\*${field}\\*\\*:\\s*([\\s\\S]*?)(?=\\n- \\*\\*|\\n###\\s+Finding\\s+\\d+|$)`, "i");
5679
+ const match = block.match(pattern);
5680
+ if (!match)
5681
+ return "";
5682
+ return match[1].replace(/`/g, "").replace(/\r/g, "").trim();
5683
+ }
5684
+ function parseAuditFindings(reportContent) {
5685
+ const headingRegex = /^###\s+Finding\s+(\d+)\s*$/gm;
5686
+ const headings = [];
5687
+ let match;
5688
+ while ((match = headingRegex.exec(reportContent)) !== null) {
5689
+ const number = parseInt(match[1], 10);
5690
+ if (!Number.isNaN(number)) {
5691
+ headings.push({
5692
+ number,
5693
+ bodyStart: headingRegex.lastIndex,
5694
+ headingStart: match.index
5695
+ });
5696
+ }
5697
+ }
5698
+ if (headings.length === 0) {
5699
+ return [];
5700
+ }
5701
+ const findings = [];
5702
+ for (let i = 0; i < headings.length; i++) {
5703
+ const current = headings[i];
5704
+ const next = headings[i + 1];
5705
+ const block = reportContent.slice(current.bodyStart, next?.headingStart ?? reportContent.length);
5706
+ const severityRaw = extractAuditField(block, "Severity");
5707
+ const category = extractAuditField(block, "Category") || "uncategorized";
5708
+ const location = extractAuditField(block, "Location") || "unknown location";
5709
+ const description = extractAuditField(block, "Description") || "No description provided";
5710
+ const snippet = extractAuditField(block, "Snippet");
5711
+ const suggestedFix = extractAuditField(block, "Suggested Fix") || "No suggested fix provided";
5712
+ findings.push({
5713
+ number: current.number,
5714
+ severity: normalizeAuditSeverity(severityRaw),
5715
+ category,
5716
+ location,
5717
+ description,
5718
+ snippet,
5719
+ suggestedFix
5720
+ });
5721
+ }
5722
+ return findings;
5723
+ }
5724
+ function loadAuditFindings(projectDir) {
5725
+ const reportPath = path13.join(projectDir, "logs", "audit-report.md");
5726
+ if (!fs14.existsSync(reportPath)) {
5727
+ return [];
5728
+ }
5729
+ const reportContent = fs14.readFileSync(reportPath, "utf-8");
5730
+ if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
5731
+ return [];
5732
+ }
5733
+ return parseAuditFindings(reportContent);
5734
+ }
5735
+ var init_report = __esm({
5736
+ "../core/dist/audit/report.js"() {
5737
+ "use strict";
5738
+ }
5739
+ });
5740
+
5741
+ // ../core/dist/utils/roadmap-state.js
5742
+ import * as fs15 from "fs";
5743
+ import * as path14 from "path";
5568
5744
  function getStateFilePath(prdDir) {
5569
- return path13.join(prdDir, STATE_FILE_NAME);
5745
+ return path14.join(prdDir, STATE_FILE_NAME);
5570
5746
  }
5571
5747
  function readJsonState(prdDir) {
5572
5748
  const statePath = getStateFilePath(prdDir);
5573
- if (!fs14.existsSync(statePath)) {
5749
+ if (!fs15.existsSync(statePath)) {
5574
5750
  return null;
5575
5751
  }
5576
5752
  try {
5577
- const content = fs14.readFileSync(statePath, "utf-8");
5753
+ const content = fs15.readFileSync(statePath, "utf-8");
5578
5754
  const parsed = JSON.parse(content);
5579
5755
  if (typeof parsed !== "object" || parsed === null) {
5580
5756
  return null;
@@ -5612,11 +5788,11 @@ function saveRoadmapState(prdDir, state) {
5612
5788
  const { roadmapState } = getRepositories();
5613
5789
  roadmapState.save(prdDir, state);
5614
5790
  const statePath = getStateFilePath(prdDir);
5615
- const dir = path13.dirname(statePath);
5616
- if (!fs14.existsSync(dir)) {
5617
- fs14.mkdirSync(dir, { recursive: true });
5791
+ const dir = path14.dirname(statePath);
5792
+ if (!fs15.existsSync(dir)) {
5793
+ fs15.mkdirSync(dir, { recursive: true });
5618
5794
  }
5619
- fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5795
+ fs15.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5620
5796
  }
5621
5797
  function createEmptyState() {
5622
5798
  return {
@@ -5656,16 +5832,16 @@ var init_roadmap_state = __esm({
5656
5832
  });
5657
5833
 
5658
5834
  // ../core/dist/templates/slicer-prompt.js
5659
- import * as fs15 from "fs";
5660
- import * as path14 from "path";
5835
+ import * as fs16 from "fs";
5836
+ import * as path15 from "path";
5661
5837
  import { fileURLToPath as fileURLToPath2 } from "url";
5662
5838
  function loadSlicerTemplate(templateDir) {
5663
5839
  if (cachedTemplate) {
5664
5840
  return cachedTemplate;
5665
5841
  }
5666
- const templatePath = templateDir ? path14.join(templateDir, "slicer.md") : path14.resolve(__dirname, "..", "..", "templates", "slicer.md");
5842
+ const templatePath = templateDir ? path15.join(templateDir, "slicer.md") : path15.resolve(__dirname, "..", "..", "templates", "slicer.md");
5667
5843
  try {
5668
- cachedTemplate = fs15.readFileSync(templatePath, "utf-8");
5844
+ cachedTemplate = fs16.readFileSync(templatePath, "utf-8");
5669
5845
  return cachedTemplate;
5670
5846
  } catch (error2) {
5671
5847
  console.warn(`Warning: Could not load slicer template from ${templatePath}, using default:`, error2 instanceof Error ? error2.message : String(error2));
@@ -5690,7 +5866,7 @@ function createSlicerPromptVars(title, section, description, prdDir, prdFilename
5690
5866
  title,
5691
5867
  section,
5692
5868
  description: description || "(No description provided)",
5693
- outputFilePath: path14.join(prdDir, prdFilename),
5869
+ outputFilePath: path15.join(prdDir, prdFilename),
5694
5870
  prdDir
5695
5871
  };
5696
5872
  }
@@ -5699,10 +5875,10 @@ var init_slicer_prompt = __esm({
5699
5875
  "../core/dist/templates/slicer-prompt.js"() {
5700
5876
  "use strict";
5701
5877
  __filename = fileURLToPath2(import.meta.url);
5702
- __dirname = path14.dirname(__filename);
5703
- 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.
5878
+ __dirname = path15.dirname(__filename);
5879
+ 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.
5704
5880
 
5705
- When this activates: \`PRD Creator: Initializing\`
5881
+ When this activates: \`Planning Mode: Principal Architect\`
5706
5882
 
5707
5883
  ---
5708
5884
 
@@ -5723,22 +5899,16 @@ The PRD directory is: \`{{PRD_DIR}}\`
5723
5899
 
5724
5900
  ## Your Task
5725
5901
 
5726
- 0. **Load Planner Skill** - Read and apply \`.claude/skills/prd-creator/SKILL.md\` before writing the PRD. If unavailable, continue with this template.
5727
-
5728
- 1. **Explore the Codebase** - Read relevant existing files to understand the project structure, patterns, and conventions.
5729
-
5730
- 2. **Assess Complexity** - Score the complexity using the rubric and determine whether this is LOW, MEDIUM, or HIGH complexity.
5731
-
5732
- 3. **Write a Complete PRD** - Create a full PRD following the prd-creator template structure with Context, Solution, Phases, Tests, and Acceptance Criteria.
5733
-
5734
- 4. **Write the PRD File** - Use the Write tool to create the PRD file at the exact path specified in \`{{OUTPUT_FILE_PATH}}\`.
5902
+ 1. **Explore the Codebase** \u2014 Read relevant existing files to understand structure, patterns, and conventions.
5903
+ 2. **Assess Complexity** \u2014 Score using the rubric below and determine LOW / MEDIUM / HIGH.
5904
+ 3. **Write a Complete PRD** \u2014 Follow the exact template structure below. Every section must be filled with concrete information.
5905
+ 4. **Write the PRD File** \u2014 Use the Write tool to create the PRD file at \`{{OUTPUT_FILE_PATH}}\`.
5735
5906
 
5736
5907
  ---
5737
5908
 
5738
5909
  ## Complexity Scoring
5739
5910
 
5740
5911
  \`\`\`
5741
- COMPLEXITY SCORE (sum all that apply):
5742
5912
  +1 Touches 1-5 files
5743
5913
  +2 Touches 6-10 files
5744
5914
  +3 Touches 10+ files
@@ -5750,7 +5920,7 @@ COMPLEXITY SCORE (sum all that apply):
5750
5920
 
5751
5921
  | Score | Level | Template Mode |
5752
5922
  | ----- | ------ | ----------------------------------------------- |
5753
- | 1-3 | LOW | Minimal (skip sections marked with MEDIUM/HIGH) |
5923
+ | 1-3 | LOW | Minimal (skip sections marked MEDIUM/HIGH) |
5754
5924
  | 4-6 | MEDIUM | Standard (all sections) |
5755
5925
  | 7+ | HIGH | Full + mandatory checkpoints every phase |
5756
5926
  \`\`\`
@@ -5759,25 +5929,77 @@ COMPLEXITY SCORE (sum all that apply):
5759
5929
 
5760
5930
  ## PRD Template Structure
5761
5931
 
5762
- Your PRD MUST follow this exact structure with these sections:
5763
- 1. **Context** - Problem, files analyzed, current behavior, integration points
5764
- 2. **Solution** - Approach, architecture diagram, key decisions, data changes
5765
- 3. **Sequence Flow** (MEDIUM/HIGH) - Mermaid sequence diagram
5766
- 4. **Execution Phases** - Concrete phases with files, implementation steps, and tests
5767
- 5. **Acceptance Criteria** - Checklist of completion requirements
5932
+ Your PRD MUST use this structure. Replace every [bracketed placeholder] with real content.
5933
+
5934
+ # PRD: [Title]
5935
+
5936
+ **Complexity: [SCORE] \u2192 [LEVEL] mode**
5937
+
5938
+ ## 1. Context
5939
+
5940
+ **Problem:** [1-2 sentences]
5941
+
5942
+ **Files Analyzed:**
5943
+ - \`path/to/file.ts\` \u2014 [what you found]
5944
+
5945
+ **Current Behavior:**
5946
+ - [3-5 bullets]
5947
+
5948
+ ### Integration Points
5949
+ - Entry point: [cron / CLI / event / route]
5950
+ - Caller file: [file invoking new code]
5951
+ - User flow: User does X \u2192 triggers Y \u2192 result Z
5952
+
5953
+ ## 2. Solution
5954
+
5955
+ **Approach:**
5956
+ - [3-5 bullets]
5957
+
5958
+ **Key Decisions:** [library choices, error handling, reused utilities]
5959
+
5960
+ **Data Changes:** [schema changes, or "None"]
5961
+
5962
+ ## 3. Sequence Flow (MEDIUM/HIGH only)
5963
+
5964
+ [mermaid sequenceDiagram]
5965
+
5966
+ ## 4. Execution Phases
5967
+
5968
+ ### Phase N: [Name] \u2014 [User-visible outcome]
5969
+
5970
+ **Files (max 5):**
5971
+ - \`src/path/file.ts\` \u2014 [what changes]
5972
+
5973
+ **Implementation:**
5974
+ - [ ] Step 1
5975
+
5976
+ **Tests Required:**
5977
+ | Test File | Test Name | Assertion |
5978
+ |-----------|-----------|-----------|
5979
+ | \`src/__tests__/feature.test.ts\` | \`should X when Y\` | \`expect(r).toBe(Z)\` |
5980
+
5981
+ **Checkpoint:** Run \`yarn verify\` and related tests after this phase.
5982
+
5983
+ ## 5. Acceptance Criteria
5984
+
5985
+ - [ ] All phases complete
5986
+ - [ ] All tests pass
5987
+ - [ ] \`yarn verify\` passes
5988
+ - [ ] Feature is reachable (not orphaned code)
5768
5989
 
5769
5990
  ---
5770
5991
 
5771
5992
  ## Critical Instructions
5772
5993
 
5773
- 1. **Read all relevant existing files BEFORE writing any code**
5774
- 2. **Follow existing patterns in the codebase**
5775
- 3. **Write the PRD with concrete file paths and implementation details**
5776
- 4. **Include specific test names and assertions**
5777
- 5. **Use the Write tool to create the PRD file at \`{{OUTPUT_FILE_PATH}}\`**
5778
- 6. **The PRD must be complete and actionable - no TODO placeholders**
5994
+ 1. Read all relevant files BEFORE writing the PRD
5995
+ 2. Follow existing patterns \u2014 use \`@/*\` path aliases, match naming conventions
5996
+ 3. Include concrete file paths and implementation steps
5997
+ 4. Include specific test names and assertions
5998
+ 5. Use the Write tool to create the file at \`{{OUTPUT_FILE_PATH}}\`
5999
+ 6. No placeholder text in the final PRD
6000
+ 7. The PRD is the GitHub issue body \u2014 make it self-contained
5779
6001
 
5780
- DO NOT leave placeholder text like "[Name]" or "[description]" in the final PRD.
6002
+ DO NOT leave [bracketed placeholder] text in the output.
5781
6003
  DO NOT skip any sections.
5782
6004
  DO NOT forget to write the file.
5783
6005
  `;
@@ -5786,65 +6008,10 @@ DO NOT forget to write the file.
5786
6008
  });
5787
6009
 
5788
6010
  // ../core/dist/utils/roadmap-scanner.js
5789
- import * as fs16 from "fs";
5790
- import * as path15 from "path";
6011
+ import * as fs17 from "fs";
6012
+ import * as path16 from "path";
5791
6013
  import { spawn } from "child_process";
5792
6014
  import { createHash as createHash3 } from "crypto";
5793
- function normalizeAuditSeverity(raw) {
5794
- const normalized = raw.trim().toLowerCase();
5795
- if (normalized === "critical")
5796
- return "critical";
5797
- if (normalized === "high")
5798
- return "high";
5799
- if (normalized === "low")
5800
- return "low";
5801
- return "medium";
5802
- }
5803
- function extractAuditField(block, field) {
5804
- const pattern = new RegExp(`- \\*\\*${field}\\*\\*:\\s*([\\s\\S]*?)(?=\\n- \\*\\*|\\n###\\s+Finding\\s+\\d+|$)`, "i");
5805
- const match = block.match(pattern);
5806
- if (!match)
5807
- return "";
5808
- return match[1].replace(/`/g, "").replace(/\r/g, "").trim();
5809
- }
5810
- function parseAuditFindings(reportContent) {
5811
- const headingRegex = /^###\s+Finding\s+(\d+)\s*$/gm;
5812
- const headings = [];
5813
- let match;
5814
- while ((match = headingRegex.exec(reportContent)) !== null) {
5815
- const number = parseInt(match[1], 10);
5816
- if (!Number.isNaN(number)) {
5817
- headings.push({
5818
- number,
5819
- bodyStart: headingRegex.lastIndex,
5820
- headingStart: match.index
5821
- });
5822
- }
5823
- }
5824
- if (headings.length === 0) {
5825
- return [];
5826
- }
5827
- const findings = [];
5828
- for (let i = 0; i < headings.length; i++) {
5829
- const current = headings[i];
5830
- const next = headings[i + 1];
5831
- const block = reportContent.slice(current.bodyStart, next?.headingStart ?? reportContent.length);
5832
- const severityRaw = extractAuditField(block, "Severity");
5833
- const category = extractAuditField(block, "Category") || "uncategorized";
5834
- const location = extractAuditField(block, "Location") || "unknown location";
5835
- const description = extractAuditField(block, "Description") || "No description provided";
5836
- const suggestedFix = extractAuditField(block, "Suggested Fix") || "No suggested fix provided";
5837
- findings.push({
5838
- number: current.number,
5839
- severity: normalizeAuditSeverity(severityRaw),
5840
- category,
5841
- location,
5842
- description,
5843
- suggestedFix
5844
- });
5845
- }
5846
- return findings;
5847
- }
5848
6015
  function auditSeverityRank(severity) {
5849
6016
  switch (severity) {
5850
6017
  case "critical":
@@ -5888,22 +6055,14 @@ function auditFindingToRoadmapItem(finding) {
5888
6055
  };
5889
6056
  }
5890
6057
  function collectAuditPlannerItems(projectDir) {
5891
- const reportPath = path15.join(projectDir, "logs", "audit-report.md");
5892
- if (!fs16.existsSync(reportPath)) {
5893
- return [];
5894
- }
5895
- const reportContent = fs16.readFileSync(reportPath, "utf-8");
5896
- if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
5897
- return [];
5898
- }
5899
- const findings = parseAuditFindings(reportContent);
6058
+ const findings = loadAuditFindings(projectDir);
5900
6059
  findings.sort((a, b) => auditSeverityRank(a.severity) - auditSeverityRank(b.severity) || a.number - b.number);
5901
6060
  return findings.map(auditFindingToRoadmapItem);
5902
6061
  }
5903
6062
  function getRoadmapStatus(projectDir, config) {
5904
- const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
6063
+ const roadmapPath = path16.join(projectDir, config.roadmapScanner.roadmapPath);
5905
6064
  const scannerEnabled = config.roadmapScanner.enabled;
5906
- if (!fs16.existsSync(roadmapPath)) {
6065
+ if (!fs17.existsSync(roadmapPath)) {
5907
6066
  return {
5908
6067
  found: false,
5909
6068
  enabled: scannerEnabled,
@@ -5914,9 +6073,9 @@ function getRoadmapStatus(projectDir, config) {
5914
6073
  items: []
5915
6074
  };
5916
6075
  }
5917
- const content = fs16.readFileSync(roadmapPath, "utf-8");
6076
+ const content = fs17.readFileSync(roadmapPath, "utf-8");
5918
6077
  const items = parseRoadmap(content);
5919
- const prdDir = path15.join(projectDir, config.prdDir);
6078
+ const prdDir = path16.join(projectDir, config.prdDir);
5920
6079
  const state = loadRoadmapState(prdDir);
5921
6080
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5922
6081
  const statusItems = items.map((item) => {
@@ -5953,10 +6112,10 @@ function getRoadmapStatus(projectDir, config) {
5953
6112
  }
5954
6113
  function scanExistingPrdSlugs(prdDir) {
5955
6114
  const slugs = /* @__PURE__ */ new Set();
5956
- if (!fs16.existsSync(prdDir)) {
6115
+ if (!fs17.existsSync(prdDir)) {
5957
6116
  return slugs;
5958
6117
  }
5959
- const files = fs16.readdirSync(prdDir);
6118
+ const files = fs17.readdirSync(prdDir);
5960
6119
  for (const file of files) {
5961
6120
  if (!file.endsWith(".md")) {
5962
6121
  continue;
@@ -5992,20 +6151,20 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5992
6151
  const nextNum = getNextPrdNumber(prdDir);
5993
6152
  const padded = String(nextNum).padStart(2, "0");
5994
6153
  const filename = `${padded}-${itemSlug}.md`;
5995
- const filePath = path15.join(prdDir, filename);
5996
- if (!fs16.existsSync(prdDir)) {
5997
- fs16.mkdirSync(prdDir, { recursive: true });
6154
+ const filePath = path16.join(prdDir, filename);
6155
+ if (!fs17.existsSync(prdDir)) {
6156
+ fs17.mkdirSync(prdDir, { recursive: true });
5998
6157
  }
5999
6158
  const promptVars = createSlicerPromptVars(item.title, item.section, item.description, prdDir, filename);
6000
6159
  const prompt = renderSlicerPrompt(promptVars);
6001
6160
  const provider = resolveJobProvider(config, "slicer");
6002
6161
  const providerArgs = buildProviderArgs(provider, prompt, projectDir);
6003
- const logDir = path15.join(projectDir, "logs");
6004
- if (!fs16.existsSync(logDir)) {
6005
- fs16.mkdirSync(logDir, { recursive: true });
6162
+ const logDir = path16.join(projectDir, "logs");
6163
+ if (!fs17.existsSync(logDir)) {
6164
+ fs17.mkdirSync(logDir, { recursive: true });
6006
6165
  }
6007
- const logFile = path15.join(logDir, `slicer-${itemSlug}.log`);
6008
- const logStream = fs16.createWriteStream(logFile, { flags: "w" });
6166
+ const logFile = path16.join(logDir, `slicer-${itemSlug}.log`);
6167
+ const logStream = fs17.createWriteStream(logFile, { flags: "w" });
6009
6168
  logStream.on("error", () => {
6010
6169
  });
6011
6170
  return new Promise((resolve9) => {
@@ -6042,7 +6201,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
6042
6201
  });
6043
6202
  return;
6044
6203
  }
6045
- if (!fs16.existsSync(filePath)) {
6204
+ if (!fs17.existsSync(filePath)) {
6046
6205
  resolve9({
6047
6206
  sliced: false,
6048
6207
  error: `Provider did not create expected file: ${filePath}`,
@@ -6065,23 +6224,23 @@ async function sliceNextItem(projectDir, config) {
6065
6224
  error: "Roadmap scanner is disabled"
6066
6225
  };
6067
6226
  }
6068
- const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
6227
+ const roadmapPath = path16.join(projectDir, config.roadmapScanner.roadmapPath);
6069
6228
  const auditItems = collectAuditPlannerItems(projectDir);
6070
- const roadmapExists = fs16.existsSync(roadmapPath);
6229
+ const roadmapExists = fs17.existsSync(roadmapPath);
6071
6230
  if (!roadmapExists && auditItems.length === 0) {
6072
6231
  return {
6073
6232
  sliced: false,
6074
6233
  error: "No pending items to process"
6075
6234
  };
6076
6235
  }
6077
- const roadmapItems = roadmapExists ? parseRoadmap(fs16.readFileSync(roadmapPath, "utf-8")) : [];
6236
+ const roadmapItems = roadmapExists ? parseRoadmap(fs17.readFileSync(roadmapPath, "utf-8")) : [];
6078
6237
  if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
6079
6238
  return {
6080
6239
  sliced: false,
6081
6240
  error: "No items in roadmap"
6082
6241
  };
6083
6242
  }
6084
- const prdDir = path15.join(projectDir, config.prdDir);
6243
+ const prdDir = path16.join(projectDir, config.prdDir);
6085
6244
  const state = loadRoadmapState(prdDir);
6086
6245
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
6087
6246
  const pickEligibleItem = (items) => {
@@ -6150,6 +6309,7 @@ function hasNewItems(projectDir, config) {
6150
6309
  var init_roadmap_scanner = __esm({
6151
6310
  "../core/dist/utils/roadmap-scanner.js"() {
6152
6311
  "use strict";
6312
+ init_report();
6153
6313
  init_config();
6154
6314
  init_prd_utils();
6155
6315
  init_roadmap_parser();
@@ -6252,8 +6412,8 @@ var init_shell = __esm({
6252
6412
  });
6253
6413
 
6254
6414
  // ../core/dist/utils/scheduling.js
6255
- import * as fs17 from "fs";
6256
- import * as path16 from "path";
6415
+ import * as fs18 from "fs";
6416
+ import * as path17 from "path";
6257
6417
  function normalizeSchedulingPriority(priority) {
6258
6418
  if (!Number.isFinite(priority)) {
6259
6419
  return DEFAULT_SCHEDULING_PRIORITY;
@@ -6288,7 +6448,7 @@ function getJobSchedule(config, jobType) {
6288
6448
  }
6289
6449
  }
6290
6450
  function loadPeerConfig(projectPath) {
6291
- if (!fs17.existsSync(projectPath) || !fs17.existsSync(path16.join(projectPath, CONFIG_FILE_NAME))) {
6451
+ if (!fs18.existsSync(projectPath) || !fs18.existsSync(path17.join(projectPath, CONFIG_FILE_NAME))) {
6292
6452
  return null;
6293
6453
  }
6294
6454
  try {
@@ -6299,10 +6459,10 @@ function loadPeerConfig(projectPath) {
6299
6459
  }
6300
6460
  function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6301
6461
  const peers = /* @__PURE__ */ new Map();
6302
- const currentPath = path16.resolve(currentProjectDir);
6462
+ const currentPath = path17.resolve(currentProjectDir);
6303
6463
  const currentSchedule = getJobSchedule(currentConfig, jobType);
6304
6464
  const addPeer = (projectPath, config) => {
6305
- const resolvedPath = path16.resolve(projectPath);
6465
+ const resolvedPath = path17.resolve(projectPath);
6306
6466
  if (!isJobTypeEnabled(config, jobType)) {
6307
6467
  return;
6308
6468
  }
@@ -6313,12 +6473,12 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6313
6473
  path: resolvedPath,
6314
6474
  config,
6315
6475
  schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
6316
- sortKey: `${path16.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
6476
+ sortKey: `${path17.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
6317
6477
  });
6318
6478
  };
6319
6479
  addPeer(currentPath, currentConfig);
6320
6480
  for (const entry of loadRegistry()) {
6321
- const resolvedPath = path16.resolve(entry.path);
6481
+ const resolvedPath = path17.resolve(entry.path);
6322
6482
  if (resolvedPath === currentPath || peers.has(resolvedPath)) {
6323
6483
  continue;
6324
6484
  }
@@ -6336,7 +6496,7 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6336
6496
  }
6337
6497
  function getSchedulingPlan(projectDir, config, jobType) {
6338
6498
  const peers = collectSchedulingPeers(projectDir, config, jobType);
6339
- const currentPath = path16.resolve(projectDir);
6499
+ const currentPath = path17.resolve(projectDir);
6340
6500
  const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
6341
6501
  const peerCount = Math.max(1, peers.length);
6342
6502
  const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
@@ -6428,8 +6588,8 @@ var init_webhook_validator = __esm({
6428
6588
 
6429
6589
  // ../core/dist/utils/worktree-manager.js
6430
6590
  import { execFileSync as execFileSync4 } from "child_process";
6431
- import * as fs18 from "fs";
6432
- import * as path17 from "path";
6591
+ import * as fs19 from "fs";
6592
+ import * as path18 from "path";
6433
6593
  function gitExec(args, cwd, logFile) {
6434
6594
  try {
6435
6595
  const result = execFileSync4("git", args, {
@@ -6439,7 +6599,7 @@ function gitExec(args, cwd, logFile) {
6439
6599
  });
6440
6600
  if (logFile && result) {
6441
6601
  try {
6442
- fs18.appendFileSync(logFile, result);
6602
+ fs19.appendFileSync(logFile, result);
6443
6603
  } catch {
6444
6604
  }
6445
6605
  }
@@ -6448,7 +6608,7 @@ function gitExec(args, cwd, logFile) {
6448
6608
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
6449
6609
  if (logFile) {
6450
6610
  try {
6451
- fs18.appendFileSync(logFile, errorMessage + "\n");
6611
+ fs19.appendFileSync(logFile, errorMessage + "\n");
6452
6612
  } catch {
6453
6613
  }
6454
6614
  }
@@ -6473,11 +6633,11 @@ function branchExistsRemotely(projectDir, branchName) {
6473
6633
  }
6474
6634
  function prepareBranchWorktree(options) {
6475
6635
  const { projectDir, worktreeDir, branchName, defaultBranch, logFile } = options;
6476
- if (fs18.existsSync(worktreeDir)) {
6636
+ if (fs19.existsSync(worktreeDir)) {
6477
6637
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
6478
6638
  if (!isRegistered) {
6479
6639
  try {
6480
- fs18.rmSync(worktreeDir, { recursive: true, force: true });
6640
+ fs19.rmSync(worktreeDir, { recursive: true, force: true });
6481
6641
  } catch {
6482
6642
  }
6483
6643
  }
@@ -6516,11 +6676,11 @@ function prepareBranchWorktree(options) {
6516
6676
  }
6517
6677
  function prepareDetachedWorktree(options) {
6518
6678
  const { projectDir, worktreeDir, defaultBranch, logFile } = options;
6519
- if (fs18.existsSync(worktreeDir)) {
6679
+ if (fs19.existsSync(worktreeDir)) {
6520
6680
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
6521
6681
  if (!isRegistered) {
6522
6682
  try {
6523
- fs18.rmSync(worktreeDir, { recursive: true, force: true });
6683
+ fs19.rmSync(worktreeDir, { recursive: true, force: true });
6524
6684
  } catch {
6525
6685
  }
6526
6686
  }
@@ -6562,7 +6722,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
6562
6722
  }
6563
6723
  }
6564
6724
  function cleanupWorktrees(projectDir, scope) {
6565
- const projectName = path17.basename(projectDir);
6725
+ const projectName = path18.basename(projectDir);
6566
6726
  const matchToken = scope ? scope : `${projectName}-nw`;
6567
6727
  const removed = [];
6568
6728
  try {
@@ -6598,11 +6758,11 @@ var init_worktree_manager = __esm({
6598
6758
 
6599
6759
  // ../core/dist/utils/job-queue.js
6600
6760
  import * as os7 from "os";
6601
- import * as path18 from "path";
6761
+ import * as path19 from "path";
6602
6762
  import Database7 from "better-sqlite3";
6603
6763
  function getStateDbPath() {
6604
- const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6605
- return path18.join(base, STATE_DB_FILE_NAME);
6764
+ const base = process.env.NIGHT_WATCH_HOME || path19.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6765
+ return path19.join(base, STATE_DB_FILE_NAME);
6606
6766
  }
6607
6767
  function openDb() {
6608
6768
  const dbPath = getStateDbPath();
@@ -6647,6 +6807,8 @@ function getLockPathForJob(projectPath, jobType) {
6647
6807
  return analyticsLockPath(projectPath);
6648
6808
  case "pr-resolver":
6649
6809
  return prResolverLockPath(projectPath);
6810
+ case "merger":
6811
+ return mergerLockPath(projectPath);
6650
6812
  }
6651
6813
  }
6652
6814
  function reconcileStaleRunningJobs(db) {
@@ -7361,9 +7523,9 @@ var init_amplitude_client = __esm({
7361
7523
  });
7362
7524
 
7363
7525
  // ../core/dist/analytics/analytics-runner.js
7364
- import * as fs19 from "fs";
7526
+ import * as fs20 from "fs";
7365
7527
  import * as os8 from "os";
7366
- import * as path19 from "path";
7528
+ import * as path20 from "path";
7367
7529
  function parseIssuesFromResponse(text) {
7368
7530
  const start = text.indexOf("[");
7369
7531
  const end = text.lastIndexOf("]");
@@ -7392,9 +7554,9 @@ async function runAnalytics(config, projectDir) {
7392
7554
 
7393
7555
  --- AMPLITUDE DATA ---
7394
7556
  ${JSON.stringify(data, null, 2)}`;
7395
- const tmpDir = fs19.mkdtempSync(path19.join(os8.tmpdir(), "nw-analytics-"));
7396
- const promptFile = path19.join(tmpDir, "analytics-prompt.md");
7397
- fs19.writeFileSync(promptFile, prompt, "utf-8");
7557
+ const tmpDir = fs20.mkdtempSync(path20.join(os8.tmpdir(), "nw-analytics-"));
7558
+ const promptFile = path20.join(tmpDir, "analytics-prompt.md");
7559
+ fs20.writeFileSync(promptFile, prompt, "utf-8");
7398
7560
  try {
7399
7561
  const provider = resolveJobProvider(config, "analytics");
7400
7562
  let providerCmd = PROVIDER_COMMANDS[provider];
@@ -7415,8 +7577,8 @@ set -euo pipefail
7415
7577
  ${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
7416
7578
  `;
7417
7579
  }
7418
- const scriptFile = path19.join(tmpDir, "run-analytics.sh");
7419
- fs19.writeFileSync(scriptFile, scriptContent, { mode: 493 });
7580
+ const scriptFile = path20.join(tmpDir, "run-analytics.sh");
7581
+ fs20.writeFileSync(scriptFile, scriptContent, { mode: 493 });
7420
7582
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
7421
7583
  if (exitCode !== 0) {
7422
7584
  throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
@@ -7463,7 +7625,7 @@ ${stderr}`;
7463
7625
  };
7464
7626
  } finally {
7465
7627
  try {
7466
- fs19.rmSync(tmpDir, { recursive: true, force: true });
7628
+ fs20.rmSync(tmpDir, { recursive: true, force: true });
7467
7629
  } catch {
7468
7630
  }
7469
7631
  }
@@ -7491,6 +7653,160 @@ var init_analytics = __esm({
7491
7653
  }
7492
7654
  });
7493
7655
 
7656
+ // ../core/dist/audit/board-sync.js
7657
+ function humanizeCategory(category) {
7658
+ return category.trim().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
7659
+ }
7660
+ function severityToPriorityLabel(severity) {
7661
+ switch (severity) {
7662
+ case "critical":
7663
+ return "P0";
7664
+ case "high":
7665
+ return "P1";
7666
+ default:
7667
+ return "P2";
7668
+ }
7669
+ }
7670
+ function truncateTitle(title, maxLength = 240) {
7671
+ const normalized = title.replace(/\s+/g, " ").trim();
7672
+ if (normalized.length <= maxLength) {
7673
+ return normalized;
7674
+ }
7675
+ return `${normalized.slice(0, maxLength - 1).trimEnd()}\u2026`;
7676
+ }
7677
+ function buildIssueTitle(finding) {
7678
+ return truncateTitle(`Audit: ${finding.severity} ${humanizeCategory(finding.category)} in ${finding.location}`);
7679
+ }
7680
+ function buildIssueBody(finding) {
7681
+ const lines = [
7682
+ "## Summary",
7683
+ "",
7684
+ `Night Watch audit detected a **${finding.severity}** finding in \`${finding.location}\`.`,
7685
+ "",
7686
+ "## Category",
7687
+ "",
7688
+ `\`${finding.category}\``,
7689
+ "",
7690
+ "## Description",
7691
+ "",
7692
+ finding.description,
7693
+ "",
7694
+ "## Suggested Fix",
7695
+ "",
7696
+ finding.suggestedFix
7697
+ ];
7698
+ if (finding.snippet) {
7699
+ lines.push("", "## Snippet", "", "```", finding.snippet, "```");
7700
+ }
7701
+ lines.push("", "## Source", "", "- Report: `logs/audit-report.md`", `- Finding: ${finding.number}`);
7702
+ return lines.join("\n");
7703
+ }
7704
+ async function syncAuditFindingsToBoard(config, projectDir) {
7705
+ const findings = loadAuditFindings(projectDir);
7706
+ const targetColumn = config.audit.targetColumn;
7707
+ if (findings.length === 0) {
7708
+ return {
7709
+ status: "skipped",
7710
+ findingsCount: 0,
7711
+ issuesCreated: 0,
7712
+ issuesFailed: 0,
7713
+ targetColumn: null,
7714
+ summary: "no actionable audit findings to sync"
7715
+ };
7716
+ }
7717
+ if (!config.boardProvider.enabled) {
7718
+ return {
7719
+ status: "skipped",
7720
+ findingsCount: findings.length,
7721
+ issuesCreated: 0,
7722
+ issuesFailed: 0,
7723
+ targetColumn: null,
7724
+ summary: `found ${findings.length} actionable audit finding(s); board sync skipped because board provider is disabled`
7725
+ };
7726
+ }
7727
+ let boardProvider;
7728
+ try {
7729
+ boardProvider = createBoardProvider(config.boardProvider, projectDir);
7730
+ } catch (err) {
7731
+ const message = err instanceof Error ? err.message : String(err);
7732
+ logger5.error("Failed to create board provider for audit sync", { error: message });
7733
+ return {
7734
+ status: "failed",
7735
+ findingsCount: findings.length,
7736
+ issuesCreated: 0,
7737
+ issuesFailed: findings.length,
7738
+ targetColumn,
7739
+ summary: `found ${findings.length} actionable audit finding(s), but board sync failed: ${message}`
7740
+ };
7741
+ }
7742
+ let created = 0;
7743
+ let failed = 0;
7744
+ for (const finding of findings) {
7745
+ try {
7746
+ await boardProvider.createIssue({
7747
+ title: buildIssueTitle(finding),
7748
+ body: buildIssueBody(finding),
7749
+ column: targetColumn,
7750
+ labels: [severityToPriorityLabel(finding.severity)]
7751
+ });
7752
+ created++;
7753
+ } catch (err) {
7754
+ failed++;
7755
+ logger5.error("Failed to create board issue for audit finding", {
7756
+ finding: finding.number,
7757
+ error: err instanceof Error ? err.message : String(err)
7758
+ });
7759
+ }
7760
+ }
7761
+ if (failed === 0) {
7762
+ return {
7763
+ status: "success",
7764
+ findingsCount: findings.length,
7765
+ issuesCreated: created,
7766
+ issuesFailed: 0,
7767
+ targetColumn,
7768
+ summary: `created ${created} audit issue(s) in ${targetColumn}`
7769
+ };
7770
+ }
7771
+ if (created === 0) {
7772
+ return {
7773
+ status: "failed",
7774
+ findingsCount: findings.length,
7775
+ issuesCreated: 0,
7776
+ issuesFailed: failed,
7777
+ targetColumn,
7778
+ summary: `found ${findings.length} actionable audit finding(s), but failed to create board issue(s)`
7779
+ };
7780
+ }
7781
+ return {
7782
+ status: "partial",
7783
+ findingsCount: findings.length,
7784
+ issuesCreated: created,
7785
+ issuesFailed: failed,
7786
+ targetColumn,
7787
+ summary: `created ${created} of ${findings.length} audit issue(s) in ${targetColumn} (${failed} failed)`
7788
+ };
7789
+ }
7790
+ var logger5;
7791
+ var init_board_sync = __esm({
7792
+ "../core/dist/audit/board-sync.js"() {
7793
+ "use strict";
7794
+ init_factory();
7795
+ init_logger();
7796
+ init_report();
7797
+ logger5 = createLogger("audit-sync");
7798
+ }
7799
+ });
7800
+
7801
+ // ../core/dist/audit/index.js
7802
+ var init_audit = __esm({
7803
+ "../core/dist/audit/index.js"() {
7804
+ "use strict";
7805
+ init_report();
7806
+ init_board_sync();
7807
+ }
7808
+ });
7809
+
7494
7810
  // ../core/dist/templates/prd-template.js
7495
7811
  function renderDependsOn(deps) {
7496
7812
  if (deps.length === 0) {
@@ -7698,6 +8014,7 @@ __export(dist_exports, {
7698
8014
  DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
7699
8015
  DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
7700
8016
  DEFAULT_AUDIT_SCHEDULE: () => DEFAULT_AUDIT_SCHEDULE,
8017
+ DEFAULT_AUDIT_TARGET_COLUMN: () => DEFAULT_AUDIT_TARGET_COLUMN,
7701
8018
  DEFAULT_AUTO_MERGE: () => DEFAULT_AUTO_MERGE,
7702
8019
  DEFAULT_AUTO_MERGE_METHOD: () => DEFAULT_AUTO_MERGE_METHOD,
7703
8020
  DEFAULT_BOARD_PROVIDER: () => DEFAULT_BOARD_PROVIDER,
@@ -7714,6 +8031,14 @@ __export(dist_exports, {
7714
8031
  DEFAULT_MAX_LOG_SIZE: () => DEFAULT_MAX_LOG_SIZE,
7715
8032
  DEFAULT_MAX_RETRIES: () => DEFAULT_MAX_RETRIES,
7716
8033
  DEFAULT_MAX_RUNTIME: () => DEFAULT_MAX_RUNTIME,
8034
+ DEFAULT_MERGER: () => DEFAULT_MERGER,
8035
+ DEFAULT_MERGER_ENABLED: () => DEFAULT_MERGER_ENABLED,
8036
+ DEFAULT_MERGER_MAX_PRS_PER_RUN: () => DEFAULT_MERGER_MAX_PRS_PER_RUN,
8037
+ DEFAULT_MERGER_MAX_RUNTIME: () => DEFAULT_MERGER_MAX_RUNTIME,
8038
+ DEFAULT_MERGER_MERGE_METHOD: () => DEFAULT_MERGER_MERGE_METHOD,
8039
+ DEFAULT_MERGER_MIN_REVIEW_SCORE: () => DEFAULT_MERGER_MIN_REVIEW_SCORE,
8040
+ DEFAULT_MERGER_REBASE_BEFORE_MERGE: () => DEFAULT_MERGER_REBASE_BEFORE_MERGE,
8041
+ DEFAULT_MERGER_SCHEDULE: () => DEFAULT_MERGER_SCHEDULE,
7717
8042
  DEFAULT_MIN_REVIEW_SCORE: () => DEFAULT_MIN_REVIEW_SCORE,
7718
8043
  DEFAULT_NOTIFICATIONS: () => DEFAULT_NOTIFICATIONS,
7719
8044
  DEFAULT_PRD_DIR: () => DEFAULT_PRD_DIR,
@@ -7772,6 +8097,7 @@ __export(dist_exports, {
7772
8097
  LocalKanbanProvider: () => LocalKanbanProvider,
7773
8098
  Logger: () => Logger,
7774
8099
  MAX_HISTORY_RECORDS_PER_PRD: () => MAX_HISTORY_RECORDS_PER_PRD,
8100
+ MERGER_LOG_NAME: () => MERGER_LOG_NAME,
7775
8101
  NIGHT_WATCH_LABELS: () => NIGHT_WATCH_LABELS,
7776
8102
  PLANNER_LOG_NAME: () => PLANNER_LOG_NAME,
7777
8103
  PRD_STATES_FILE_NAME: () => PRD_STATES_FILE_NAME,
@@ -7927,6 +8253,7 @@ __export(dist_exports, {
7927
8253
  isValidPriority: () => isValidPriority,
7928
8254
  label: () => label,
7929
8255
  listPrdStatesByStatus: () => listPrdStatesByStatus,
8256
+ loadAuditFindings: () => loadAuditFindings,
7930
8257
  loadConfig: () => loadConfig,
7931
8258
  loadGlobalNotificationsConfig: () => loadGlobalNotificationsConfig,
7932
8259
  loadHistory: () => loadHistory,
@@ -7936,9 +8263,12 @@ __export(dist_exports, {
7936
8263
  markItemProcessed: () => markItemProcessed,
7937
8264
  markJobRunning: () => markJobRunning,
7938
8265
  markPrdDone: () => markPrdDone,
8266
+ mergerLockPath: () => mergerLockPath,
7939
8267
  migrateJsonToSqlite: () => migrateJsonToSqlite,
8268
+ normalizeAuditSeverity: () => normalizeAuditSeverity,
7940
8269
  normalizeJobConfig: () => normalizeJobConfig,
7941
8270
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
8271
+ parseAuditFindings: () => parseAuditFindings,
7942
8272
  parsePrdDependencies: () => parsePrdDependencies,
7943
8273
  parseRoadmap: () => parseRoadmap,
7944
8274
  parseScriptResult: () => parseScriptResult,
@@ -7988,6 +8318,7 @@ __export(dist_exports, {
7988
8318
  sortByPriority: () => sortByPriority,
7989
8319
  step: () => step,
7990
8320
  success: () => success,
8321
+ syncAuditFindingsToBoard: () => syncAuditFindingsToBoard,
7991
8322
  unmarkItemProcessed: () => unmarkItemProcessed,
7992
8323
  unregisterProject: () => unregisterProject,
7993
8324
  updateJobStatus: () => updateJobStatus,
@@ -8044,6 +8375,7 @@ var init_dist = __esm({
8044
8375
  init_job_queue();
8045
8376
  init_summary();
8046
8377
  init_analytics();
8378
+ init_audit();
8047
8379
  init_prd_template();
8048
8380
  init_slicer_prompt();
8049
8381
  init_jobs();
@@ -8053,30 +8385,30 @@ var init_dist = __esm({
8053
8385
  // src/cli.ts
8054
8386
  import "reflect-metadata";
8055
8387
  import { Command as Command3 } from "commander";
8056
- import { existsSync as existsSync30, readFileSync as readFileSync19 } from "fs";
8388
+ import { existsSync as existsSync31, readFileSync as readFileSync20 } from "fs";
8057
8389
  import { fileURLToPath as fileURLToPath6 } from "url";
8058
- import { dirname as dirname12, join as join36 } from "path";
8390
+ import { dirname as dirname12, join as join37 } from "path";
8059
8391
 
8060
8392
  // src/commands/init.ts
8061
8393
  init_dist();
8062
- import fs20 from "fs";
8063
- import path20 from "path";
8394
+ import fs21 from "fs";
8395
+ import path21 from "path";
8064
8396
  import { execSync as execSync3 } from "child_process";
8065
8397
  import { fileURLToPath as fileURLToPath3 } from "url";
8066
- import { dirname as dirname6, join as join18 } from "path";
8398
+ import { dirname as dirname6, join as join19 } from "path";
8067
8399
  import * as readline from "readline";
8068
8400
  var __filename2 = fileURLToPath3(import.meta.url);
8069
8401
  var __dirname2 = dirname6(__filename2);
8070
8402
  function findTemplatesDir(startDir) {
8071
8403
  let d = startDir;
8072
8404
  for (let i = 0; i < 8; i++) {
8073
- const candidate = join18(d, "templates");
8074
- if (fs20.existsSync(candidate) && fs20.statSync(candidate).isDirectory()) {
8405
+ const candidate = join19(d, "templates");
8406
+ if (fs21.existsSync(candidate) && fs21.statSync(candidate).isDirectory()) {
8075
8407
  return candidate;
8076
8408
  }
8077
8409
  d = dirname6(d);
8078
8410
  }
8079
- return join18(startDir, "templates");
8411
+ return join19(startDir, "templates");
8080
8412
  }
8081
8413
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
8082
8414
  var NW_SKILLS = [
@@ -8088,12 +8420,12 @@ var NW_SKILLS = [
8088
8420
  "nw-review"
8089
8421
  ];
8090
8422
  function hasPlaywrightDependency(cwd) {
8091
- const packageJsonPath = path20.join(cwd, "package.json");
8092
- if (!fs20.existsSync(packageJsonPath)) {
8423
+ const packageJsonPath = path21.join(cwd, "package.json");
8424
+ if (!fs21.existsSync(packageJsonPath)) {
8093
8425
  return false;
8094
8426
  }
8095
8427
  try {
8096
- const packageJson2 = JSON.parse(fs20.readFileSync(packageJsonPath, "utf-8"));
8428
+ const packageJson2 = JSON.parse(fs21.readFileSync(packageJsonPath, "utf-8"));
8097
8429
  return Boolean(
8098
8430
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
8099
8431
  );
@@ -8105,7 +8437,7 @@ function detectPlaywright(cwd) {
8105
8437
  if (hasPlaywrightDependency(cwd)) {
8106
8438
  return true;
8107
8439
  }
8108
- if (fs20.existsSync(path20.join(cwd, "node_modules", ".bin", "playwright"))) {
8440
+ if (fs21.existsSync(path21.join(cwd, "node_modules", ".bin", "playwright"))) {
8109
8441
  return true;
8110
8442
  }
8111
8443
  try {
@@ -8121,10 +8453,10 @@ function detectPlaywright(cwd) {
8121
8453
  }
8122
8454
  }
8123
8455
  function resolvePlaywrightInstallCommand(cwd) {
8124
- if (fs20.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) {
8456
+ if (fs21.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) {
8125
8457
  return "pnpm add -D @playwright/test";
8126
8458
  }
8127
- if (fs20.existsSync(path20.join(cwd, "yarn.lock"))) {
8459
+ if (fs21.existsSync(path21.join(cwd, "yarn.lock"))) {
8128
8460
  return "yarn add -D @playwright/test";
8129
8461
  }
8130
8462
  return "npm install -D @playwright/test";
@@ -8270,8 +8602,8 @@ function promptProviderSelection(providers) {
8270
8602
  });
8271
8603
  }
8272
8604
  function ensureDir(dirPath) {
8273
- if (!fs20.existsSync(dirPath)) {
8274
- fs20.mkdirSync(dirPath, { recursive: true });
8605
+ if (!fs21.existsSync(dirPath)) {
8606
+ fs21.mkdirSync(dirPath, { recursive: true });
8275
8607
  }
8276
8608
  }
8277
8609
  function buildInitConfig(params) {
@@ -8319,6 +8651,7 @@ function buildInitConfig(params) {
8319
8651
  },
8320
8652
  audit: { ...defaults.audit },
8321
8653
  analytics: { ...defaults.analytics },
8654
+ merger: { ...defaults.merger },
8322
8655
  prResolver: { ...defaults.prResolver },
8323
8656
  jobProviders: { ...defaults.jobProviders },
8324
8657
  queue: {
@@ -8329,30 +8662,30 @@ function buildInitConfig(params) {
8329
8662
  }
8330
8663
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
8331
8664
  if (customTemplatesDir !== null) {
8332
- const customPath = join18(customTemplatesDir, templateName);
8333
- if (fs20.existsSync(customPath)) {
8665
+ const customPath = join19(customTemplatesDir, templateName);
8666
+ if (fs21.existsSync(customPath)) {
8334
8667
  return { path: customPath, source: "custom" };
8335
8668
  }
8336
8669
  }
8337
- return { path: join18(bundledTemplatesDir, templateName), source: "bundled" };
8670
+ return { path: join19(bundledTemplatesDir, templateName), source: "bundled" };
8338
8671
  }
8339
8672
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
8340
- if (fs20.existsSync(targetPath) && !force) {
8673
+ if (fs21.existsSync(targetPath) && !force) {
8341
8674
  console.log(` Skipped (exists): ${targetPath}`);
8342
8675
  return { created: false, source: source ?? "bundled" };
8343
8676
  }
8344
- const templatePath = sourcePath ?? join18(TEMPLATES_DIR, templateName);
8677
+ const templatePath = sourcePath ?? join19(TEMPLATES_DIR, templateName);
8345
8678
  const resolvedSource = source ?? "bundled";
8346
- let content = fs20.readFileSync(templatePath, "utf-8");
8679
+ let content = fs21.readFileSync(templatePath, "utf-8");
8347
8680
  for (const [key, value] of Object.entries(replacements)) {
8348
8681
  content = content.replaceAll(key, value);
8349
8682
  }
8350
- fs20.writeFileSync(targetPath, content);
8683
+ fs21.writeFileSync(targetPath, content);
8351
8684
  console.log(` Created: ${targetPath} (${resolvedSource})`);
8352
8685
  return { created: true, source: resolvedSource };
8353
8686
  }
8354
8687
  function addToGitignore(cwd) {
8355
- const gitignorePath = path20.join(cwd, ".gitignore");
8688
+ const gitignorePath = path21.join(cwd, ".gitignore");
8356
8689
  const entries = [
8357
8690
  {
8358
8691
  pattern: "/logs/",
@@ -8366,13 +8699,13 @@ function addToGitignore(cwd) {
8366
8699
  },
8367
8700
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
8368
8701
  ];
8369
- if (!fs20.existsSync(gitignorePath)) {
8702
+ if (!fs21.existsSync(gitignorePath)) {
8370
8703
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
8371
- fs20.writeFileSync(gitignorePath, lines.join("\n"));
8704
+ fs21.writeFileSync(gitignorePath, lines.join("\n"));
8372
8705
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
8373
8706
  return;
8374
8707
  }
8375
- const content = fs20.readFileSync(gitignorePath, "utf-8");
8708
+ const content = fs21.readFileSync(gitignorePath, "utf-8");
8376
8709
  const missing = entries.filter((e) => !e.check(content));
8377
8710
  if (missing.length === 0) {
8378
8711
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -8380,59 +8713,59 @@ function addToGitignore(cwd) {
8380
8713
  }
8381
8714
  const additions = missing.map((e) => e.pattern).join("\n");
8382
8715
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
8383
- fs20.writeFileSync(gitignorePath, newContent);
8716
+ fs21.writeFileSync(gitignorePath, newContent);
8384
8717
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
8385
8718
  }
8386
8719
  function installSkills(cwd, provider, force, templatesDir) {
8387
- const skillsTemplatesDir = path20.join(templatesDir, "skills");
8388
- if (!fs20.existsSync(skillsTemplatesDir)) {
8720
+ const skillsTemplatesDir = path21.join(templatesDir, "skills");
8721
+ if (!fs21.existsSync(skillsTemplatesDir)) {
8389
8722
  return { location: "", installed: 0, skipped: 0, type: "none" };
8390
8723
  }
8391
8724
  const isClaudeProvider = provider === "claude" || provider.startsWith("claude");
8392
8725
  const isCodexProvider = provider === "codex";
8393
- const claudeDir = path20.join(cwd, ".claude");
8394
- if (isClaudeProvider || fs20.existsSync(claudeDir)) {
8726
+ const claudeDir = path21.join(cwd, ".claude");
8727
+ if (isClaudeProvider || fs21.existsSync(claudeDir)) {
8395
8728
  ensureDir(claudeDir);
8396
- const skillsDir = path20.join(claudeDir, "skills");
8729
+ const skillsDir = path21.join(claudeDir, "skills");
8397
8730
  ensureDir(skillsDir);
8398
8731
  let installed = 0;
8399
8732
  let skipped = 0;
8400
8733
  for (const skillName of NW_SKILLS) {
8401
- const templateFile = path20.join(skillsTemplatesDir, `${skillName}.md`);
8402
- if (!fs20.existsSync(templateFile)) continue;
8403
- const skillDir = path20.join(skillsDir, skillName);
8734
+ const templateFile = path21.join(skillsTemplatesDir, `${skillName}.md`);
8735
+ if (!fs21.existsSync(templateFile)) continue;
8736
+ const skillDir = path21.join(skillsDir, skillName);
8404
8737
  ensureDir(skillDir);
8405
- const target = path20.join(skillDir, "SKILL.md");
8406
- if (fs20.existsSync(target) && !force) {
8738
+ const target = path21.join(skillDir, "SKILL.md");
8739
+ if (fs21.existsSync(target) && !force) {
8407
8740
  skipped++;
8408
8741
  continue;
8409
8742
  }
8410
- fs20.copyFileSync(templateFile, target);
8743
+ fs21.copyFileSync(templateFile, target);
8411
8744
  installed++;
8412
8745
  }
8413
8746
  return { location: ".claude/skills/", installed, skipped, type: "claude" };
8414
8747
  }
8415
8748
  if (isCodexProvider) {
8416
- const agentsFile = path20.join(cwd, "AGENTS.md");
8417
- const blockFile = path20.join(skillsTemplatesDir, "_codex-block.md");
8418
- if (!fs20.existsSync(blockFile)) {
8749
+ const agentsFile = path21.join(cwd, "AGENTS.md");
8750
+ const blockFile = path21.join(skillsTemplatesDir, "_codex-block.md");
8751
+ if (!fs21.existsSync(blockFile)) {
8419
8752
  return { location: "", installed: 0, skipped: 0, type: "none" };
8420
8753
  }
8421
- const block = fs20.readFileSync(blockFile, "utf-8");
8754
+ const block = fs21.readFileSync(blockFile, "utf-8");
8422
8755
  const marker = "## Night Watch Skills";
8423
- if (!fs20.existsSync(agentsFile)) {
8424
- fs20.writeFileSync(agentsFile, block);
8756
+ if (!fs21.existsSync(agentsFile)) {
8757
+ fs21.writeFileSync(agentsFile, block);
8425
8758
  return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
8426
8759
  }
8427
- const existing = fs20.readFileSync(agentsFile, "utf-8");
8760
+ const existing = fs21.readFileSync(agentsFile, "utf-8");
8428
8761
  if (existing.includes(marker)) {
8429
8762
  if (!force) {
8430
8763
  return { location: "AGENTS.md", installed: 0, skipped: NW_SKILLS.length, type: "codex" };
8431
8764
  }
8432
8765
  const withoutSection = existing.replace(/\n\n## Night Watch Skills[\s\S]*$/, "");
8433
- fs20.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
8766
+ fs21.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
8434
8767
  } else {
8435
- fs20.appendFileSync(agentsFile, "\n\n" + block);
8768
+ fs21.appendFileSync(agentsFile, "\n\n" + block);
8436
8769
  }
8437
8770
  return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
8438
8771
  }
@@ -8556,28 +8889,28 @@ function initCommand(program2) {
8556
8889
  "${DEFAULT_BRANCH}": defaultBranch
8557
8890
  };
8558
8891
  step(6, totalSteps, "Creating PRD directory structure...");
8559
- const prdDirPath = path20.join(cwd, prdDir);
8560
- const doneDirPath = path20.join(prdDirPath, "done");
8892
+ const prdDirPath = path21.join(cwd, prdDir);
8893
+ const doneDirPath = path21.join(prdDirPath, "done");
8561
8894
  ensureDir(doneDirPath);
8562
8895
  success(`Created ${prdDirPath}/`);
8563
8896
  success(`Created ${doneDirPath}/`);
8564
8897
  step(7, totalSteps, "Creating logs directory...");
8565
- const logsPath = path20.join(cwd, LOG_DIR);
8898
+ const logsPath = path21.join(cwd, LOG_DIR);
8566
8899
  ensureDir(logsPath);
8567
8900
  success(`Created ${logsPath}/`);
8568
8901
  addToGitignore(cwd);
8569
8902
  step(8, totalSteps, "Creating instructions directory...");
8570
- const instructionsDir = path20.join(cwd, "instructions");
8903
+ const instructionsDir = path21.join(cwd, "instructions");
8571
8904
  ensureDir(instructionsDir);
8572
8905
  success(`Created ${instructionsDir}/`);
8573
8906
  const existingConfig = loadConfig(cwd);
8574
- const customTemplatesDirPath = path20.join(cwd, existingConfig.templatesDir);
8575
- const customTemplatesDir = fs20.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
8907
+ const customTemplatesDirPath = path21.join(cwd, existingConfig.templatesDir);
8908
+ const customTemplatesDir = fs21.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
8576
8909
  const templateSources = [];
8577
8910
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
8578
8911
  const nwResult = processTemplate(
8579
8912
  "executor.md",
8580
- path20.join(instructionsDir, "executor.md"),
8913
+ path21.join(instructionsDir, "executor.md"),
8581
8914
  replacements,
8582
8915
  force,
8583
8916
  nwResolution.path,
@@ -8591,7 +8924,7 @@ function initCommand(program2) {
8591
8924
  );
8592
8925
  const peResult = processTemplate(
8593
8926
  "prd-executor.md",
8594
- path20.join(instructionsDir, "prd-executor.md"),
8927
+ path21.join(instructionsDir, "prd-executor.md"),
8595
8928
  replacements,
8596
8929
  force,
8597
8930
  peResolution.path,
@@ -8601,7 +8934,7 @@ function initCommand(program2) {
8601
8934
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
8602
8935
  const prResult = processTemplate(
8603
8936
  "pr-reviewer.md",
8604
- path20.join(instructionsDir, "pr-reviewer.md"),
8937
+ path21.join(instructionsDir, "pr-reviewer.md"),
8605
8938
  replacements,
8606
8939
  force,
8607
8940
  prResolution.path,
@@ -8611,7 +8944,7 @@ function initCommand(program2) {
8611
8944
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
8612
8945
  const qaResult = processTemplate(
8613
8946
  "qa.md",
8614
- path20.join(instructionsDir, "qa.md"),
8947
+ path21.join(instructionsDir, "qa.md"),
8615
8948
  replacements,
8616
8949
  force,
8617
8950
  qaResolution.path,
@@ -8621,7 +8954,7 @@ function initCommand(program2) {
8621
8954
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
8622
8955
  const auditResult = processTemplate(
8623
8956
  "audit.md",
8624
- path20.join(instructionsDir, "audit.md"),
8957
+ path21.join(instructionsDir, "audit.md"),
8625
8958
  replacements,
8626
8959
  force,
8627
8960
  auditResolution.path,
@@ -8635,7 +8968,7 @@ function initCommand(program2) {
8635
8968
  );
8636
8969
  const plannerResult = processTemplate(
8637
8970
  "prd-creator.md",
8638
- path20.join(instructionsDir, "prd-creator.md"),
8971
+ path21.join(instructionsDir, "prd-creator.md"),
8639
8972
  replacements,
8640
8973
  force,
8641
8974
  plannerResolution.path,
@@ -8643,8 +8976,8 @@ function initCommand(program2) {
8643
8976
  );
8644
8977
  templateSources.push({ name: "prd-creator.md", source: plannerResult.source });
8645
8978
  step(9, totalSteps, "Creating configuration file...");
8646
- const configPath = path20.join(cwd, CONFIG_FILE_NAME);
8647
- if (fs20.existsSync(configPath) && !force) {
8979
+ const configPath = path21.join(cwd, CONFIG_FILE_NAME);
8980
+ if (fs21.existsSync(configPath) && !force) {
8648
8981
  console.log(` Skipped (exists): ${configPath}`);
8649
8982
  } else {
8650
8983
  const config = buildInitConfig({
@@ -8654,11 +8987,11 @@ function initCommand(program2) {
8654
8987
  reviewerEnabled,
8655
8988
  prdDir
8656
8989
  });
8657
- fs20.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
8990
+ fs21.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
8658
8991
  success(`Created ${configPath}`);
8659
8992
  }
8660
8993
  step(10, totalSteps, "Setting up GitHub Project board...");
8661
- const existingRaw = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
8994
+ const existingRaw = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
8662
8995
  const existingBoard = existingRaw.boardProvider;
8663
8996
  let boardSetupStatus = "Skipped";
8664
8997
  if (existingBoard?.projectNumber && !force) {
@@ -8680,13 +9013,13 @@ function initCommand(program2) {
8680
9013
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
8681
9014
  const boardTitle = `${projectName} Night Watch`;
8682
9015
  const board = await provider.setupBoard(boardTitle);
8683
- const rawConfig = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
9016
+ const rawConfig = JSON.parse(fs21.readFileSync(configPath, "utf-8"));
8684
9017
  rawConfig.boardProvider = {
8685
9018
  enabled: true,
8686
9019
  provider: "github",
8687
9020
  projectNumber: board.number
8688
9021
  };
8689
- fs20.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
9022
+ fs21.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
8690
9023
  boardSetupStatus = `Created (#${board.number})`;
8691
9024
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
8692
9025
  } catch (boardErr) {
@@ -8869,8 +9202,8 @@ function getTelegramStatusWebhooks(config) {
8869
9202
  }
8870
9203
 
8871
9204
  // src/commands/run.ts
8872
- import * as fs21 from "fs";
8873
- import * as path21 from "path";
9205
+ import * as fs22 from "fs";
9206
+ import * as path22 from "path";
8874
9207
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
8875
9208
  if (exitCode === 124) {
8876
9209
  return "run_timeout";
@@ -8902,12 +9235,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
8902
9235
  return scriptStatus === "skip_no_eligible_prd";
8903
9236
  }
8904
9237
  function getCrossProjectFallbackCandidates(currentProjectDir) {
8905
- const current = path21.resolve(currentProjectDir);
9238
+ const current = path22.resolve(currentProjectDir);
8906
9239
  const { valid, invalid } = validateRegistry();
8907
9240
  for (const entry of invalid) {
8908
9241
  warn(`Skipping invalid registry entry: ${entry.path}`);
8909
9242
  }
8910
- return valid.filter((entry) => path21.resolve(entry.path) !== current);
9243
+ return valid.filter((entry) => path22.resolve(entry.path) !== current);
8911
9244
  }
8912
9245
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
8913
9246
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -8917,7 +9250,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
8917
9250
  if (nonTelegramWebhooks.length > 0) {
8918
9251
  const _rateLimitCtx = {
8919
9252
  event: "rate_limit_fallback",
8920
- projectName: path21.basename(projectDir),
9253
+ projectName: path22.basename(projectDir),
8921
9254
  exitCode,
8922
9255
  provider: config.provider
8923
9256
  };
@@ -8949,7 +9282,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
8949
9282
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
8950
9283
  const _ctx = {
8951
9284
  event,
8952
- projectName: path21.basename(projectDir),
9285
+ projectName: path22.basename(projectDir),
8953
9286
  exitCode,
8954
9287
  provider: config.provider,
8955
9288
  prdName: scriptResult?.data.prd,
@@ -9111,20 +9444,20 @@ function applyCliOverrides(config, options) {
9111
9444
  return overridden;
9112
9445
  }
9113
9446
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
9114
- const absolutePrdDir = path21.join(projectDir, prdDir);
9115
- const doneDir = path21.join(absolutePrdDir, "done");
9447
+ const absolutePrdDir = path22.join(projectDir, prdDir);
9448
+ const doneDir = path22.join(absolutePrdDir, "done");
9116
9449
  const pending = [];
9117
9450
  const completed = [];
9118
- if (fs21.existsSync(absolutePrdDir)) {
9119
- const entries = fs21.readdirSync(absolutePrdDir, { withFileTypes: true });
9451
+ if (fs22.existsSync(absolutePrdDir)) {
9452
+ const entries = fs22.readdirSync(absolutePrdDir, { withFileTypes: true });
9120
9453
  for (const entry of entries) {
9121
9454
  if (entry.isFile() && entry.name.endsWith(".md")) {
9122
- const claimPath = path21.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
9455
+ const claimPath = path22.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
9123
9456
  let claimed = false;
9124
9457
  let claimInfo = null;
9125
- if (fs21.existsSync(claimPath)) {
9458
+ if (fs22.existsSync(claimPath)) {
9126
9459
  try {
9127
- const content = fs21.readFileSync(claimPath, "utf-8");
9460
+ const content = fs22.readFileSync(claimPath, "utf-8");
9128
9461
  const data = JSON.parse(content);
9129
9462
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
9130
9463
  if (age < maxRuntime) {
@@ -9138,8 +9471,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
9138
9471
  }
9139
9472
  }
9140
9473
  }
9141
- if (fs21.existsSync(doneDir)) {
9142
- const entries = fs21.readdirSync(doneDir, { withFileTypes: true });
9474
+ if (fs22.existsSync(doneDir)) {
9475
+ const entries = fs22.readdirSync(doneDir, { withFileTypes: true });
9143
9476
  for (const entry of entries) {
9144
9477
  if (entry.isFile() && entry.name.endsWith(".md")) {
9145
9478
  completed.push(entry.name);
@@ -9299,7 +9632,7 @@ ${stderr}`);
9299
9632
  // src/commands/review.ts
9300
9633
  init_dist();
9301
9634
  import { execFileSync as execFileSync5 } from "child_process";
9302
- import * as path22 from "path";
9635
+ import * as path23 from "path";
9303
9636
  function shouldSendReviewNotification(scriptStatus) {
9304
9637
  if (!scriptStatus) {
9305
9638
  return true;
@@ -9309,6 +9642,15 @@ function shouldSendReviewNotification(scriptStatus) {
9309
9642
  }
9310
9643
  return !scriptStatus.startsWith("skip_");
9311
9644
  }
9645
+ function shouldSendReviewCompletionNotification(exitCode, scriptStatus) {
9646
+ if (exitCode !== 0) {
9647
+ return false;
9648
+ }
9649
+ if (scriptStatus === "failure" || scriptStatus === "timeout") {
9650
+ return false;
9651
+ }
9652
+ return shouldSendReviewNotification(scriptStatus);
9653
+ }
9312
9654
  function parseAutoMergedPrNumbers(raw) {
9313
9655
  if (!raw || raw.trim().length === 0) {
9314
9656
  return [];
@@ -9390,10 +9732,6 @@ function buildEnvVars2(config, options) {
9390
9732
  env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
9391
9733
  env.NW_PRD_DIR = config.prdDir;
9392
9734
  env.NW_CLAUDE_MODEL_ID = CLAUDE_MODEL_IDS[config.primaryFallbackModel ?? config.claudeModel ?? "sonnet"];
9393
- if (config.autoMerge) {
9394
- env.NW_AUTO_MERGE = "1";
9395
- }
9396
- env.NW_AUTO_MERGE_METHOD = config.autoMergeMethod;
9397
9735
  return env;
9398
9736
  }
9399
9737
  function applyCliOverrides2(config, options) {
@@ -9407,9 +9745,6 @@ function applyCliOverrides2(config, options) {
9407
9745
  if (options.provider) {
9408
9746
  overridden._cliProviderOverride = options.provider;
9409
9747
  }
9410
- if (options.autoMerge !== void 0) {
9411
- overridden.autoMerge = options.autoMerge;
9412
- }
9413
9748
  return overridden;
9414
9749
  }
9415
9750
  function isFailingCheck(check) {
@@ -9470,7 +9805,7 @@ function getOpenPrsNeedingWork(branchPatterns) {
9470
9805
  }
9471
9806
  }
9472
9807
  function reviewCommand(program2) {
9473
- 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) => {
9808
+ 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) => {
9474
9809
  const projectDir = process.cwd();
9475
9810
  let config = loadConfig(projectDir);
9476
9811
  config = applyCliOverrides2(config, options);
@@ -9493,10 +9828,6 @@ function reviewCommand(program2) {
9493
9828
  ]);
9494
9829
  configTable.push(["Min Review Score", `${config.minReviewScore}/100`]);
9495
9830
  configTable.push(["Branch Patterns", config.branchPatterns.join(", ")]);
9496
- configTable.push([
9497
- "Auto-merge",
9498
- config.autoMerge ? `Enabled (${config.autoMergeMethod})` : "Disabled"
9499
- ]);
9500
9831
  configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
9501
9832
  configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
9502
9833
  configTable.push([
@@ -9567,12 +9898,15 @@ ${stderr}`);
9567
9898
  spinner.fail(`PR reviewer exited with code ${exitCode}`);
9568
9899
  }
9569
9900
  if (!options.dryRun) {
9570
- const skipNotification = !shouldSendReviewNotification(scriptResult?.status);
9571
- if (skipNotification) {
9572
- info("Skipping review notification (no actionable review result)");
9901
+ const shouldNotifyCompletion = shouldSendReviewCompletionNotification(
9902
+ exitCode,
9903
+ scriptResult?.status
9904
+ );
9905
+ if (!shouldNotifyCompletion) {
9906
+ info("Skipping review completion notification (review did not complete successfully)");
9573
9907
  }
9574
9908
  let fallbackPrDetails = null;
9575
- if (!skipNotification && exitCode === 0) {
9909
+ if (shouldNotifyCompletion) {
9576
9910
  const reviewedPrNumbers = parseReviewedPrNumbers(scriptResult?.data.prs);
9577
9911
  const firstReviewedPrNumber = reviewedPrNumbers[0];
9578
9912
  if (firstReviewedPrNumber !== void 0) {
@@ -9582,7 +9916,7 @@ ${stderr}`);
9582
9916
  fallbackPrDetails = fetchReviewedPrDetails(config.branchPatterns, projectDir);
9583
9917
  }
9584
9918
  }
9585
- if (!skipNotification) {
9919
+ if (shouldNotifyCompletion) {
9586
9920
  const attempts = parseRetryAttempts(scriptResult?.data.attempts);
9587
9921
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
9588
9922
  const legacyNoChangesNeeded = scriptResult?.data.no_changes_needed === "1";
@@ -9598,7 +9932,7 @@ ${stderr}`);
9598
9932
  const reviewEvent = legacyNoChangesNeeded ? "review_ready_for_human" : "review_completed";
9599
9933
  await sendNotifications(config, {
9600
9934
  event: reviewEvent,
9601
- projectName: path22.basename(projectDir),
9935
+ projectName: path23.basename(projectDir),
9602
9936
  exitCode,
9603
9937
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
9604
9938
  prUrl: fallbackPrDetails?.url,
@@ -9620,7 +9954,7 @@ ${stderr}`);
9620
9954
  const reviewEvent = target.noChangesNeeded ? "review_ready_for_human" : "review_completed";
9621
9955
  await sendNotifications(config, {
9622
9956
  event: reviewEvent,
9623
- projectName: path22.basename(projectDir),
9957
+ projectName: path23.basename(projectDir),
9624
9958
  exitCode,
9625
9959
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
9626
9960
  prUrl: prDetails?.url,
@@ -9642,7 +9976,7 @@ ${stderr}`);
9642
9976
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
9643
9977
  const _mergeCtx = {
9644
9978
  event: "pr_auto_merged",
9645
- projectName: path22.basename(projectDir),
9979
+ projectName: path23.basename(projectDir),
9646
9980
  exitCode,
9647
9981
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
9648
9982
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -9667,7 +10001,7 @@ ${stderr}`);
9667
10001
 
9668
10002
  // src/commands/qa.ts
9669
10003
  init_dist();
9670
- import * as path23 from "path";
10004
+ import * as path24 from "path";
9671
10005
  function shouldSendQaNotification(scriptStatus) {
9672
10006
  if (!scriptStatus) {
9673
10007
  return true;
@@ -9808,7 +10142,7 @@ ${stderr}`);
9808
10142
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
9809
10143
  const _qaCtx = {
9810
10144
  event: "qa_completed",
9811
- projectName: path23.basename(projectDir),
10145
+ projectName: path24.basename(projectDir),
9812
10146
  exitCode,
9813
10147
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
9814
10148
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -9834,8 +10168,8 @@ ${stderr}`);
9834
10168
 
9835
10169
  // src/commands/audit.ts
9836
10170
  init_dist();
9837
- import * as fs22 from "fs";
9838
- import * as path24 from "path";
10171
+ import * as fs23 from "fs";
10172
+ import * as path25 from "path";
9839
10173
  function buildEnvVars4(config, options) {
9840
10174
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
9841
10175
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -9878,7 +10212,8 @@ function auditCommand(program2) {
9878
10212
  configTable.push(["Provider", auditProvider]);
9879
10213
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
9880
10214
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
9881
- configTable.push(["Report File", path24.join(projectDir, "logs", "audit-report.md")]);
10215
+ configTable.push(["Target Column", config.audit.targetColumn]);
10216
+ configTable.push(["Report File", path25.join(projectDir, "logs", "audit-report.md")]);
9882
10217
  console.log(configTable.toString());
9883
10218
  header("Provider Invocation");
9884
10219
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -9913,21 +10248,27 @@ ${stderr}`);
9913
10248
  } else if (scriptResult?.status?.startsWith("skip_")) {
9914
10249
  spinner.succeed("Code audit skipped");
9915
10250
  } else {
9916
- const reportPath = path24.join(projectDir, "logs", "audit-report.md");
9917
- if (!fs22.existsSync(reportPath)) {
10251
+ const reportPath = path25.join(projectDir, "logs", "audit-report.md");
10252
+ if (!fs23.existsSync(reportPath)) {
9918
10253
  spinner.fail("Code audit finished without a report file");
9919
10254
  process.exit(1);
9920
10255
  }
9921
- spinner.succeed(`Code audit complete \u2014 report written to ${reportPath}`);
10256
+ const syncResult = await syncAuditFindingsToBoard(config, projectDir);
10257
+ const message = `Code audit complete \u2014 report written to ${reportPath}; ${syncResult.summary}`;
10258
+ if (syncResult.status === "failed" || syncResult.status === "partial") {
10259
+ spinner.warn(message);
10260
+ } else {
10261
+ spinner.succeed(message);
10262
+ }
9922
10263
  }
9923
10264
  } else {
9924
10265
  const statusSuffix = scriptResult?.status ? ` (${scriptResult.status})` : "";
9925
10266
  const providerExit = scriptResult?.data?.provider_exit;
9926
10267
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
9927
10268
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
9928
- const logPath = path24.join(projectDir, "logs", "audit.log");
9929
- if (fs22.existsSync(logPath)) {
9930
- const logLines = fs22.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
10269
+ const logPath = path25.join(projectDir, "logs", "audit.log");
10270
+ if (fs23.existsSync(logPath)) {
10271
+ const logLines = fs23.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
9931
10272
  if (logLines.length > 0) {
9932
10273
  process.stderr.write(logLines.join("\n") + "\n");
9933
10274
  }
@@ -10001,16 +10342,16 @@ function analyticsCommand(program2) {
10001
10342
  // src/commands/install.ts
10002
10343
  init_dist();
10003
10344
  import { execSync as execSync4 } from "child_process";
10004
- import * as path25 from "path";
10005
- import * as fs23 from "fs";
10345
+ import * as path26 from "path";
10346
+ import * as fs24 from "fs";
10006
10347
  function shellQuote(value) {
10007
10348
  return `'${value.replace(/'/g, `'"'"'`)}'`;
10008
10349
  }
10009
10350
  function getNightWatchBinPath() {
10010
10351
  try {
10011
10352
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
10012
- const binPath = path25.join(npmBin, "night-watch");
10013
- if (fs23.existsSync(binPath)) {
10353
+ const binPath = path26.join(npmBin, "night-watch");
10354
+ if (fs24.existsSync(binPath)) {
10014
10355
  return binPath;
10015
10356
  }
10016
10357
  } catch {
@@ -10023,17 +10364,17 @@ function getNightWatchBinPath() {
10023
10364
  }
10024
10365
  function getNodeBinDir() {
10025
10366
  if (process.execPath && process.execPath !== "node") {
10026
- return path25.dirname(process.execPath);
10367
+ return path26.dirname(process.execPath);
10027
10368
  }
10028
10369
  try {
10029
10370
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
10030
- return path25.dirname(nodePath);
10371
+ return path26.dirname(nodePath);
10031
10372
  } catch {
10032
10373
  return "";
10033
10374
  }
10034
10375
  }
10035
10376
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
10036
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path25.dirname(nightWatchBin) : "";
10377
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path26.dirname(nightWatchBin) : "";
10037
10378
  const pathParts = Array.from(
10038
10379
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
10039
10380
  );
@@ -10049,12 +10390,12 @@ function performInstall(projectDir, config, options) {
10049
10390
  const nightWatchBin = getNightWatchBinPath();
10050
10391
  const projectName = getProjectName(projectDir);
10051
10392
  const marker = generateMarker(projectName);
10052
- const logDir = path25.join(projectDir, LOG_DIR);
10053
- if (!fs23.existsSync(logDir)) {
10054
- fs23.mkdirSync(logDir, { recursive: true });
10393
+ const logDir = path26.join(projectDir, LOG_DIR);
10394
+ if (!fs24.existsSync(logDir)) {
10395
+ fs24.mkdirSync(logDir, { recursive: true });
10055
10396
  }
10056
- const executorLog = path25.join(logDir, "executor.log");
10057
- const reviewerLog = path25.join(logDir, "reviewer.log");
10397
+ const executorLog = path26.join(logDir, "executor.log");
10398
+ const reviewerLog = path26.join(logDir, "reviewer.log");
10058
10399
  if (!options?.force) {
10059
10400
  const existingEntries2 = Array.from(
10060
10401
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -10091,7 +10432,7 @@ function performInstall(projectDir, config, options) {
10091
10432
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
10092
10433
  if (installSlicer) {
10093
10434
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
10094
- const slicerLog = path25.join(logDir, "slicer.log");
10435
+ const slicerLog = path26.join(logDir, "slicer.log");
10095
10436
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
10096
10437
  entries.push(slicerEntry);
10097
10438
  }
@@ -10099,7 +10440,7 @@ function performInstall(projectDir, config, options) {
10099
10440
  const installQa = disableQa ? false : config.qa.enabled;
10100
10441
  if (installQa) {
10101
10442
  const qaSchedule = config.qa.schedule;
10102
- const qaLog = path25.join(logDir, "qa.log");
10443
+ const qaLog = path26.join(logDir, "qa.log");
10103
10444
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
10104
10445
  entries.push(qaEntry);
10105
10446
  }
@@ -10107,7 +10448,7 @@ function performInstall(projectDir, config, options) {
10107
10448
  const installAudit = disableAudit ? false : config.audit.enabled;
10108
10449
  if (installAudit) {
10109
10450
  const auditSchedule = config.audit.schedule;
10110
- const auditLog = path25.join(logDir, "audit.log");
10451
+ const auditLog = path26.join(logDir, "audit.log");
10111
10452
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
10112
10453
  entries.push(auditEntry);
10113
10454
  }
@@ -10115,7 +10456,7 @@ function performInstall(projectDir, config, options) {
10115
10456
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
10116
10457
  if (installAnalytics) {
10117
10458
  const analyticsSchedule = config.analytics.schedule;
10118
- const analyticsLog = path25.join(logDir, "analytics.log");
10459
+ const analyticsLog = path26.join(logDir, "analytics.log");
10119
10460
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
10120
10461
  entries.push(analyticsEntry);
10121
10462
  }
@@ -10123,10 +10464,18 @@ function performInstall(projectDir, config, options) {
10123
10464
  const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10124
10465
  if (installPrResolver) {
10125
10466
  const prResolverSchedule = config.prResolver.schedule;
10126
- const prResolverLog = path25.join(logDir, "pr-resolver.log");
10467
+ const prResolverLog = path26.join(logDir, "pr-resolver.log");
10127
10468
  const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10128
10469
  entries.push(prResolverEntry);
10129
10470
  }
10471
+ const disableMerger = options?.noMerger === true || options?.merger === false;
10472
+ const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
10473
+ if (installMerger) {
10474
+ const mergerSchedule = config.merger.schedule;
10475
+ const mergerLog = path26.join(logDir, "merger.log");
10476
+ const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
10477
+ entries.push(mergerEntry);
10478
+ }
10130
10479
  const existingEntries = new Set(
10131
10480
  Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
10132
10481
  );
@@ -10145,7 +10494,7 @@ function performInstall(projectDir, config, options) {
10145
10494
  }
10146
10495
  }
10147
10496
  function installCommand(program2) {
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) => {
10497
+ 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) => {
10149
10498
  try {
10150
10499
  const projectDir = process.cwd();
10151
10500
  const config = loadConfig(projectDir);
@@ -10154,12 +10503,12 @@ function installCommand(program2) {
10154
10503
  const nightWatchBin = getNightWatchBinPath();
10155
10504
  const projectName = getProjectName(projectDir);
10156
10505
  const marker = generateMarker(projectName);
10157
- const logDir = path25.join(projectDir, LOG_DIR);
10158
- if (!fs23.existsSync(logDir)) {
10159
- fs23.mkdirSync(logDir, { recursive: true });
10506
+ const logDir = path26.join(projectDir, LOG_DIR);
10507
+ if (!fs24.existsSync(logDir)) {
10508
+ fs24.mkdirSync(logDir, { recursive: true });
10160
10509
  }
10161
- const executorLog = path25.join(logDir, "executor.log");
10162
- const reviewerLog = path25.join(logDir, "reviewer.log");
10510
+ const executorLog = path26.join(logDir, "executor.log");
10511
+ const reviewerLog = path26.join(logDir, "reviewer.log");
10163
10512
  const existingEntries = Array.from(
10164
10513
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
10165
10514
  );
@@ -10195,7 +10544,7 @@ function installCommand(program2) {
10195
10544
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
10196
10545
  let slicerLog;
10197
10546
  if (installSlicer) {
10198
- slicerLog = path25.join(logDir, "slicer.log");
10547
+ slicerLog = path26.join(logDir, "slicer.log");
10199
10548
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
10200
10549
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
10201
10550
  entries.push(slicerEntry);
@@ -10204,7 +10553,7 @@ function installCommand(program2) {
10204
10553
  const installQa = disableQa ? false : config.qa.enabled;
10205
10554
  let qaLog;
10206
10555
  if (installQa) {
10207
- qaLog = path25.join(logDir, "qa.log");
10556
+ qaLog = path26.join(logDir, "qa.log");
10208
10557
  const qaSchedule = config.qa.schedule;
10209
10558
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
10210
10559
  entries.push(qaEntry);
@@ -10213,7 +10562,7 @@ function installCommand(program2) {
10213
10562
  const installAudit = disableAudit ? false : config.audit.enabled;
10214
10563
  let auditLog;
10215
10564
  if (installAudit) {
10216
- auditLog = path25.join(logDir, "audit.log");
10565
+ auditLog = path26.join(logDir, "audit.log");
10217
10566
  const auditSchedule = config.audit.schedule;
10218
10567
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
10219
10568
  entries.push(auditEntry);
@@ -10222,7 +10571,7 @@ function installCommand(program2) {
10222
10571
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
10223
10572
  let analyticsLog;
10224
10573
  if (installAnalytics) {
10225
- analyticsLog = path25.join(logDir, "analytics.log");
10574
+ analyticsLog = path26.join(logDir, "analytics.log");
10226
10575
  const analyticsSchedule = config.analytics.schedule;
10227
10576
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
10228
10577
  entries.push(analyticsEntry);
@@ -10231,11 +10580,20 @@ function installCommand(program2) {
10231
10580
  const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10232
10581
  let prResolverLog;
10233
10582
  if (installPrResolver) {
10234
- prResolverLog = path25.join(logDir, "pr-resolver.log");
10583
+ prResolverLog = path26.join(logDir, "pr-resolver.log");
10235
10584
  const prResolverSchedule = config.prResolver.schedule;
10236
10585
  const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10237
10586
  entries.push(prResolverEntry);
10238
10587
  }
10588
+ const disableMerger = options.noMerger === true || options.merger === false;
10589
+ const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
10590
+ let mergerLog;
10591
+ if (installMerger) {
10592
+ mergerLog = path26.join(logDir, "merger.log");
10593
+ const mergerSchedule = config.merger.schedule;
10594
+ const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
10595
+ entries.push(mergerEntry);
10596
+ }
10239
10597
  const existingEntrySet = new Set(existingEntries);
10240
10598
  const currentCrontab = readCrontab();
10241
10599
  const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
@@ -10268,6 +10626,9 @@ function installCommand(program2) {
10268
10626
  if (installPrResolver && prResolverLog) {
10269
10627
  dim(` PR Resolver: ${prResolverLog}`);
10270
10628
  }
10629
+ if (installMerger && mergerLog) {
10630
+ dim(` Merger: ${mergerLog}`);
10631
+ }
10271
10632
  console.log();
10272
10633
  dim("To uninstall, run: night-watch uninstall");
10273
10634
  dim("To check status, run: night-watch status");
@@ -10282,8 +10643,8 @@ function installCommand(program2) {
10282
10643
 
10283
10644
  // src/commands/uninstall.ts
10284
10645
  init_dist();
10285
- import * as path26 from "path";
10286
- import * as fs24 from "fs";
10646
+ import * as path27 from "path";
10647
+ import * as fs25 from "fs";
10287
10648
  function performUninstall(projectDir, options) {
10288
10649
  try {
10289
10650
  const projectName = getProjectName(projectDir);
@@ -10298,8 +10659,8 @@ function performUninstall(projectDir, options) {
10298
10659
  const removedCount = removeEntriesForProject(projectDir, marker);
10299
10660
  unregisterProject(projectDir);
10300
10661
  if (!options?.keepLogs) {
10301
- const logDir = path26.join(projectDir, "logs");
10302
- if (fs24.existsSync(logDir)) {
10662
+ const logDir = path27.join(projectDir, "logs");
10663
+ if (fs25.existsSync(logDir)) {
10303
10664
  const logFiles = [
10304
10665
  "executor.log",
10305
10666
  "reviewer.log",
@@ -10308,15 +10669,15 @@ function performUninstall(projectDir, options) {
10308
10669
  "pr-resolver.log"
10309
10670
  ];
10310
10671
  logFiles.forEach((logFile) => {
10311
- const logPath = path26.join(logDir, logFile);
10312
- if (fs24.existsSync(logPath)) {
10313
- fs24.unlinkSync(logPath);
10672
+ const logPath = path27.join(logDir, logFile);
10673
+ if (fs25.existsSync(logPath)) {
10674
+ fs25.unlinkSync(logPath);
10314
10675
  }
10315
10676
  });
10316
10677
  try {
10317
- const remainingFiles = fs24.readdirSync(logDir);
10678
+ const remainingFiles = fs25.readdirSync(logDir);
10318
10679
  if (remainingFiles.length === 0) {
10319
- fs24.rmdirSync(logDir);
10680
+ fs25.rmdirSync(logDir);
10320
10681
  }
10321
10682
  } catch {
10322
10683
  }
@@ -10349,8 +10710,8 @@ function uninstallCommand(program2) {
10349
10710
  existingEntries.forEach((entry) => dim(` ${entry}`));
10350
10711
  const removedCount = removeEntriesForProject(projectDir, marker);
10351
10712
  if (!options.keepLogs) {
10352
- const logDir = path26.join(projectDir, "logs");
10353
- if (fs24.existsSync(logDir)) {
10713
+ const logDir = path27.join(projectDir, "logs");
10714
+ if (fs25.existsSync(logDir)) {
10354
10715
  const logFiles = [
10355
10716
  "executor.log",
10356
10717
  "reviewer.log",
@@ -10360,16 +10721,16 @@ function uninstallCommand(program2) {
10360
10721
  ];
10361
10722
  let logsRemoved = 0;
10362
10723
  logFiles.forEach((logFile) => {
10363
- const logPath = path26.join(logDir, logFile);
10364
- if (fs24.existsSync(logPath)) {
10365
- fs24.unlinkSync(logPath);
10724
+ const logPath = path27.join(logDir, logFile);
10725
+ if (fs25.existsSync(logPath)) {
10726
+ fs25.unlinkSync(logPath);
10366
10727
  logsRemoved++;
10367
10728
  }
10368
10729
  });
10369
10730
  try {
10370
- const remainingFiles = fs24.readdirSync(logDir);
10731
+ const remainingFiles = fs25.readdirSync(logDir);
10371
10732
  if (remainingFiles.length === 0) {
10372
- fs24.rmdirSync(logDir);
10733
+ fs25.rmdirSync(logDir);
10373
10734
  }
10374
10735
  } catch {
10375
10736
  }
@@ -10413,11 +10774,15 @@ function statusCommand(program2) {
10413
10774
  const qaProc = snapshot.processes.find((p) => p.name === "qa");
10414
10775
  const auditProc = snapshot.processes.find((p) => p.name === "audit");
10415
10776
  const plannerProc = snapshot.processes.find((p) => p.name === "planner");
10777
+ const analyticsProc = snapshot.processes.find((p) => p.name === "analytics");
10778
+ const mergerProc = snapshot.processes.find((p) => p.name === "merger");
10416
10779
  const executorLog = snapshot.logs.find((l) => l.name === "executor");
10417
10780
  const reviewerLog = snapshot.logs.find((l) => l.name === "reviewer");
10418
10781
  const qaLog = snapshot.logs.find((l) => l.name === "qa");
10419
10782
  const auditLog = snapshot.logs.find((l) => l.name === "audit");
10420
10783
  const plannerLog = snapshot.logs.find((l) => l.name === "planner");
10784
+ const analyticsLog = snapshot.logs.find((l) => l.name === "analytics");
10785
+ const mergerLog = snapshot.logs.find((l) => l.name === "merger");
10421
10786
  const pendingPrds = snapshot.prds.filter(
10422
10787
  (p) => p.status === "ready" || p.status === "blocked"
10423
10788
  ).length;
@@ -10435,6 +10800,8 @@ function statusCommand(program2) {
10435
10800
  qa: { running: qaProc?.running ?? false, pid: qaProc?.pid ?? null },
10436
10801
  audit: { running: auditProc?.running ?? false, pid: auditProc?.pid ?? null },
10437
10802
  planner: { running: plannerProc?.running ?? false, pid: plannerProc?.pid ?? null },
10803
+ analytics: { running: analyticsProc?.running ?? false, pid: analyticsProc?.pid ?? null },
10804
+ merger: { running: mergerProc?.running ?? false, pid: mergerProc?.pid ?? null },
10438
10805
  prds: { pending: pendingPrds, claimed: claimedPrds, done: donePrds },
10439
10806
  prs: { open: snapshot.prs.length },
10440
10807
  crontab: snapshot.crontab,
@@ -10468,6 +10835,18 @@ function statusCommand(program2) {
10468
10835
  lastLines: plannerLog.lastLines,
10469
10836
  exists: plannerLog.exists,
10470
10837
  size: plannerLog.size
10838
+ } : void 0,
10839
+ analytics: analyticsLog ? {
10840
+ path: analyticsLog.path,
10841
+ lastLines: analyticsLog.lastLines,
10842
+ exists: analyticsLog.exists,
10843
+ size: analyticsLog.size
10844
+ } : void 0,
10845
+ merger: mergerLog ? {
10846
+ path: mergerLog.path,
10847
+ lastLines: mergerLog.lastLines,
10848
+ exists: mergerLog.exists,
10849
+ size: mergerLog.size
10471
10850
  } : void 0
10472
10851
  }
10473
10852
  };
@@ -10505,6 +10884,14 @@ function statusCommand(program2) {
10505
10884
  "Planner",
10506
10885
  formatRunningStatus(status.planner.running, status.planner.pid)
10507
10886
  ]);
10887
+ processTable.push([
10888
+ "Analytics",
10889
+ formatRunningStatus(status.analytics.running, status.analytics.pid)
10890
+ ]);
10891
+ processTable.push([
10892
+ "Merger",
10893
+ formatRunningStatus(status.merger.running, status.merger.pid)
10894
+ ]);
10508
10895
  console.log(processTable.toString());
10509
10896
  header("PRD Status");
10510
10897
  const prdTable = createTable({ head: ["Status", "Count"] });
@@ -10561,6 +10948,20 @@ function statusCommand(program2) {
10561
10948
  status.logs.planner.exists ? "Exists" : "Not found"
10562
10949
  ]);
10563
10950
  }
10951
+ if (status.logs.analytics) {
10952
+ logTable.push([
10953
+ "Analytics",
10954
+ status.logs.analytics.exists ? formatBytes(status.logs.analytics.size) : "-",
10955
+ status.logs.analytics.exists ? "Exists" : "Not found"
10956
+ ]);
10957
+ }
10958
+ if (status.logs.merger) {
10959
+ logTable.push([
10960
+ "Merger",
10961
+ status.logs.merger.exists ? formatBytes(status.logs.merger.size) : "-",
10962
+ status.logs.merger.exists ? "Exists" : "Not found"
10963
+ ]);
10964
+ }
10564
10965
  console.log(logTable.toString());
10565
10966
  if (options.verbose) {
10566
10967
  if (status.logs.executor?.exists && status.logs.executor.lastLines.length > 0) {
@@ -10583,6 +10984,14 @@ function statusCommand(program2) {
10583
10984
  dim(" Planner last 5 lines:");
10584
10985
  status.logs.planner.lastLines.forEach((line) => dim(` ${line}`));
10585
10986
  }
10987
+ if (status.logs.analytics?.exists && status.logs.analytics.lastLines.length > 0) {
10988
+ dim(" Analytics last 5 lines:");
10989
+ status.logs.analytics.lastLines.forEach((line) => dim(` ${line}`));
10990
+ }
10991
+ if (status.logs.merger?.exists && status.logs.merger.lastLines.length > 0) {
10992
+ dim(" Merger last 5 lines:");
10993
+ status.logs.merger.lastLines.forEach((line) => dim(` ${line}`));
10994
+ }
10586
10995
  }
10587
10996
  header("Commands");
10588
10997
  dim(" night-watch install - Install crontab entries");
@@ -10592,6 +11001,8 @@ function statusCommand(program2) {
10592
11001
  dim(" night-watch qa - Run QA now");
10593
11002
  dim(" night-watch audit - Run audit now");
10594
11003
  dim(" night-watch planner - Run planner now");
11004
+ dim(" night-watch analytics - Run analytics now");
11005
+ dim(" night-watch merge - Run merger now");
10595
11006
  console.log();
10596
11007
  } catch (error2) {
10597
11008
  console.error(
@@ -10605,14 +11016,14 @@ function statusCommand(program2) {
10605
11016
  // src/commands/logs.ts
10606
11017
  init_dist();
10607
11018
  import { spawn as spawn3 } from "child_process";
10608
- import * as path27 from "path";
10609
- import * as fs25 from "fs";
11019
+ import * as path28 from "path";
11020
+ import * as fs26 from "fs";
10610
11021
  function getLastLines(filePath, lineCount) {
10611
- if (!fs25.existsSync(filePath)) {
11022
+ if (!fs26.existsSync(filePath)) {
10612
11023
  return `Log file not found: ${filePath}`;
10613
11024
  }
10614
11025
  try {
10615
- const content = fs25.readFileSync(filePath, "utf-8");
11026
+ const content = fs26.readFileSync(filePath, "utf-8");
10616
11027
  const lines = content.trim().split("\n");
10617
11028
  return lines.slice(-lineCount).join("\n");
10618
11029
  } catch (error2) {
@@ -10620,7 +11031,7 @@ function getLastLines(filePath, lineCount) {
10620
11031
  }
10621
11032
  }
10622
11033
  function followLog(filePath) {
10623
- if (!fs25.existsSync(filePath)) {
11034
+ if (!fs26.existsSync(filePath)) {
10624
11035
  console.log(`Log file not found: ${filePath}`);
10625
11036
  console.log("The log file will be created when the first execution runs.");
10626
11037
  return;
@@ -10637,32 +11048,42 @@ function followLog(filePath) {
10637
11048
  });
10638
11049
  }
10639
11050
  function logsCommand(program2) {
10640
- program2.command("logs").description("View night-watch log output").option("-n, --lines <count>", "Number of lines to show", "50").option("-f, --follow", "Follow log output (tail -f)").option("-t, --type <type>", "Log type to view (executor|reviewer|qa|audit|planner|all)", "all").action(async (options) => {
11051
+ program2.command("logs").description("View night-watch log output").option("-n, --lines <count>", "Number of lines to show", "50").option("-f, --follow", "Follow log output (tail -f)").option(
11052
+ "-t, --type <type>",
11053
+ "Log type to view (executor|reviewer|qa|audit|planner|analytics|merger|all)",
11054
+ "all"
11055
+ ).action(async (options) => {
10641
11056
  try {
10642
11057
  const projectDir = process.cwd();
10643
- const logDir = path27.join(projectDir, LOG_DIR);
11058
+ const logDir = path28.join(projectDir, LOG_DIR);
10644
11059
  const lineCount = parseInt(options.lines || "50", 10);
10645
- const executorLog = path27.join(logDir, EXECUTOR_LOG_FILE);
10646
- const reviewerLog = path27.join(logDir, REVIEWER_LOG_FILE);
10647
- const qaLog = path27.join(logDir, `${QA_LOG_NAME}.log`);
10648
- const auditLog = path27.join(logDir, `${AUDIT_LOG_NAME}.log`);
10649
- const plannerLog = path27.join(logDir, `${PLANNER_LOG_NAME}.log`);
11060
+ const executorLog = path28.join(logDir, EXECUTOR_LOG_FILE);
11061
+ const reviewerLog = path28.join(logDir, REVIEWER_LOG_FILE);
11062
+ const qaLog = path28.join(logDir, `${QA_LOG_NAME}.log`);
11063
+ const auditLog = path28.join(logDir, `${AUDIT_LOG_NAME}.log`);
11064
+ const plannerLog = path28.join(logDir, `${PLANNER_LOG_NAME}.log`);
11065
+ const analyticsLog = path28.join(logDir, `${ANALYTICS_LOG_NAME}.log`);
11066
+ const mergerLog = path28.join(logDir, `${MERGER_LOG_NAME}.log`);
10650
11067
  const logType = options.type?.toLowerCase() || "all";
10651
11068
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
10652
11069
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
10653
11070
  const showQa = logType === "all" || logType === "qa";
10654
11071
  const showAudit = logType === "all" || logType === "audit";
10655
11072
  const showPlanner = logType === "all" || logType === "planner" || logType === "slice" || logType === "slicer";
11073
+ const showAnalytics = logType === "all" || logType === "analytics";
11074
+ const showMerger = logType === "all" || logType === "merge" || logType === "merger";
10656
11075
  if (options.follow) {
10657
11076
  if (logType === "all") {
10658
11077
  dim("Note: Following all logs is not supported. Showing executor log.");
10659
- dim("Use --type reviewer|qa|audit|planner for other logs.\n");
11078
+ dim("Use --type reviewer|qa|audit|planner|analytics|merger for other logs.\n");
10660
11079
  }
10661
11080
  let targetLog = executorLog;
10662
11081
  if (showReviewer) targetLog = reviewerLog;
10663
11082
  else if (showQa) targetLog = qaLog;
10664
11083
  else if (showAudit) targetLog = auditLog;
10665
11084
  else if (showPlanner) targetLog = plannerLog;
11085
+ else if (showAnalytics) targetLog = analyticsLog;
11086
+ else if (showMerger) targetLog = mergerLog;
10666
11087
  followLog(targetLog);
10667
11088
  return;
10668
11089
  }
@@ -10697,10 +11118,24 @@ function logsCommand(program2) {
10697
11118
  console.log();
10698
11119
  console.log(getLastLines(plannerLog, lineCount));
10699
11120
  }
11121
+ if (showAnalytics) {
11122
+ header("Analytics Log");
11123
+ dim(`File: ${analyticsLog}`);
11124
+ console.log();
11125
+ console.log(getLastLines(analyticsLog, lineCount));
11126
+ }
11127
+ if (showMerger) {
11128
+ header("Merger Log");
11129
+ dim(`File: ${mergerLog}`);
11130
+ console.log();
11131
+ console.log(getLastLines(mergerLog, lineCount));
11132
+ }
10700
11133
  console.log();
10701
11134
  dim("---");
10702
11135
  dim("Tip: Use -f to follow logs in real-time");
10703
- dim(" Use --type executor|reviewer|qa|audit|planner to view specific logs");
11136
+ dim(
11137
+ " Use --type executor|reviewer|qa|audit|planner|analytics|merger to view specific logs"
11138
+ );
10704
11139
  } catch (err) {
10705
11140
  console.error(`Error reading logs: ${err instanceof Error ? err.message : String(err)}`);
10706
11141
  process.exit(1);
@@ -10711,8 +11146,8 @@ function logsCommand(program2) {
10711
11146
  // src/commands/prd.ts
10712
11147
  init_dist();
10713
11148
  import { execSync as execSync5, spawn as spawn4, spawnSync } from "child_process";
10714
- import * as fs26 from "fs";
10715
- import * as path28 from "path";
11149
+ import * as fs27 from "fs";
11150
+ import * as path29 from "path";
10716
11151
  import { fileURLToPath as fileURLToPath4 } from "url";
10717
11152
  import { dirname as dirname9 } from "path";
10718
11153
  var __filename3 = fileURLToPath4(import.meta.url);
@@ -10720,21 +11155,21 @@ var __dirname3 = dirname9(__filename3);
10720
11155
  function findTemplatesDir2(startDir) {
10721
11156
  let current = startDir;
10722
11157
  for (let i = 0; i < 8; i++) {
10723
- const candidate = path28.join(current, "templates");
10724
- if (fs26.existsSync(candidate) && fs26.statSync(candidate).isDirectory()) {
11158
+ const candidate = path29.join(current, "templates");
11159
+ if (fs27.existsSync(candidate) && fs27.statSync(candidate).isDirectory()) {
10725
11160
  return candidate;
10726
11161
  }
10727
- current = path28.dirname(current);
11162
+ current = path29.dirname(current);
10728
11163
  }
10729
- return path28.join(startDir, "templates");
11164
+ return path29.join(startDir, "templates");
10730
11165
  }
10731
11166
  var TEMPLATES_DIR2 = findTemplatesDir2(__dirname3);
10732
11167
  function slugify2(name) {
10733
11168
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
10734
11169
  }
10735
11170
  function getNextPrdNumber2(prdDir) {
10736
- if (!fs26.existsSync(prdDir)) return 1;
10737
- const files = fs26.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
11171
+ if (!fs27.existsSync(prdDir)) return 1;
11172
+ const files = fs27.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
10738
11173
  const numbers = files.map((f) => {
10739
11174
  const match = f.match(/^(\d+)-/);
10740
11175
  return match ? parseInt(match[1], 10) : 0;
@@ -10815,13 +11250,13 @@ function resolveGitHubBlobUrl(projectDir, relPath) {
10815
11250
  return null;
10816
11251
  }
10817
11252
  const ref = branch && branch !== "HEAD" ? branch : "main";
10818
- return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path28.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
11253
+ return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path29.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
10819
11254
  } catch {
10820
11255
  return null;
10821
11256
  }
10822
11257
  }
10823
11258
  function buildGithubIssueBody(prdPath, projectDir, prdContent) {
10824
- const relPath = path28.relative(projectDir, prdPath);
11259
+ const relPath = path29.relative(projectDir, prdPath);
10825
11260
  const blobUrl = resolveGitHubBlobUrl(projectDir, relPath);
10826
11261
  const fileLine = blobUrl ? `PRD file: [\`${relPath}\`](${blobUrl})` : `PRD file: \`${relPath}\``;
10827
11262
  return `${fileLine}
@@ -10832,13 +11267,13 @@ ${prdContent}
10832
11267
  Created via \`night-watch prd create\`.`;
10833
11268
  }
10834
11269
  async function generatePrdWithClaude(description, projectDir, model) {
10835
- const bundledTemplatePath = path28.join(TEMPLATES_DIR2, "prd-creator.md");
10836
- const installedTemplatePath = path28.join(projectDir, "instructions", "prd-creator.md");
10837
- const templatePath = fs26.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
10838
- if (!fs26.existsSync(templatePath)) {
11270
+ const bundledTemplatePath = path29.join(TEMPLATES_DIR2, "prd-creator.md");
11271
+ const installedTemplatePath = path29.join(projectDir, "instructions", "prd-creator.md");
11272
+ const templatePath = fs27.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
11273
+ if (!fs27.existsSync(templatePath)) {
10839
11274
  return null;
10840
11275
  }
10841
- const planningPrinciples = fs26.readFileSync(templatePath, "utf-8");
11276
+ const planningPrinciples = fs27.readFileSync(templatePath, "utf-8");
10842
11277
  const prompt = buildPrdPrompt(description, projectDir, planningPrinciples);
10843
11278
  const modelId = model ?? CLAUDE_MODEL_IDS.opus;
10844
11279
  const env = buildNativeClaudeEnv(process.env);
@@ -10906,17 +11341,17 @@ function runGh(args, cwd) {
10906
11341
  return null;
10907
11342
  }
10908
11343
  function createGithubIssue(title, prdPath, projectDir, prdContent) {
10909
- const tmpFile = path28.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
11344
+ const tmpFile = path29.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
10910
11345
  try {
10911
11346
  const body = buildGithubIssueBody(prdPath, projectDir, prdContent);
10912
- fs26.writeFileSync(tmpFile, body, "utf-8");
11347
+ fs27.writeFileSync(tmpFile, body, "utf-8");
10913
11348
  const baseArgs = ["issue", "create", "--title", `PRD: ${title}`, "--body-file", tmpFile];
10914
11349
  return runGh([...baseArgs, "--label", "prd"], projectDir) ?? runGh(baseArgs, projectDir);
10915
11350
  } catch {
10916
11351
  return null;
10917
11352
  } finally {
10918
11353
  try {
10919
- fs26.unlinkSync(tmpFile);
11354
+ fs27.unlinkSync(tmpFile);
10920
11355
  } catch {
10921
11356
  }
10922
11357
  }
@@ -10928,10 +11363,10 @@ function parseDependencies(content) {
10928
11363
  }
10929
11364
  function isClaimActive(claimPath, maxRuntime) {
10930
11365
  try {
10931
- if (!fs26.existsSync(claimPath)) {
11366
+ if (!fs27.existsSync(claimPath)) {
10932
11367
  return { active: false };
10933
11368
  }
10934
- const content = fs26.readFileSync(claimPath, "utf-8");
11369
+ const content = fs27.readFileSync(claimPath, "utf-8");
10935
11370
  const claim = JSON.parse(content);
10936
11371
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
10937
11372
  if (age < maxRuntime) {
@@ -10946,9 +11381,9 @@ function prdCommand(program2) {
10946
11381
  const prd = program2.command("prd").description("Manage PRD files");
10947
11382
  prd.command("create").description("Generate a new PRD markdown file using Claude").argument("<name>", "PRD description").option("--number", "Add auto-numbering prefix to the filename", false).option("--model <model>", "Claude model to use (e.g. sonnet, opus, or a full model ID)").action(async (name, options) => {
10948
11383
  const projectDir = process.cwd();
10949
- const prdDir = path28.join(projectDir, resolvePrdCreateDir());
10950
- if (!fs26.existsSync(prdDir)) {
10951
- fs26.mkdirSync(prdDir, { recursive: true });
11384
+ const prdDir = path29.join(projectDir, resolvePrdCreateDir());
11385
+ if (!fs27.existsSync(prdDir)) {
11386
+ fs27.mkdirSync(prdDir, { recursive: true });
10952
11387
  }
10953
11388
  const resolvedModel = options.model ? CLAUDE_MODEL_IDS[options.model] ?? options.model : void 0;
10954
11389
  const modelLabel = resolvedModel ?? CLAUDE_MODEL_IDS.opus;
@@ -10962,13 +11397,13 @@ function prdCommand(program2) {
10962
11397
  const prdTitle = extractPrdTitle(generated) ?? name;
10963
11398
  const slug = slugify2(prdTitle);
10964
11399
  const filename = options.number ? `${String(getNextPrdNumber2(prdDir)).padStart(2, "0")}-${slug}.md` : `${slug}.md`;
10965
- const filePath = path28.join(prdDir, filename);
10966
- if (fs26.existsSync(filePath)) {
11400
+ const filePath = path29.join(prdDir, filename);
11401
+ if (fs27.existsSync(filePath)) {
10967
11402
  error(`File already exists: ${filePath}`);
10968
11403
  dim("Use a different name or remove the existing file.");
10969
11404
  process.exit(1);
10970
11405
  }
10971
- fs26.writeFileSync(filePath, generated, "utf-8");
11406
+ fs27.writeFileSync(filePath, generated, "utf-8");
10972
11407
  header("PRD Created");
10973
11408
  success(`Created: ${filePath}`);
10974
11409
  const issueUrl = createGithubIssue(prdTitle, filePath, projectDir, generated);
@@ -10981,15 +11416,15 @@ function prdCommand(program2) {
10981
11416
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
10982
11417
  const projectDir = process.cwd();
10983
11418
  const config = loadConfig(projectDir);
10984
- const absolutePrdDir = path28.join(projectDir, config.prdDir);
10985
- const doneDir = path28.join(absolutePrdDir, "done");
11419
+ const absolutePrdDir = path29.join(projectDir, config.prdDir);
11420
+ const doneDir = path29.join(absolutePrdDir, "done");
10986
11421
  const pending = [];
10987
- if (fs26.existsSync(absolutePrdDir)) {
10988
- const files = fs26.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
11422
+ if (fs27.existsSync(absolutePrdDir)) {
11423
+ const files = fs27.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
10989
11424
  for (const file of files) {
10990
- const content = fs26.readFileSync(path28.join(absolutePrdDir, file), "utf-8");
11425
+ const content = fs27.readFileSync(path29.join(absolutePrdDir, file), "utf-8");
10991
11426
  const deps = parseDependencies(content);
10992
- const claimPath = path28.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
11427
+ const claimPath = path29.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
10993
11428
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
10994
11429
  pending.push({
10995
11430
  name: file,
@@ -11000,10 +11435,10 @@ function prdCommand(program2) {
11000
11435
  }
11001
11436
  }
11002
11437
  const done = [];
11003
- if (fs26.existsSync(doneDir)) {
11004
- const files = fs26.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
11438
+ if (fs27.existsSync(doneDir)) {
11439
+ const files = fs27.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
11005
11440
  for (const file of files) {
11006
- const content = fs26.readFileSync(path28.join(doneDir, file), "utf-8");
11441
+ const content = fs27.readFileSync(path29.join(doneDir, file), "utf-8");
11007
11442
  const deps = parseDependencies(content);
11008
11443
  done.push({ name: file, dependencies: deps });
11009
11444
  }
@@ -11040,7 +11475,7 @@ import blessed6 from "blessed";
11040
11475
  // src/commands/dashboard/tab-status.ts
11041
11476
  init_dist();
11042
11477
  import blessed from "blessed";
11043
- import * as fs27 from "fs";
11478
+ import * as fs28 from "fs";
11044
11479
  function sortPrdsByPriority(prds, priority) {
11045
11480
  if (priority.length === 0) return prds;
11046
11481
  const priorityMap = /* @__PURE__ */ new Map();
@@ -11136,7 +11571,7 @@ function renderLogPane(projectDir, logs) {
11136
11571
  let newestMtime = 0;
11137
11572
  for (const log of existingLogs) {
11138
11573
  try {
11139
- const stat = fs27.statSync(log.path);
11574
+ const stat = fs28.statSync(log.path);
11140
11575
  if (stat.mtimeMs > newestMtime) {
11141
11576
  newestMtime = stat.mtimeMs;
11142
11577
  newestLog = log;
@@ -12791,8 +13226,8 @@ function createActionsTab() {
12791
13226
  // src/commands/dashboard/tab-logs.ts
12792
13227
  init_dist();
12793
13228
  import blessed5 from "blessed";
12794
- import * as fs28 from "fs";
12795
- import * as path29 from "path";
13229
+ import * as fs29 from "fs";
13230
+ import * as path30 from "path";
12796
13231
  var LOG_NAMES = ["executor", "reviewer"];
12797
13232
  var LOG_LINES = 200;
12798
13233
  function createLogsTab() {
@@ -12833,7 +13268,7 @@ function createLogsTab() {
12833
13268
  let activeKeyHandlers = [];
12834
13269
  let activeCtx = null;
12835
13270
  function getLogPath(projectDir, logName) {
12836
- return path29.join(projectDir, "logs", `${logName}.log`);
13271
+ return path30.join(projectDir, "logs", `${logName}.log`);
12837
13272
  }
12838
13273
  function updateSelector() {
12839
13274
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -12847,7 +13282,7 @@ function createLogsTab() {
12847
13282
  function loadLog(ctx) {
12848
13283
  const logName = LOG_NAMES[selectedLogIndex];
12849
13284
  const logPath = getLogPath(ctx.projectDir, logName);
12850
- if (!fs28.existsSync(logPath)) {
13285
+ if (!fs29.existsSync(logPath)) {
12851
13286
  logContent.setContent(
12852
13287
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
12853
13288
 
@@ -12857,7 +13292,7 @@ Log will appear here once the ${logName} runs.`
12857
13292
  return;
12858
13293
  }
12859
13294
  try {
12860
- const stat = fs28.statSync(logPath);
13295
+ const stat = fs29.statSync(logPath);
12861
13296
  const sizeKB = (stat.size / 1024).toFixed(1);
12862
13297
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
12863
13298
  } catch {
@@ -13355,12 +13790,12 @@ function doctorCommand(program2) {
13355
13790
 
13356
13791
  // src/commands/serve.ts
13357
13792
  init_dist();
13358
- import * as fs33 from "fs";
13793
+ import * as fs34 from "fs";
13359
13794
 
13360
13795
  // ../server/dist/index.js
13361
13796
  init_dist();
13362
- import * as fs32 from "fs";
13363
- import * as path35 from "path";
13797
+ import * as fs33 from "fs";
13798
+ import * as path36 from "path";
13364
13799
  import { dirname as dirname11 } from "path";
13365
13800
  import { fileURLToPath as fileURLToPath5 } from "url";
13366
13801
  import cors from "cors";
@@ -13445,8 +13880,8 @@ function setupGracefulShutdown(server, beforeClose) {
13445
13880
 
13446
13881
  // ../server/dist/middleware/project-resolver.middleware.js
13447
13882
  init_dist();
13448
- import * as fs29 from "fs";
13449
- import * as path30 from "path";
13883
+ import * as fs30 from "fs";
13884
+ import * as path31 from "path";
13450
13885
  function resolveProject(req, res, next) {
13451
13886
  const projectId = req.params.projectId;
13452
13887
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -13456,7 +13891,7 @@ function resolveProject(req, res, next) {
13456
13891
  res.status(404).json({ error: `Project not found: ${decodedId}` });
13457
13892
  return;
13458
13893
  }
13459
- if (!fs29.existsSync(entry.path) || !fs29.existsSync(path30.join(entry.path, CONFIG_FILE_NAME))) {
13894
+ if (!fs30.existsSync(entry.path) || !fs30.existsSync(path31.join(entry.path, CONFIG_FILE_NAME))) {
13460
13895
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
13461
13896
  return;
13462
13897
  }
@@ -13501,8 +13936,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
13501
13936
 
13502
13937
  // ../server/dist/routes/action.routes.js
13503
13938
  init_dist();
13504
- import * as fs30 from "fs";
13505
- import * as path31 from "path";
13939
+ import * as fs31 from "fs";
13940
+ import * as path32 from "path";
13506
13941
  import { execSync as execSync6, spawn as spawn6 } from "child_process";
13507
13942
  import { Router } from "express";
13508
13943
 
@@ -13540,17 +13975,17 @@ function getBoardProvider(config, projectDir) {
13540
13975
  function cleanOrphanedClaims(dir) {
13541
13976
  let entries;
13542
13977
  try {
13543
- entries = fs30.readdirSync(dir, { withFileTypes: true });
13978
+ entries = fs31.readdirSync(dir, { withFileTypes: true });
13544
13979
  } catch {
13545
13980
  return;
13546
13981
  }
13547
13982
  for (const entry of entries) {
13548
- const fullPath = path31.join(dir, entry.name);
13983
+ const fullPath = path32.join(dir, entry.name);
13549
13984
  if (entry.isDirectory() && entry.name !== "done") {
13550
13985
  cleanOrphanedClaims(fullPath);
13551
13986
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
13552
13987
  try {
13553
- fs30.unlinkSync(fullPath);
13988
+ fs31.unlinkSync(fullPath);
13554
13989
  } catch {
13555
13990
  }
13556
13991
  }
@@ -13705,19 +14140,19 @@ function createActionRouteHandlers(ctx) {
13705
14140
  res.status(400).json({ error: "Invalid PRD name" });
13706
14141
  return;
13707
14142
  }
13708
- const prdDir = path31.join(projectDir, config.prdDir);
14143
+ const prdDir = path32.join(projectDir, config.prdDir);
13709
14144
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
13710
- const pendingPath = path31.join(prdDir, normalized);
13711
- const donePath = path31.join(prdDir, "done", normalized);
13712
- if (fs30.existsSync(pendingPath)) {
14145
+ const pendingPath = path32.join(prdDir, normalized);
14146
+ const donePath = path32.join(prdDir, "done", normalized);
14147
+ if (fs31.existsSync(pendingPath)) {
13713
14148
  res.json({ message: `"${normalized}" is already pending` });
13714
14149
  return;
13715
14150
  }
13716
- if (!fs30.existsSync(donePath)) {
14151
+ if (!fs31.existsSync(donePath)) {
13717
14152
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
13718
14153
  return;
13719
14154
  }
13720
- fs30.renameSync(donePath, pendingPath);
14155
+ fs31.renameSync(donePath, pendingPath);
13721
14156
  res.json({ message: `Moved "${normalized}" back to pending` });
13722
14157
  } catch (error2) {
13723
14158
  res.status(500).json({
@@ -13735,11 +14170,11 @@ function createActionRouteHandlers(ctx) {
13735
14170
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
13736
14171
  return;
13737
14172
  }
13738
- if (fs30.existsSync(lockPath)) {
13739
- fs30.unlinkSync(lockPath);
14173
+ if (fs31.existsSync(lockPath)) {
14174
+ fs31.unlinkSync(lockPath);
13740
14175
  }
13741
- const prdDir = path31.join(projectDir, config.prdDir);
13742
- if (fs30.existsSync(prdDir)) {
14176
+ const prdDir = path32.join(projectDir, config.prdDir);
14177
+ if (fs31.existsSync(prdDir)) {
13743
14178
  cleanOrphanedClaims(prdDir);
13744
14179
  }
13745
14180
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -14294,6 +14729,9 @@ function validateConfigChanges(changes, currentConfig) {
14294
14729
  if (audit.maxRuntime !== void 0 && (typeof audit.maxRuntime !== "number" || audit.maxRuntime < 60)) {
14295
14730
  return "audit.maxRuntime must be a number >= 60";
14296
14731
  }
14732
+ if (audit.targetColumn !== void 0 && !BOARD_COLUMNS.includes(audit.targetColumn)) {
14733
+ return `audit.targetColumn must be one of: ${BOARD_COLUMNS.join(", ")}`;
14734
+ }
14297
14735
  }
14298
14736
  if (changes.analytics !== void 0) {
14299
14737
  if (typeof changes.analytics !== "object" || changes.analytics === null) {
@@ -14314,9 +14752,8 @@ function validateConfigChanges(changes, currentConfig) {
14314
14752
  return "analytics.lookbackDays must be a number between 1 and 90";
14315
14753
  }
14316
14754
  if (analytics.targetColumn !== void 0) {
14317
- const validColumns = ["Draft", "Ready", "In Progress", "Review", "Done"];
14318
- if (!validColumns.includes(analytics.targetColumn)) {
14319
- return `analytics.targetColumn must be one of: ${validColumns.join(", ")}`;
14755
+ if (!BOARD_COLUMNS.includes(analytics.targetColumn)) {
14756
+ return `analytics.targetColumn must be one of: ${BOARD_COLUMNS.join(", ")}`;
14320
14757
  }
14321
14758
  }
14322
14759
  if (analytics.analysisPrompt !== void 0 && typeof analytics.analysisPrompt !== "string") {
@@ -14341,16 +14778,8 @@ function validateConfigChanges(changes, currentConfig) {
14341
14778
  if (typeof queue.priority !== "object" || queue.priority === null) {
14342
14779
  return "queue.priority must be an object";
14343
14780
  }
14344
- const validQueueJobs = [
14345
- "executor",
14346
- "reviewer",
14347
- "qa",
14348
- "audit",
14349
- "slicer",
14350
- "analytics"
14351
- ];
14352
14781
  for (const [jobType, value] of Object.entries(queue.priority)) {
14353
- if (!validQueueJobs.includes(jobType)) {
14782
+ if (!VALID_JOB_TYPES.includes(jobType)) {
14354
14783
  return `queue.priority contains invalid job type: ${jobType}`;
14355
14784
  }
14356
14785
  if (typeof value !== "number" || Number.isNaN(value)) {
@@ -14504,8 +14933,8 @@ function createProjectConfigRoutes() {
14504
14933
 
14505
14934
  // ../server/dist/routes/doctor.routes.js
14506
14935
  init_dist();
14507
- import * as fs31 from "fs";
14508
- import * as path32 from "path";
14936
+ import * as fs32 from "fs";
14937
+ import * as path33 from "path";
14509
14938
  import { execSync as execSync7 } from "child_process";
14510
14939
  import { Router as Router4 } from "express";
14511
14940
  function runDoctorChecks(projectDir, config) {
@@ -14538,7 +14967,7 @@ function runDoctorChecks(projectDir, config) {
14538
14967
  });
14539
14968
  }
14540
14969
  try {
14541
- const projectName = path32.basename(projectDir);
14970
+ const projectName = path33.basename(projectDir);
14542
14971
  const marker = generateMarker(projectName);
14543
14972
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
14544
14973
  if (crontabEntries.length > 0) {
@@ -14561,8 +14990,8 @@ function runDoctorChecks(projectDir, config) {
14561
14990
  detail: "Failed to check crontab"
14562
14991
  });
14563
14992
  }
14564
- const configPath = path32.join(projectDir, CONFIG_FILE_NAME);
14565
- if (fs31.existsSync(configPath)) {
14993
+ const configPath = path33.join(projectDir, CONFIG_FILE_NAME);
14994
+ if (fs32.existsSync(configPath)) {
14566
14995
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
14567
14996
  } else {
14568
14997
  checks.push({
@@ -14571,9 +15000,9 @@ function runDoctorChecks(projectDir, config) {
14571
15000
  detail: "Config file not found (using defaults)"
14572
15001
  });
14573
15002
  }
14574
- const prdDir = path32.join(projectDir, config.prdDir);
14575
- if (fs31.existsSync(prdDir)) {
14576
- const prds = fs31.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
15003
+ const prdDir = path33.join(projectDir, config.prdDir);
15004
+ if (fs32.existsSync(prdDir)) {
15005
+ const prds = fs32.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
14577
15006
  checks.push({
14578
15007
  name: "prdDir",
14579
15008
  status: "pass",
@@ -14616,7 +15045,7 @@ function createProjectDoctorRoutes() {
14616
15045
 
14617
15046
  // ../server/dist/routes/log.routes.js
14618
15047
  init_dist();
14619
- import * as path33 from "path";
15048
+ import * as path34 from "path";
14620
15049
  import { Router as Router5 } from "express";
14621
15050
  function createLogRoutes(deps) {
14622
15051
  const { projectDir } = deps;
@@ -14624,7 +15053,7 @@ function createLogRoutes(deps) {
14624
15053
  router.get("/:name", (req, res) => {
14625
15054
  try {
14626
15055
  const { name } = req.params;
14627
- const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
15056
+ const validNames = Object.keys(LOG_FILE_NAMES);
14628
15057
  if (!validNames.includes(name)) {
14629
15058
  res.status(400).json({
14630
15059
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -14635,7 +15064,7 @@ function createLogRoutes(deps) {
14635
15064
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
14636
15065
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
14637
15066
  const fileName = LOG_FILE_NAMES[name] || name;
14638
- const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
15067
+ const logPath = path34.join(projectDir, LOG_DIR, `${fileName}.log`);
14639
15068
  const logLines = getLastLogLines(logPath, linesToRead);
14640
15069
  res.json({ name, lines: logLines });
14641
15070
  } catch (error2) {
@@ -14650,7 +15079,7 @@ function createProjectLogRoutes() {
14650
15079
  try {
14651
15080
  const projectDir = req.projectDir;
14652
15081
  const { name } = req.params;
14653
- const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
15082
+ const validNames = Object.keys(LOG_FILE_NAMES);
14654
15083
  if (!validNames.includes(name)) {
14655
15084
  res.status(400).json({
14656
15085
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -14661,7 +15090,7 @@ function createProjectLogRoutes() {
14661
15090
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
14662
15091
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
14663
15092
  const fileName = LOG_FILE_NAMES[name] || name;
14664
- const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
15093
+ const logPath = path34.join(projectDir, LOG_DIR, `${fileName}.log`);
14665
15094
  const logLines = getLastLogLines(logPath, linesToRead);
14666
15095
  res.json({ name, lines: logLines });
14667
15096
  } catch (error2) {
@@ -14696,7 +15125,7 @@ function createProjectPrdRoutes() {
14696
15125
 
14697
15126
  // ../server/dist/routes/roadmap.routes.js
14698
15127
  init_dist();
14699
- import * as path34 from "path";
15128
+ import * as path35 from "path";
14700
15129
  import { Router as Router7 } from "express";
14701
15130
  function createRoadmapRouteHandlers(ctx) {
14702
15131
  const router = Router7({ mergeParams: true });
@@ -14706,7 +15135,7 @@ function createRoadmapRouteHandlers(ctx) {
14706
15135
  const config = ctx.getConfig(req);
14707
15136
  const projectDir = ctx.getProjectDir(req);
14708
15137
  const status = getRoadmapStatus(projectDir, config);
14709
- const prdDir = path34.join(projectDir, config.prdDir);
15138
+ const prdDir = path35.join(projectDir, config.prdDir);
14710
15139
  const state = loadRoadmapState(prdDir);
14711
15140
  res.json({
14712
15141
  ...status,
@@ -14831,12 +15260,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
14831
15260
  const auditPlan = getSchedulingPlan(projectDir, config, "audit");
14832
15261
  const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
14833
15262
  const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
15263
+ const mergerPlan = getSchedulingPlan(projectDir, config, "merger");
14834
15264
  const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
14835
15265
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
14836
15266
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
14837
15267
  const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
14838
15268
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
14839
15269
  const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
15270
+ const mergerInstalled = installed && (config.merger?.enabled ?? false) && hasScheduledCommand(entries, "merge");
14840
15271
  return {
14841
15272
  executor: {
14842
15273
  schedule: config.cronSchedule,
@@ -14886,6 +15317,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
14886
15317
  manualDelayMinutes: analyticsPlan.manualDelayMinutes,
14887
15318
  balancedDelayMinutes: analyticsPlan.balancedDelayMinutes
14888
15319
  },
15320
+ merger: {
15321
+ schedule: config.merger?.schedule ?? "55 */4 * * *",
15322
+ installed: mergerInstalled,
15323
+ nextRun: mergerInstalled ? addDelayToIsoString(computeNextRun(config.merger?.schedule ?? "55 */4 * * *"), mergerPlan.totalDelayMinutes) : null,
15324
+ delayMinutes: mergerPlan.totalDelayMinutes,
15325
+ manualDelayMinutes: mergerPlan.manualDelayMinutes,
15326
+ balancedDelayMinutes: mergerPlan.balancedDelayMinutes
15327
+ },
14889
15328
  paused: !installed,
14890
15329
  schedulingPriority: config.schedulingPriority,
14891
15330
  entries
@@ -15040,14 +15479,14 @@ function createQueueRoutes(deps) {
15040
15479
  var __filename4 = fileURLToPath5(import.meta.url);
15041
15480
  var __dirname4 = dirname11(__filename4);
15042
15481
  function resolveWebDistPath() {
15043
- const bundled = path35.join(__dirname4, "web");
15044
- if (fs32.existsSync(path35.join(bundled, "index.html")))
15482
+ const bundled = path36.join(__dirname4, "web");
15483
+ if (fs33.existsSync(path36.join(bundled, "index.html")))
15045
15484
  return bundled;
15046
15485
  let d = __dirname4;
15047
15486
  for (let i = 0; i < 8; i++) {
15048
- if (fs32.existsSync(path35.join(d, "turbo.json"))) {
15049
- const dev = path35.join(d, "web/dist");
15050
- if (fs32.existsSync(path35.join(dev, "index.html")))
15487
+ if (fs33.existsSync(path36.join(d, "turbo.json"))) {
15488
+ const dev = path36.join(d, "web/dist");
15489
+ if (fs33.existsSync(path36.join(dev, "index.html")))
15051
15490
  return dev;
15052
15491
  break;
15053
15492
  }
@@ -15057,7 +15496,7 @@ function resolveWebDistPath() {
15057
15496
  }
15058
15497
  function setupStaticFiles(app) {
15059
15498
  const webDistPath = resolveWebDistPath();
15060
- if (fs32.existsSync(webDistPath)) {
15499
+ if (fs33.existsSync(webDistPath)) {
15061
15500
  app.use(express.static(webDistPath));
15062
15501
  }
15063
15502
  app.use((req, res, next) => {
@@ -15065,8 +15504,8 @@ function setupStaticFiles(app) {
15065
15504
  next();
15066
15505
  return;
15067
15506
  }
15068
- const indexPath = path35.resolve(webDistPath, "index.html");
15069
- if (fs32.existsSync(indexPath)) {
15507
+ const indexPath = path36.resolve(webDistPath, "index.html");
15508
+ if (fs33.existsSync(indexPath)) {
15070
15509
  res.sendFile(indexPath, (err) => {
15071
15510
  if (err)
15072
15511
  next();
@@ -15199,7 +15638,7 @@ function createGlobalApp() {
15199
15638
  return app;
15200
15639
  }
15201
15640
  function bootContainer() {
15202
- initContainer(path35.dirname(getDbPath()));
15641
+ initContainer(path36.dirname(getDbPath()));
15203
15642
  }
15204
15643
  function startServer(projectDir, port) {
15205
15644
  bootContainer();
@@ -15252,8 +15691,8 @@ function isProcessRunning2(pid) {
15252
15691
  }
15253
15692
  function readPid(lockPath) {
15254
15693
  try {
15255
- if (!fs33.existsSync(lockPath)) return null;
15256
- const raw = fs33.readFileSync(lockPath, "utf-8").trim();
15694
+ if (!fs34.existsSync(lockPath)) return null;
15695
+ const raw = fs34.readFileSync(lockPath, "utf-8").trim();
15257
15696
  const pid = parseInt(raw, 10);
15258
15697
  return Number.isFinite(pid) ? pid : null;
15259
15698
  } catch {
@@ -15265,10 +15704,10 @@ function acquireServeLock(mode, port) {
15265
15704
  let stalePidCleaned;
15266
15705
  for (let attempt = 0; attempt < 2; attempt++) {
15267
15706
  try {
15268
- const fd = fs33.openSync(lockPath, "wx");
15269
- fs33.writeFileSync(fd, `${process.pid}
15707
+ const fd = fs34.openSync(lockPath, "wx");
15708
+ fs34.writeFileSync(fd, `${process.pid}
15270
15709
  `);
15271
- fs33.closeSync(fd);
15710
+ fs34.closeSync(fd);
15272
15711
  return { acquired: true, lockPath, stalePidCleaned };
15273
15712
  } catch (error2) {
15274
15713
  const err = error2;
@@ -15289,7 +15728,7 @@ function acquireServeLock(mode, port) {
15289
15728
  };
15290
15729
  }
15291
15730
  try {
15292
- fs33.unlinkSync(lockPath);
15731
+ fs34.unlinkSync(lockPath);
15293
15732
  if (existingPid) {
15294
15733
  stalePidCleaned = existingPid;
15295
15734
  }
@@ -15312,10 +15751,10 @@ function acquireServeLock(mode, port) {
15312
15751
  }
15313
15752
  function releaseServeLock(lockPath) {
15314
15753
  try {
15315
- if (!fs33.existsSync(lockPath)) return;
15754
+ if (!fs34.existsSync(lockPath)) return;
15316
15755
  const lockPid = readPid(lockPath);
15317
15756
  if (lockPid !== null && lockPid !== process.pid) return;
15318
- fs33.unlinkSync(lockPath);
15757
+ fs34.unlinkSync(lockPath);
15319
15758
  } catch {
15320
15759
  }
15321
15760
  }
@@ -15411,14 +15850,14 @@ function historyCommand(program2) {
15411
15850
  // src/commands/update.ts
15412
15851
  init_dist();
15413
15852
  import { spawnSync as spawnSync2 } from "child_process";
15414
- import * as fs34 from "fs";
15415
- import * as path36 from "path";
15853
+ import * as fs35 from "fs";
15854
+ import * as path37 from "path";
15416
15855
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
15417
15856
  function parseProjectDirs(projects, cwd) {
15418
15857
  if (!projects || projects.trim().length === 0) {
15419
15858
  return [cwd];
15420
15859
  }
15421
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path36.resolve(cwd, entry));
15860
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path37.resolve(cwd, entry));
15422
15861
  return Array.from(new Set(dirs));
15423
15862
  }
15424
15863
  function shouldInstallGlobal(options) {
@@ -15460,7 +15899,7 @@ function updateCommand(program2) {
15460
15899
  }
15461
15900
  const nightWatchBin = resolveNightWatchBin();
15462
15901
  for (const projectDir of projectDirs) {
15463
- if (!fs34.existsSync(projectDir) || !fs34.statSync(projectDir).isDirectory()) {
15902
+ if (!fs35.existsSync(projectDir) || !fs35.statSync(projectDir).isDirectory()) {
15464
15903
  warn(`Skipping invalid project directory: ${projectDir}`);
15465
15904
  continue;
15466
15905
  }
@@ -15504,8 +15943,8 @@ function prdStateCommand(program2) {
15504
15943
 
15505
15944
  // src/commands/retry.ts
15506
15945
  init_dist();
15507
- import * as fs35 from "fs";
15508
- import * as path37 from "path";
15946
+ import * as fs36 from "fs";
15947
+ import * as path38 from "path";
15509
15948
  function normalizePrdName(name) {
15510
15949
  if (!name.endsWith(".md")) {
15511
15950
  return `${name}.md`;
@@ -15513,26 +15952,26 @@ function normalizePrdName(name) {
15513
15952
  return name;
15514
15953
  }
15515
15954
  function getDonePrds(doneDir) {
15516
- if (!fs35.existsSync(doneDir)) {
15955
+ if (!fs36.existsSync(doneDir)) {
15517
15956
  return [];
15518
15957
  }
15519
- return fs35.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
15958
+ return fs36.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
15520
15959
  }
15521
15960
  function retryCommand(program2) {
15522
15961
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
15523
15962
  const projectDir = process.cwd();
15524
15963
  const config = loadConfig(projectDir);
15525
- const prdDir = path37.join(projectDir, config.prdDir);
15526
- const doneDir = path37.join(prdDir, "done");
15964
+ const prdDir = path38.join(projectDir, config.prdDir);
15965
+ const doneDir = path38.join(prdDir, "done");
15527
15966
  const normalizedPrdName = normalizePrdName(prdName);
15528
- const pendingPath = path37.join(prdDir, normalizedPrdName);
15529
- if (fs35.existsSync(pendingPath)) {
15967
+ const pendingPath = path38.join(prdDir, normalizedPrdName);
15968
+ if (fs36.existsSync(pendingPath)) {
15530
15969
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
15531
15970
  return;
15532
15971
  }
15533
- const donePath = path37.join(doneDir, normalizedPrdName);
15534
- if (fs35.existsSync(donePath)) {
15535
- fs35.renameSync(donePath, pendingPath);
15972
+ const donePath = path38.join(doneDir, normalizedPrdName);
15973
+ if (fs36.existsSync(donePath)) {
15974
+ fs36.renameSync(donePath, pendingPath);
15536
15975
  success(`Moved "${normalizedPrdName}" back to pending.`);
15537
15976
  dim(`From: ${donePath}`);
15538
15977
  dim(`To: ${pendingPath}`);
@@ -15784,7 +16223,7 @@ function prdsCommand(program2) {
15784
16223
 
15785
16224
  // src/commands/cancel.ts
15786
16225
  init_dist();
15787
- import * as fs36 from "fs";
16226
+ import * as fs37 from "fs";
15788
16227
  import * as readline2 from "readline";
15789
16228
  function getLockFilePaths2(projectDir) {
15790
16229
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -15831,7 +16270,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
15831
16270
  const pid = lockStatus.pid;
15832
16271
  if (!lockStatus.running) {
15833
16272
  try {
15834
- fs36.unlinkSync(lockPath);
16273
+ fs37.unlinkSync(lockPath);
15835
16274
  return {
15836
16275
  success: true,
15837
16276
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -15869,7 +16308,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
15869
16308
  await sleep2(3e3);
15870
16309
  if (!isProcessRunning3(pid)) {
15871
16310
  try {
15872
- fs36.unlinkSync(lockPath);
16311
+ fs37.unlinkSync(lockPath);
15873
16312
  } catch {
15874
16313
  }
15875
16314
  return {
@@ -15904,7 +16343,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
15904
16343
  await sleep2(500);
15905
16344
  if (!isProcessRunning3(pid)) {
15906
16345
  try {
15907
- fs36.unlinkSync(lockPath);
16346
+ fs37.unlinkSync(lockPath);
15908
16347
  } catch {
15909
16348
  }
15910
16349
  return {
@@ -15965,67 +16404,69 @@ function cancelCommand(program2) {
15965
16404
 
15966
16405
  // src/commands/slice.ts
15967
16406
  init_dist();
15968
- import * as fs37 from "fs";
15969
- import * as path38 from "path";
16407
+ import * as fs38 from "fs";
16408
+ import * as path39 from "path";
15970
16409
  function plannerLockPath2(projectDir) {
15971
16410
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
15972
16411
  }
15973
16412
  function acquirePlannerLock(projectDir) {
15974
16413
  const lockFile = plannerLockPath2(projectDir);
15975
- if (fs37.existsSync(lockFile)) {
15976
- const pidRaw = fs37.readFileSync(lockFile, "utf-8").trim();
16414
+ if (fs38.existsSync(lockFile)) {
16415
+ const pidRaw = fs38.readFileSync(lockFile, "utf-8").trim();
15977
16416
  const pid = parseInt(pidRaw, 10);
15978
16417
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
15979
16418
  return { acquired: false, lockFile, pid };
15980
16419
  }
15981
16420
  try {
15982
- fs37.unlinkSync(lockFile);
16421
+ fs38.unlinkSync(lockFile);
15983
16422
  } catch {
15984
16423
  }
15985
16424
  }
15986
- fs37.writeFileSync(lockFile, String(process.pid));
16425
+ fs38.writeFileSync(lockFile, String(process.pid));
15987
16426
  return { acquired: true, lockFile };
15988
16427
  }
15989
16428
  function releasePlannerLock(lockFile) {
15990
16429
  try {
15991
- if (fs37.existsSync(lockFile)) {
15992
- fs37.unlinkSync(lockFile);
16430
+ if (fs38.existsSync(lockFile)) {
16431
+ fs38.unlinkSync(lockFile);
15993
16432
  }
15994
16433
  } catch {
15995
16434
  }
15996
16435
  }
15997
16436
  function resolvePlannerIssueColumn(config) {
15998
- return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
16437
+ return config.roadmapScanner.issueColumn === "Draft" ? "Draft" : "Ready";
15999
16438
  }
16000
16439
  function buildPlannerIssueBody(projectDir, config, result) {
16001
- const relativePrdPath = path38.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
16002
- const absolutePrdPath = path38.join(projectDir, config.prdDir, result.file ?? "");
16440
+ const relativePrdPath = path39.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
16441
+ const absolutePrdPath = path39.join(projectDir, config.prdDir, result.file ?? "");
16003
16442
  const sourceItem = result.item;
16004
16443
  let prdContent;
16005
16444
  try {
16006
- prdContent = fs37.readFileSync(absolutePrdPath, "utf-8");
16445
+ prdContent = fs38.readFileSync(absolutePrdPath, "utf-8");
16007
16446
  } catch {
16008
16447
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
16009
16448
  }
16010
16449
  const maxBodyChars = 6e4;
16011
16450
  const truncated = prdContent.length > maxBodyChars;
16012
- const prdPreview = truncated ? `${prdContent.slice(0, maxBodyChars)}
16451
+ const prdBody = truncated ? `${prdContent.slice(0, maxBodyChars)}
16013
16452
 
16014
16453
  ...[truncated]` : prdContent;
16015
- const sourceLines = sourceItem ? [
16016
- `- Source section: ${sourceItem.section}`,
16017
- `- Source item: ${sourceItem.title}`,
16018
- sourceItem.description ? `- Source summary: ${sourceItem.description}` : ""
16019
- ].filter((line) => line.length > 0) : [];
16454
+ const metaLines = [`- PRD file: \`${relativePrdPath}\``];
16455
+ if (sourceItem) {
16456
+ metaLines.push(`- Source section: ${sourceItem.section}`);
16457
+ if (sourceItem.description) {
16458
+ metaLines.push(`- Source summary: ${sourceItem.description}`);
16459
+ }
16460
+ }
16020
16461
  return [
16021
- "## Planner Generated PRD",
16462
+ prdBody,
16022
16463
  "",
16023
- `- PRD file: \`${relativePrdPath}\``,
16024
- ...sourceLines,
16464
+ "<details>",
16465
+ "<summary>Source metadata</summary>",
16025
16466
  "",
16026
- "---",
16467
+ ...metaLines,
16027
16468
  "",
16028
- prdPreview
16469
+ "</details>"
16029
16470
  ].join("\n");
16030
16471
  }
16031
16472
  async function createPlannerIssue(projectDir, config, result) {
@@ -16040,9 +16481,11 @@ async function createPlannerIssue(projectDir, config, result) {
16040
16481
  if (!board) {
16041
16482
  return { created: false, skippedReason: "board-not-configured" };
16042
16483
  }
16484
+ const issueTitle = `PRD: ${result.item.title}`;
16485
+ const normalizeTitle = (t) => t.replace(/^PRD:\s*/i, "").trim().toLowerCase();
16043
16486
  const existingIssues = await provider.getAllIssues();
16044
16487
  const existing = existingIssues.find(
16045
- (issue2) => issue2.title.trim().toLowerCase() === result.item.title.trim().toLowerCase()
16488
+ (issue2) => normalizeTitle(issue2.title) === normalizeTitle(result.item.title)
16046
16489
  );
16047
16490
  if (existing) {
16048
16491
  return {
@@ -16053,7 +16496,7 @@ async function createPlannerIssue(projectDir, config, result) {
16053
16496
  };
16054
16497
  }
16055
16498
  const issue = await provider.createIssue({
16056
- title: result.item.title,
16499
+ title: issueTitle,
16057
16500
  body: buildPlannerIssueBody(projectDir, config, result),
16058
16501
  column: resolvePlannerIssueColumn(config)
16059
16502
  });
@@ -16206,7 +16649,7 @@ function sliceCommand(program2) {
16206
16649
  if (!options.dryRun && result.sliced) {
16207
16650
  await sendNotifications(config, {
16208
16651
  event: "run_succeeded",
16209
- projectName: path38.basename(projectDir),
16652
+ projectName: path39.basename(projectDir),
16210
16653
  exitCode,
16211
16654
  provider: config.provider,
16212
16655
  prTitle: result.item?.title
@@ -16214,7 +16657,7 @@ function sliceCommand(program2) {
16214
16657
  } else if (!options.dryRun && !nothingPending) {
16215
16658
  await sendNotifications(config, {
16216
16659
  event: "run_failed",
16217
- projectName: path38.basename(projectDir),
16660
+ projectName: path39.basename(projectDir),
16218
16661
  exitCode,
16219
16662
  provider: config.provider
16220
16663
  });
@@ -16231,20 +16674,20 @@ function sliceCommand(program2) {
16231
16674
  // src/commands/state.ts
16232
16675
  init_dist();
16233
16676
  import * as os9 from "os";
16234
- import * as path39 from "path";
16677
+ import * as path40 from "path";
16235
16678
  import chalk5 from "chalk";
16236
16679
  import { Command } from "commander";
16237
16680
  function createStateCommand() {
16238
16681
  const state = new Command("state");
16239
16682
  state.description("Manage Night Watch state");
16240
16683
  state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
16241
- const nightWatchHome = process.env.NIGHT_WATCH_HOME || path39.join(os9.homedir(), GLOBAL_CONFIG_DIR);
16684
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path40.join(os9.homedir(), GLOBAL_CONFIG_DIR);
16242
16685
  if (opts.dryRun) {
16243
16686
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
16244
16687
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
16245
- console.log(` ${path39.join(nightWatchHome, "projects.json")}`);
16246
- console.log(` ${path39.join(nightWatchHome, "history.json")}`);
16247
- console.log(` ${path39.join(nightWatchHome, "prd-states.json")}`);
16688
+ console.log(` ${path40.join(nightWatchHome, "projects.json")}`);
16689
+ console.log(` ${path40.join(nightWatchHome, "history.json")}`);
16690
+ console.log(` ${path40.join(nightWatchHome, "prd-states.json")}`);
16248
16691
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
16249
16692
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
16250
16693
  return;
@@ -16282,8 +16725,8 @@ function createStateCommand() {
16282
16725
  init_dist();
16283
16726
  init_dist();
16284
16727
  import { execFileSync as execFileSync6 } from "child_process";
16285
- import * as fs38 from "fs";
16286
- import * as path40 from "path";
16728
+ import * as fs39 from "fs";
16729
+ import * as path41 from "path";
16287
16730
  import * as readline3 from "readline";
16288
16731
  import chalk6 from "chalk";
16289
16732
  async function run(fn) {
@@ -16305,7 +16748,7 @@ function getProvider(config, cwd) {
16305
16748
  return createBoardProvider(bp, cwd);
16306
16749
  }
16307
16750
  function defaultBoardTitle(cwd) {
16308
- return `${path40.basename(cwd)} Night Watch`;
16751
+ return `${path41.basename(cwd)} Night Watch`;
16309
16752
  }
16310
16753
  async function ensureBoardConfigured(config, cwd, provider, options) {
16311
16754
  if (config.boardProvider?.projectNumber) {
@@ -16504,11 +16947,11 @@ function boardCommand(program2) {
16504
16947
  let body = options.body ?? "";
16505
16948
  if (options.bodyFile) {
16506
16949
  const filePath = options.bodyFile;
16507
- if (!fs38.existsSync(filePath)) {
16950
+ if (!fs39.existsSync(filePath)) {
16508
16951
  console.error(`File not found: ${filePath}`);
16509
16952
  process.exit(1);
16510
16953
  }
16511
- body = fs38.readFileSync(filePath, "utf-8");
16954
+ body = fs39.readFileSync(filePath, "utf-8");
16512
16955
  }
16513
16956
  const labels = [];
16514
16957
  if (options.label) {
@@ -16749,12 +17192,12 @@ function boardCommand(program2) {
16749
17192
  const config = loadConfig(cwd);
16750
17193
  const provider = getProvider(config, cwd);
16751
17194
  await ensureBoardConfigured(config, cwd, provider);
16752
- const roadmapPath = options.roadmap ?? path40.join(cwd, "ROADMAP.md");
16753
- if (!fs38.existsSync(roadmapPath)) {
17195
+ const roadmapPath = options.roadmap ?? path41.join(cwd, "ROADMAP.md");
17196
+ if (!fs39.existsSync(roadmapPath)) {
16754
17197
  console.error(`Roadmap file not found: ${roadmapPath}`);
16755
17198
  process.exit(1);
16756
17199
  }
16757
- const roadmapContent = fs38.readFileSync(roadmapPath, "utf-8");
17200
+ const roadmapContent = fs39.readFileSync(roadmapPath, "utf-8");
16758
17201
  const items = parseRoadmap(roadmapContent);
16759
17202
  const uncheckedItems = getUncheckedItems(items);
16760
17203
  if (uncheckedItems.length === 0) {
@@ -16878,11 +17321,11 @@ function boardCommand(program2) {
16878
17321
  // src/commands/queue.ts
16879
17322
  init_dist();
16880
17323
  init_dist();
16881
- import * as path41 from "path";
17324
+ import * as path42 from "path";
16882
17325
  import { spawn as spawn7 } from "child_process";
16883
17326
  import chalk7 from "chalk";
16884
17327
  import { Command as Command2 } from "commander";
16885
- var logger5 = createLogger("queue");
17328
+ var logger6 = createLogger("queue");
16886
17329
  var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer", "planner"];
16887
17330
  function formatTimestamp(unixTs) {
16888
17331
  if (unixTs === null) return "-";
@@ -16998,7 +17441,7 @@ function createQueueCommand() {
16998
17441
  process.exit(1);
16999
17442
  }
17000
17443
  }
17001
- const projectName = path41.basename(projectDir);
17444
+ const projectName = path42.basename(projectDir);
17002
17445
  const queueConfig = loadConfig(projectDir).queue;
17003
17446
  const id = enqueueJob(
17004
17447
  projectDir,
@@ -17031,13 +17474,13 @@ function createQueueCommand() {
17031
17474
  const configDir = _opts.projectDir ?? process.cwd();
17032
17475
  const entry = dispatchNextJob(loadConfig(configDir).queue);
17033
17476
  if (!entry) {
17034
- logger5.info("No pending jobs to dispatch");
17477
+ logger6.info("No pending jobs to dispatch");
17035
17478
  return;
17036
17479
  }
17037
- logger5.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
17480
+ logger6.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
17038
17481
  const scriptName = getScriptNameForJobType(entry.jobType);
17039
17482
  if (!scriptName) {
17040
- logger5.error(`Unknown job type: ${entry.jobType}`);
17483
+ logger6.error(`Unknown job type: ${entry.jobType}`);
17041
17484
  return;
17042
17485
  }
17043
17486
  let projectEnv;
@@ -17056,7 +17499,7 @@ function createQueueCommand() {
17056
17499
  NW_QUEUE_ENTRY_ID: String(entry.id)
17057
17500
  };
17058
17501
  const scriptPath = getScriptPath(scriptName);
17059
- logger5.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
17502
+ logger6.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
17060
17503
  try {
17061
17504
  const child = spawn7("bash", [scriptPath, entry.projectPath], {
17062
17505
  detached: true,
@@ -17065,11 +17508,11 @@ function createQueueCommand() {
17065
17508
  cwd: entry.projectPath
17066
17509
  });
17067
17510
  child.unref();
17068
- logger5.info(`Spawned PID: ${child.pid}`);
17511
+ logger6.info(`Spawned PID: ${child.pid}`);
17069
17512
  markJobRunning(entry.id);
17070
17513
  } catch (error2) {
17071
17514
  updateJobStatus(entry.id, "pending");
17072
- logger5.error(
17515
+ logger6.error(
17073
17516
  `Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
17074
17517
  );
17075
17518
  process.exit(1);
@@ -17084,7 +17527,7 @@ function createQueueCommand() {
17084
17527
  process.exit(1);
17085
17528
  }
17086
17529
  const queueConfig = loadConfig(projectDir).queue;
17087
- const projectName = path41.basename(projectDir);
17530
+ const projectName = path42.basename(projectDir);
17088
17531
  const result = claimJobSlot(
17089
17532
  projectDir,
17090
17533
  projectName,
@@ -17221,7 +17664,7 @@ function notifyCommand(program2) {
17221
17664
 
17222
17665
  // src/commands/summary.ts
17223
17666
  init_dist();
17224
- import path42 from "path";
17667
+ import path43 from "path";
17225
17668
  import chalk8 from "chalk";
17226
17669
  function formatDuration2(seconds) {
17227
17670
  if (seconds === null) return "-";
@@ -17251,7 +17694,7 @@ function formatJobStatus(status) {
17251
17694
  return chalk8.dim(status);
17252
17695
  }
17253
17696
  function getProjectName2(projectPath) {
17254
- return path42.basename(projectPath) || projectPath;
17697
+ return path43.basename(projectPath) || projectPath;
17255
17698
  }
17256
17699
  function formatProvider(providerKey) {
17257
17700
  return providerKey.split(":")[0] || providerKey;
@@ -17370,7 +17813,7 @@ function summaryCommand(program2) {
17370
17813
  // src/commands/resolve.ts
17371
17814
  init_dist();
17372
17815
  import { execFileSync as execFileSync7 } from "child_process";
17373
- import * as path43 from "path";
17816
+ import * as path44 from "path";
17374
17817
  function buildEnvVars6(config, options) {
17375
17818
  const env = buildBaseEnvVars(config, "pr-resolver", options.dryRun);
17376
17819
  env.NW_PR_RESOLVER_MAX_RUNTIME = String(config.prResolver.maxRuntime);
@@ -17500,7 +17943,7 @@ ${stderr}`);
17500
17943
  const notificationEvent = exitCode === 0 ? "pr_resolver_completed" : "pr_resolver_failed";
17501
17944
  await sendNotifications(config, {
17502
17945
  event: notificationEvent,
17503
- projectName: path43.basename(projectDir),
17946
+ projectName: path44.basename(projectDir),
17504
17947
  exitCode,
17505
17948
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
17506
17949
  });
@@ -17513,19 +17956,141 @@ ${stderr}`);
17513
17956
  });
17514
17957
  }
17515
17958
 
17959
+ // src/commands/merge.ts
17960
+ init_dist();
17961
+ import * as path45 from "path";
17962
+ function buildEnvVars7(config, options) {
17963
+ const env = buildBaseEnvVars(config, "merger", options.dryRun);
17964
+ env.NW_MERGER_MAX_RUNTIME = String(config.merger.maxRuntime);
17965
+ env.NW_MERGER_MERGE_METHOD = config.merger.mergeMethod;
17966
+ env.NW_MERGER_MIN_REVIEW_SCORE = String(config.merger.minReviewScore);
17967
+ env.NW_MERGER_BRANCH_PATTERNS = (config.merger.branchPatterns.length > 0 ? config.merger.branchPatterns : config.branchPatterns).join(",");
17968
+ env.NW_MERGER_REBASE_BEFORE_MERGE = config.merger.rebaseBeforeMerge ? "1" : "0";
17969
+ env.NW_MERGER_MAX_PRS_PER_RUN = String(config.merger.maxPrsPerRun);
17970
+ return env;
17971
+ }
17972
+ function applyCliOverrides6(config, options) {
17973
+ const overridden = { ...config, merger: { ...config.merger } };
17974
+ if (options.timeout) {
17975
+ const timeout = parseInt(options.timeout, 10);
17976
+ if (!isNaN(timeout)) {
17977
+ overridden.merger.maxRuntime = timeout;
17978
+ }
17979
+ }
17980
+ if (options.provider) {
17981
+ overridden._cliProviderOverride = options.provider;
17982
+ }
17983
+ return overridden;
17984
+ }
17985
+ function resolveMergeNotificationEvent(exitCode, mergedCount, failedCount) {
17986
+ if (exitCode === 0 && mergedCount > 0) {
17987
+ return "merge_completed";
17988
+ }
17989
+ if (exitCode !== 0 || failedCount > 0) {
17990
+ return "merge_failed";
17991
+ }
17992
+ return null;
17993
+ }
17994
+ function printDryRun(config, envVars, scriptPath, projectDir) {
17995
+ header("Dry Run: Merge Orchestrator");
17996
+ const mergerProvider = resolveJobProvider(config, "merger");
17997
+ header("Configuration");
17998
+ const configTable = createTable({ head: ["Setting", "Value"] });
17999
+ configTable.push(["Provider", mergerProvider]);
18000
+ configTable.push([
18001
+ "Max Runtime",
18002
+ `${config.merger.maxRuntime}s (${Math.floor(config.merger.maxRuntime / 60)}min)`
18003
+ ]);
18004
+ configTable.push(["Merge Method", config.merger.mergeMethod]);
18005
+ configTable.push(["Min Review Score", `${config.merger.minReviewScore}/100`]);
18006
+ configTable.push([
18007
+ "Branch Patterns",
18008
+ config.merger.branchPatterns.length > 0 ? config.merger.branchPatterns.join(", ") : "(top-level)"
18009
+ ]);
18010
+ configTable.push(["Rebase Before Merge", config.merger.rebaseBeforeMerge ? "Yes" : "No"]);
18011
+ configTable.push([
18012
+ "Max PRs Per Run",
18013
+ config.merger.maxPrsPerRun === 0 ? "Unlimited" : String(config.merger.maxPrsPerRun)
18014
+ ]);
18015
+ console.log(configTable.toString());
18016
+ header("Environment Variables");
18017
+ for (const [key, value] of Object.entries(envVars)) {
18018
+ dim(` ${key}=${value}`);
18019
+ }
18020
+ header("Command");
18021
+ dim(` bash ${scriptPath} ${projectDir}`);
18022
+ console.log();
18023
+ }
18024
+ function mergeCommand(program2) {
18025
+ 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) => {
18026
+ const projectDir = process.cwd();
18027
+ let config = loadConfig(projectDir);
18028
+ config = applyCliOverrides6(config, options);
18029
+ if (!config.merger.enabled && !options.dryRun) {
18030
+ info("Merge orchestrator is disabled in config; skipping.");
18031
+ process.exit(0);
18032
+ }
18033
+ const envVars = buildEnvVars7(config, options);
18034
+ const scriptPath = getScriptPath("night-watch-merger-cron.sh");
18035
+ if (options.dryRun) {
18036
+ printDryRun(config, envVars, scriptPath, projectDir);
18037
+ process.exit(0);
18038
+ }
18039
+ const spinner = createSpinner("Running merge orchestrator...");
18040
+ spinner.start();
18041
+ try {
18042
+ await maybeApplyCronSchedulingDelay(config, "merger", projectDir);
18043
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(
18044
+ scriptPath,
18045
+ [projectDir],
18046
+ envVars
18047
+ );
18048
+ const scriptResult = parseScriptResult(`${stdout}
18049
+ ${stderr}`);
18050
+ if (exitCode === 0) {
18051
+ if (scriptResult?.status === "queued") {
18052
+ spinner.succeed("Merge orchestrator queued \u2014 another job is currently running");
18053
+ } else if (scriptResult?.status?.startsWith("skip_")) {
18054
+ spinner.succeed("Merge orchestrator completed (no eligible PRs)");
18055
+ } else {
18056
+ spinner.succeed("Merge orchestrator completed successfully");
18057
+ }
18058
+ } else {
18059
+ spinner.fail(`Merge orchestrator exited with code ${exitCode}`);
18060
+ }
18061
+ const mergedCount = parseInt(scriptResult?.data?.merged ?? "0", 10);
18062
+ const failedCount = parseInt(scriptResult?.data?.failed ?? "0", 10);
18063
+ const notificationEvent = resolveMergeNotificationEvent(exitCode, mergedCount, failedCount);
18064
+ if (notificationEvent) {
18065
+ await sendNotifications(config, {
18066
+ event: notificationEvent,
18067
+ projectName: path45.basename(projectDir),
18068
+ exitCode,
18069
+ provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
18070
+ });
18071
+ }
18072
+ process.exit(exitCode);
18073
+ } catch (err) {
18074
+ spinner.fail("Failed to execute merge command");
18075
+ error(`${err instanceof Error ? err.message : String(err)}`);
18076
+ process.exit(1);
18077
+ }
18078
+ });
18079
+ }
18080
+
17516
18081
  // src/cli.ts
17517
18082
  var __filename5 = fileURLToPath6(import.meta.url);
17518
18083
  var __dirname5 = dirname12(__filename5);
17519
18084
  function findPackageRoot(dir) {
17520
18085
  let d = dir;
17521
18086
  for (let i = 0; i < 5; i++) {
17522
- if (existsSync30(join36(d, "package.json"))) return d;
18087
+ if (existsSync31(join37(d, "package.json"))) return d;
17523
18088
  d = dirname12(d);
17524
18089
  }
17525
18090
  return dir;
17526
18091
  }
17527
18092
  var packageRoot = findPackageRoot(__dirname5);
17528
- var packageJson = JSON.parse(readFileSync19(join36(packageRoot, "package.json"), "utf-8"));
18093
+ var packageJson = JSON.parse(readFileSync20(join37(packageRoot, "package.json"), "utf-8"));
17529
18094
  var program = new Command3();
17530
18095
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
17531
18096
  initCommand(program);
@@ -17556,4 +18121,5 @@ queueCommand(program);
17556
18121
  notifyCommand(program);
17557
18122
  summaryCommand(program);
17558
18123
  resolveCommand(program);
18124
+ mergeCommand(program);
17559
18125
  program.parse();