@jonit-dev/night-watch-cli 1.8.12-beta.0 → 1.8.12-beta.3

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 (137) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.js +1812 -631
  3. package/dist/cli.js.map +1 -0
  4. package/dist/commands/agent.d.ts +12 -0
  5. package/dist/commands/agent.d.ts.map +1 -0
  6. package/dist/commands/agent.js +307 -0
  7. package/dist/commands/agent.js.map +1 -0
  8. package/dist/commands/analytics.d.ts +14 -0
  9. package/dist/commands/analytics.js +69 -0
  10. package/dist/commands/analytics.js.map +1 -0
  11. package/dist/commands/audit.d.ts +19 -0
  12. package/dist/commands/audit.js +144 -0
  13. package/dist/commands/audit.js.map +1 -0
  14. package/dist/commands/board.d.ts +9 -0
  15. package/dist/commands/board.js +702 -0
  16. package/dist/commands/board.js.map +1 -0
  17. package/dist/commands/cancel.d.ts +46 -0
  18. package/dist/commands/cancel.js +239 -0
  19. package/dist/commands/cancel.js.map +1 -0
  20. package/dist/commands/cron.d.ts +8 -0
  21. package/dist/commands/cron.js +134 -0
  22. package/dist/commands/cron.js.map +1 -0
  23. package/dist/commands/dashboard/tab-actions.d.ts +10 -0
  24. package/dist/commands/dashboard/tab-actions.js +247 -0
  25. package/dist/commands/dashboard/tab-actions.js.map +1 -0
  26. package/dist/commands/dashboard/tab-config.d.ts +21 -0
  27. package/dist/commands/dashboard/tab-config.js +874 -0
  28. package/dist/commands/dashboard/tab-config.js.map +1 -0
  29. package/dist/commands/dashboard/tab-logs.d.ts +10 -0
  30. package/dist/commands/dashboard/tab-logs.js +202 -0
  31. package/dist/commands/dashboard/tab-logs.js.map +1 -0
  32. package/dist/commands/dashboard/tab-schedules.d.ts +21 -0
  33. package/dist/commands/dashboard/tab-schedules.js +320 -0
  34. package/dist/commands/dashboard/tab-schedules.js.map +1 -0
  35. package/dist/commands/dashboard/tab-status.d.ts +32 -0
  36. package/dist/commands/dashboard/tab-status.js +424 -0
  37. package/dist/commands/dashboard/tab-status.js.map +1 -0
  38. package/dist/commands/dashboard/types.d.ts +42 -0
  39. package/dist/commands/dashboard/types.js +5 -0
  40. package/dist/commands/dashboard/types.js.map +1 -0
  41. package/dist/commands/dashboard.d.ts +11 -0
  42. package/dist/commands/dashboard.js +242 -0
  43. package/dist/commands/dashboard.js.map +1 -0
  44. package/dist/commands/doctor.d.ts +16 -0
  45. package/dist/commands/doctor.js +195 -0
  46. package/dist/commands/doctor.js.map +1 -0
  47. package/dist/commands/history.d.ts +7 -0
  48. package/dist/commands/history.js +49 -0
  49. package/dist/commands/history.js.map +1 -0
  50. package/dist/commands/init.d.ts +45 -0
  51. package/dist/commands/init.d.ts.map +1 -1
  52. package/dist/commands/init.js +12 -0
  53. package/dist/commands/init.js.map +1 -1
  54. package/dist/commands/install.d.ts +65 -0
  55. package/dist/commands/install.js +405 -0
  56. package/dist/commands/install.js.map +1 -0
  57. package/dist/commands/logs.d.ts +15 -0
  58. package/dist/commands/logs.js +155 -0
  59. package/dist/commands/logs.js.map +1 -0
  60. package/dist/commands/merge.d.ts +26 -0
  61. package/dist/commands/merge.js +159 -0
  62. package/dist/commands/merge.js.map +1 -0
  63. package/dist/commands/notify.d.ts +7 -0
  64. package/dist/commands/notify.js +43 -0
  65. package/dist/commands/notify.js.map +1 -0
  66. package/dist/commands/plan.d.ts +19 -0
  67. package/dist/commands/plan.js +88 -0
  68. package/dist/commands/plan.js.map +1 -0
  69. package/dist/commands/prd-state.d.ts +12 -0
  70. package/dist/commands/prd-state.js +47 -0
  71. package/dist/commands/prd-state.js.map +1 -0
  72. package/dist/commands/prd.d.ts +18 -0
  73. package/dist/commands/prd.js +363 -0
  74. package/dist/commands/prd.js.map +1 -0
  75. package/dist/commands/prds.d.ts +13 -0
  76. package/dist/commands/prds.js +194 -0
  77. package/dist/commands/prds.js.map +1 -0
  78. package/dist/commands/prs.d.ts +14 -0
  79. package/dist/commands/prs.js +104 -0
  80. package/dist/commands/prs.js.map +1 -0
  81. package/dist/commands/qa.d.ts +34 -0
  82. package/dist/commands/qa.js +214 -0
  83. package/dist/commands/qa.js.map +1 -0
  84. package/dist/commands/queue.d.ts +8 -0
  85. package/dist/commands/queue.d.ts.map +1 -1
  86. package/dist/commands/queue.js +398 -0
  87. package/dist/commands/queue.js.map +1 -0
  88. package/dist/commands/resolve.d.ts +26 -0
  89. package/dist/commands/resolve.js +186 -0
  90. package/dist/commands/resolve.js.map +1 -0
  91. package/dist/commands/retry.d.ts +9 -0
  92. package/dist/commands/retry.js +71 -0
  93. package/dist/commands/retry.js.map +1 -0
  94. package/dist/commands/review.d.ts +77 -0
  95. package/dist/commands/review.d.ts.map +1 -1
  96. package/dist/commands/review.js +1 -74
  97. package/dist/commands/review.js.map +1 -1
  98. package/dist/commands/run.d.ts +73 -0
  99. package/dist/commands/serve.d.ts +19 -0
  100. package/dist/commands/serve.js +142 -0
  101. package/dist/commands/serve.js.map +1 -0
  102. package/dist/commands/shared/env-builder.d.ts +49 -0
  103. package/dist/commands/shared/env-builder.d.ts.map +1 -1
  104. package/dist/commands/shared/env-builder.js +152 -0
  105. package/dist/commands/shared/env-builder.js.map +1 -0
  106. package/dist/commands/slice.d.ts +35 -0
  107. package/dist/commands/slice.js +316 -0
  108. package/dist/commands/slice.js.map +1 -0
  109. package/dist/commands/state.d.ts +8 -0
  110. package/dist/commands/state.js +54 -0
  111. package/dist/commands/state.js.map +1 -0
  112. package/dist/commands/status.d.ts +14 -0
  113. package/dist/commands/status.js +297 -0
  114. package/dist/commands/status.js.map +1 -0
  115. package/dist/commands/summary.d.ts +14 -0
  116. package/dist/commands/summary.js +193 -0
  117. package/dist/commands/summary.js.map +1 -0
  118. package/dist/commands/uninstall.d.ts +25 -0
  119. package/dist/commands/uninstall.js +134 -0
  120. package/dist/commands/uninstall.js.map +1 -0
  121. package/dist/commands/update.d.ts +22 -0
  122. package/dist/commands/update.js +90 -0
  123. package/dist/commands/update.js.map +1 -0
  124. package/dist/scripts/night-watch-audit-cron.sh +1 -0
  125. package/dist/scripts/night-watch-cron.sh +1 -0
  126. package/dist/scripts/night-watch-helpers.sh +22 -1
  127. package/dist/scripts/night-watch-merger-cron.sh +32 -3
  128. package/dist/scripts/night-watch-plan-cron.sh +2 -0
  129. package/dist/scripts/night-watch-pr-resolver-cron.sh +6 -0
  130. package/dist/scripts/night-watch-pr-reviewer-cron.sh +18 -5
  131. package/dist/scripts/night-watch-qa-cron.sh +8 -1
  132. package/dist/scripts/night-watch-slicer-cron.sh +3 -0
  133. package/dist/templates/night-watch.config.json +21 -20
  134. package/dist/web/assets/index-C-xpWpS8.css +1 -0
  135. package/dist/web/assets/index-CEYe-290.js +412 -0
  136. package/dist/web/index.html +2 -2
  137. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -64,13 +64,13 @@ function getLockSuffix(jobId) {
64
64
  }
65
65
  function normalizeJobConfig(raw, jobDef) {
66
66
  const readBoolean = (v) => typeof v === "boolean" ? v : void 0;
67
- const readString = (v) => typeof v === "string" ? v : void 0;
67
+ const readString2 = (v) => typeof v === "string" ? v : void 0;
68
68
  const readNumber = (v) => typeof v === "number" && !Number.isNaN(v) ? v : void 0;
69
- const readStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
69
+ const readStringArray2 = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
70
70
  const defaults = jobDef.defaultConfig;
71
71
  const result = {
72
72
  enabled: readBoolean(raw.enabled) ?? defaults.enabled,
73
- schedule: readString(raw.schedule) ?? defaults.schedule,
73
+ schedule: readString2(raw.schedule) ?? defaults.schedule,
74
74
  maxRuntime: readNumber(raw.maxRuntime) ?? defaults.maxRuntime
75
75
  };
76
76
  for (const field of jobDef.extraFields ?? []) {
@@ -79,16 +79,16 @@ function normalizeJobConfig(raw, jobDef) {
79
79
  result[field.name] = readBoolean(raw[field.name]) ?? field.defaultValue;
80
80
  break;
81
81
  case "string":
82
- result[field.name] = readString(raw[field.name]) ?? field.defaultValue;
82
+ result[field.name] = readString2(raw[field.name]) ?? field.defaultValue;
83
83
  break;
84
84
  case "number":
85
85
  result[field.name] = readNumber(raw[field.name]) ?? field.defaultValue;
86
86
  break;
87
87
  case "string[]":
88
- result[field.name] = readStringArray(raw[field.name]) ?? field.defaultValue;
88
+ result[field.name] = readStringArray2(raw[field.name]) ?? field.defaultValue;
89
89
  break;
90
90
  case "enum": {
91
- const val = readString(raw[field.name]);
91
+ const val = readString2(raw[field.name]);
92
92
  result[field.name] = val && field.enumValues?.includes(val) ? val : field.defaultValue;
93
93
  break;
94
94
  }
@@ -390,7 +390,7 @@ function resolveProviderBucketKey(provider, providerEnv) {
390
390
  return `claude-proxy:${baseUrl}`;
391
391
  }
392
392
  }
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;
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, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS, DEFAULT_WEBHOOK_TRIGGERS, 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;
394
394
  var init_constants = __esm({
395
395
  "../core/dist/constants.js"() {
396
396
  "use strict";
@@ -537,6 +537,20 @@ If no issues are warranted, output an empty array: []`;
537
537
  VALID_JOB_TYPES = getValidJobTypes();
538
538
  DEFAULT_JOB_PROVIDERS = {};
539
539
  DEFAULT_PROVIDER_SCHEDULE_OVERRIDES = [];
540
+ DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV = "NIGHT_WATCH_WEBHOOK_SECRET";
541
+ DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS = 300;
542
+ DEFAULT_WEBHOOK_TRIGGERS = {
543
+ enabled: false,
544
+ secretEnv: DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV,
545
+ allowedJobIds: getValidJobTypes(),
546
+ requireTimestamp: false,
547
+ maxSkewSeconds: DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS,
548
+ github: {
549
+ enabled: false,
550
+ events: [],
551
+ rules: []
552
+ }
553
+ };
540
554
  BUILT_IN_PRESETS = {
541
555
  claude: {
542
556
  name: "Claude",
@@ -645,26 +659,26 @@ function validateProvider(value) {
645
659
  function normalizeConfig(rawConfig) {
646
660
  const normalized = {};
647
661
  const hasOwn = (key) => Object.prototype.hasOwnProperty.call(rawConfig, key);
648
- const readString = (value) => typeof value === "string" ? value : void 0;
662
+ const readString2 = (value) => typeof value === "string" ? value : void 0;
649
663
  const readNumber = (value) => typeof value === "number" && !Number.isNaN(value) ? value : void 0;
650
664
  const readBoolean = (value) => typeof value === "boolean" ? value : void 0;
651
- const readStringArray = (value) => Array.isArray(value) && value.every((v) => typeof v === "string") ? value : void 0;
652
- const readObject = (value) => value !== null && typeof value === "object" ? value : void 0;
653
- const cron = readObject(rawConfig.cron);
654
- const review = readObject(rawConfig.review);
655
- const logging = readObject(rawConfig.logging);
656
- normalized.defaultBranch = readString(rawConfig.defaultBranch);
657
- normalized.prdDir = readString(rawConfig.prdDir) ?? readString(rawConfig.prdDirectory);
665
+ const readStringArray2 = (value) => Array.isArray(value) && value.every((v) => typeof v === "string") ? value : void 0;
666
+ const readObject2 = (value) => value !== null && typeof value === "object" ? value : void 0;
667
+ const cron = readObject2(rawConfig.cron);
668
+ const review = readObject2(rawConfig.review);
669
+ const logging = readObject2(rawConfig.logging);
670
+ normalized.defaultBranch = readString2(rawConfig.defaultBranch);
671
+ normalized.prdDir = readString2(rawConfig.prdDir) ?? readString2(rawConfig.prdDirectory);
658
672
  normalized.maxRuntime = readNumber(rawConfig.maxRuntime);
659
673
  normalized.sessionMaxRuntime = readNumber(rawConfig.sessionMaxRuntime);
660
674
  normalized.reviewerMaxRuntime = readNumber(rawConfig.reviewerMaxRuntime);
661
- normalized.branchPrefix = readString(rawConfig.branchPrefix);
662
- normalized.branchPatterns = readStringArray(rawConfig.branchPatterns) ?? readStringArray(review?.branchPatterns);
675
+ normalized.branchPrefix = readString2(rawConfig.branchPrefix);
676
+ normalized.branchPatterns = readStringArray2(rawConfig.branchPatterns) ?? readStringArray2(review?.branchPatterns);
663
677
  normalized.minReviewScore = readNumber(rawConfig.minReviewScore) ?? readNumber(review?.minScore);
664
678
  normalized.maxLogSize = readNumber(rawConfig.maxLogSize) ?? readNumber(logging?.maxLogSize);
665
679
  normalized.gitPushNoVerify = readBoolean(rawConfig.gitPushNoVerify);
666
- normalized.cronSchedule = readString(rawConfig.cronSchedule) ?? readString(cron?.executorSchedule);
667
- normalized.reviewerSchedule = readString(rawConfig.reviewerSchedule) ?? readString(cron?.reviewerSchedule);
680
+ normalized.cronSchedule = readString2(rawConfig.cronSchedule) ?? readString2(cron?.executorSchedule);
681
+ normalized.reviewerSchedule = readString2(rawConfig.reviewerSchedule) ?? readString2(cron?.reviewerSchedule);
668
682
  const rawScheduleBundleId = rawConfig.scheduleBundleId;
669
683
  if (typeof rawScheduleBundleId === "string") {
670
684
  const trimmed = rawScheduleBundleId.trim();
@@ -681,11 +695,11 @@ function normalizeConfig(rawConfig) {
681
695
  normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
682
696
  normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
683
697
  normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
684
- const providerLabelVal = readString(rawConfig.providerLabel);
698
+ const providerLabelVal = readString2(rawConfig.providerLabel);
685
699
  if (providerLabelVal) {
686
700
  normalized.providerLabel = providerLabelVal;
687
701
  }
688
- const rawProviderEnv = readObject(rawConfig.providerEnv);
702
+ const rawProviderEnv = readObject2(rawConfig.providerEnv);
689
703
  if (rawProviderEnv) {
690
704
  const env = {};
691
705
  for (const [key, value] of Object.entries(rawProviderEnv)) {
@@ -697,26 +711,26 @@ function normalizeConfig(rawConfig) {
697
711
  normalized.providerEnv = env;
698
712
  }
699
713
  }
700
- const rawProviderPresets = readObject(rawConfig.providerPresets);
714
+ const rawProviderPresets = readObject2(rawConfig.providerPresets);
701
715
  if (rawProviderPresets) {
702
716
  const presets = {};
703
717
  for (const [presetId, presetVal] of Object.entries(rawProviderPresets)) {
704
- const rawPreset = readObject(presetVal);
718
+ const rawPreset = readObject2(presetVal);
705
719
  if (rawPreset) {
706
- const name = readString(rawPreset.name);
707
- const command = readString(rawPreset.command);
720
+ const name = readString2(rawPreset.name);
721
+ const command = readString2(rawPreset.command);
708
722
  if (name && command) {
709
723
  const preset = {
710
724
  name,
711
725
  command,
712
- subcommand: readString(rawPreset.subcommand),
713
- promptFlag: readString(rawPreset.promptFlag),
714
- autoApproveFlag: readString(rawPreset.autoApproveFlag),
715
- workdirFlag: readString(rawPreset.workdirFlag),
716
- modelFlag: readString(rawPreset.modelFlag),
717
- model: readString(rawPreset.model)
726
+ subcommand: readString2(rawPreset.subcommand),
727
+ promptFlag: readString2(rawPreset.promptFlag),
728
+ autoApproveFlag: readString2(rawPreset.autoApproveFlag),
729
+ workdirFlag: readString2(rawPreset.workdirFlag),
730
+ modelFlag: readString2(rawPreset.modelFlag),
731
+ model: readString2(rawPreset.model)
718
732
  };
719
- const rawEnvVars = readObject(rawPreset.envVars);
733
+ const rawEnvVars = readObject2(rawPreset.envVars);
720
734
  if (rawEnvVars) {
721
735
  const envVars = {};
722
736
  for (const [envKey, envVal] of Object.entries(rawEnvVars)) {
@@ -736,7 +750,7 @@ function normalizeConfig(rawConfig) {
736
750
  normalized.providerPresets = presets;
737
751
  }
738
752
  }
739
- const rawNotifications = readObject(rawConfig.notifications);
753
+ const rawNotifications = readObject2(rawConfig.notifications);
740
754
  if (rawNotifications) {
741
755
  const rawWebhooks = Array.isArray(rawNotifications.webhooks) ? rawNotifications.webhooks : [];
742
756
  const webhooks = [];
@@ -754,18 +768,18 @@ function normalizeConfig(rawConfig) {
754
768
  }
755
769
  normalized.notifications = { webhooks };
756
770
  }
757
- normalized.prdPriority = readStringArray(rawConfig.prdPriority);
758
- const rawRoadmapScanner = readObject(rawConfig.roadmapScanner);
771
+ normalized.prdPriority = readStringArray2(rawConfig.prdPriority);
772
+ const rawRoadmapScanner = readObject2(rawConfig.roadmapScanner);
759
773
  if (rawRoadmapScanner) {
760
- const priorityModeRaw = readString(rawRoadmapScanner.priorityMode);
774
+ const priorityModeRaw = readString2(rawRoadmapScanner.priorityMode);
761
775
  const priorityMode = priorityModeRaw === "roadmap-first" || priorityModeRaw === "audit-first" ? priorityModeRaw : DEFAULT_ROADMAP_SCANNER.priorityMode;
762
- const issueColumnRaw = readString(rawRoadmapScanner.issueColumn);
776
+ const issueColumnRaw = readString2(rawRoadmapScanner.issueColumn);
763
777
  const issueColumn = issueColumnRaw === "Draft" || issueColumnRaw === "Ready" ? issueColumnRaw : DEFAULT_ROADMAP_SCANNER.issueColumn;
764
778
  const roadmapScanner = {
765
779
  enabled: readBoolean(rawRoadmapScanner.enabled) ?? DEFAULT_ROADMAP_SCANNER.enabled,
766
- roadmapPath: readString(rawRoadmapScanner.roadmapPath) ?? DEFAULT_ROADMAP_SCANNER.roadmapPath,
780
+ roadmapPath: readString2(rawRoadmapScanner.roadmapPath) ?? DEFAULT_ROADMAP_SCANNER.roadmapPath,
767
781
  autoScanInterval: readNumber(rawRoadmapScanner.autoScanInterval) ?? DEFAULT_ROADMAP_SCANNER.autoScanInterval,
768
- slicerSchedule: readString(rawRoadmapScanner.slicerSchedule) ?? DEFAULT_ROADMAP_SCANNER.slicerSchedule,
782
+ slicerSchedule: readString2(rawRoadmapScanner.slicerSchedule) ?? DEFAULT_ROADMAP_SCANNER.slicerSchedule,
769
783
  slicerMaxRuntime: readNumber(rawRoadmapScanner.slicerMaxRuntime) ?? DEFAULT_ROADMAP_SCANNER.slicerMaxRuntime,
770
784
  priorityMode,
771
785
  issueColumn
@@ -775,12 +789,12 @@ function normalizeConfig(rawConfig) {
775
789
  }
776
790
  normalized.roadmapScanner = roadmapScanner;
777
791
  }
778
- normalized.templatesDir = readString(rawConfig.templatesDir);
779
- const rawBoardProvider = readObject(rawConfig.boardProvider);
792
+ normalized.templatesDir = readString2(rawConfig.templatesDir);
793
+ const rawBoardProvider = readObject2(rawConfig.boardProvider);
780
794
  if (rawBoardProvider) {
781
795
  const bp = {
782
796
  enabled: readBoolean(rawBoardProvider.enabled) ?? DEFAULT_BOARD_PROVIDER.enabled,
783
- provider: readString(rawBoardProvider.provider) ?? DEFAULT_BOARD_PROVIDER.provider
797
+ provider: readString2(rawBoardProvider.provider) ?? DEFAULT_BOARD_PROVIDER.provider
784
798
  };
785
799
  if (typeof rawBoardProvider.projectNumber === "number") {
786
800
  bp.projectNumber = rawBoardProvider.projectNumber;
@@ -796,7 +810,7 @@ function normalizeConfig(rawConfig) {
796
810
  normalized.primaryFallbackModel = null;
797
811
  normalized.claudeModel = null;
798
812
  } else {
799
- const primaryFallbackModelRaw = readString(rawConfig.primaryFallbackModel);
813
+ const primaryFallbackModelRaw = readString2(rawConfig.primaryFallbackModel);
800
814
  if (primaryFallbackModelRaw && VALID_CLAUDE_MODELS.includes(primaryFallbackModelRaw)) {
801
815
  normalized.primaryFallbackModel = primaryFallbackModelRaw;
802
816
  normalized.claudeModel = primaryFallbackModelRaw;
@@ -806,7 +820,7 @@ function normalizeConfig(rawConfig) {
806
820
  if (rawConfig.claudeModel === null) {
807
821
  normalized.claudeModel = null;
808
822
  } else {
809
- const claudeModelRaw = readString(rawConfig.claudeModel);
823
+ const claudeModelRaw = readString2(rawConfig.claudeModel);
810
824
  if (claudeModelRaw && VALID_CLAUDE_MODELS.includes(claudeModelRaw)) {
811
825
  normalized.primaryFallbackModel = claudeModelRaw;
812
826
  normalized.claudeModel = claudeModelRaw;
@@ -817,16 +831,16 @@ function normalizeConfig(rawConfig) {
817
831
  if (rawConfig.secondaryFallbackModel === null) {
818
832
  normalized.secondaryFallbackModel = null;
819
833
  } else {
820
- const secondaryFallbackModelRaw = readString(rawConfig.secondaryFallbackModel);
834
+ const secondaryFallbackModelRaw = readString2(rawConfig.secondaryFallbackModel);
821
835
  if (secondaryFallbackModelRaw && VALID_CLAUDE_MODELS.includes(secondaryFallbackModelRaw)) {
822
836
  normalized.secondaryFallbackModel = secondaryFallbackModelRaw;
823
837
  }
824
838
  }
825
839
  }
826
- normalized.primaryFallbackPreset = readString(rawConfig.primaryFallbackPreset);
827
- normalized.secondaryFallbackPreset = readString(rawConfig.secondaryFallbackPreset);
840
+ normalized.primaryFallbackPreset = readString2(rawConfig.primaryFallbackPreset);
841
+ normalized.secondaryFallbackPreset = readString2(rawConfig.secondaryFallbackPreset);
828
842
  normalized.autoMerge = readBoolean(rawConfig.autoMerge);
829
- const mergeMethod = readString(rawConfig.autoMergeMethod);
843
+ const mergeMethod = readString2(rawConfig.autoMergeMethod);
830
844
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
831
845
  normalized.autoMergeMethod = mergeMethod;
832
846
  }
@@ -834,23 +848,23 @@ function normalizeConfig(rawConfig) {
834
848
  const jobDef = getJobDef(jobId);
835
849
  if (!jobDef)
836
850
  continue;
837
- const rawJob = readObject(rawConfig[jobId]);
851
+ const rawJob = readObject2(rawConfig[jobId]);
838
852
  if (rawJob) {
839
853
  normalized[jobId] = normalizeJobConfig(rawJob, jobDef);
840
854
  }
841
855
  }
842
856
  const prResolverDef = getJobDef("pr-resolver");
843
857
  if (prResolverDef) {
844
- const rawJob = readObject(rawConfig.prResolver);
858
+ const rawJob = readObject2(rawConfig.prResolver);
845
859
  if (rawJob) {
846
860
  normalized.prResolver = normalizeJobConfig(rawJob, prResolverDef);
847
861
  }
848
862
  }
849
- const rawJobProviders = readObject(rawConfig.jobProviders);
863
+ const rawJobProviders = readObject2(rawConfig.jobProviders);
850
864
  if (rawJobProviders) {
851
865
  const jobProviders = {};
852
866
  for (const jobType of VALID_JOB_TYPES) {
853
- const providerValue = readString(rawJobProviders[jobType]);
867
+ const providerValue = readString2(rawJobProviders[jobType]);
854
868
  if (providerValue && providerValue.trim().length > 0) {
855
869
  jobProviders[jobType] = providerValue.trim();
856
870
  }
@@ -859,7 +873,20 @@ function normalizeConfig(rawConfig) {
859
873
  normalized.jobProviders = jobProviders;
860
874
  }
861
875
  }
862
- const rawScheduleOverrides = readObject(rawConfig.providerScheduleOverrides);
876
+ const rawPausedJobs = readObject2(rawConfig.pausedJobs);
877
+ if (rawPausedJobs) {
878
+ const pausedJobs = {};
879
+ for (const jobType of VALID_JOB_TYPES) {
880
+ const paused = readBoolean(rawPausedJobs[jobType]);
881
+ if (paused !== void 0) {
882
+ pausedJobs[jobType] = paused;
883
+ }
884
+ }
885
+ if (Object.keys(pausedJobs).length > 0) {
886
+ normalized.pausedJobs = pausedJobs;
887
+ }
888
+ }
889
+ const rawScheduleOverrides = readObject2(rawConfig.providerScheduleOverrides);
863
890
  if (rawScheduleOverrides) {
864
891
  if (Array.isArray(rawScheduleOverrides)) {
865
892
  const overrides = [];
@@ -867,10 +894,10 @@ function normalizeConfig(rawConfig) {
867
894
  for (const rawOverride of rawScheduleOverrides) {
868
895
  if (rawOverride && typeof rawOverride === "object") {
869
896
  const overrideObj = rawOverride;
870
- const label2 = readString(overrideObj.label);
871
- const presetId = readString(overrideObj.presetId);
872
- const startTime = readString(overrideObj.startTime);
873
- const endTime = readString(overrideObj.endTime);
897
+ const label2 = readString2(overrideObj.label);
898
+ const presetId = readString2(overrideObj.presetId);
899
+ const startTime = readString2(overrideObj.startTime);
900
+ const endTime = readString2(overrideObj.endTime);
874
901
  const rawDays = overrideObj.days;
875
902
  const rawJobTypes = overrideObj.jobTypes;
876
903
  const enabled = readBoolean(overrideObj.enabled);
@@ -920,9 +947,9 @@ function normalizeConfig(rawConfig) {
920
947
  }
921
948
  }
922
949
  }
923
- const rawQueue = readObject(rawConfig.queue);
950
+ const rawQueue = readObject2(rawConfig.queue);
924
951
  if (rawQueue) {
925
- const rawMode = readString(rawQueue.mode);
952
+ const rawMode = readString2(rawQueue.mode);
926
953
  const mode = rawMode === "conservative" || rawMode === "provider-aware" || rawMode === "auto" ? rawMode : DEFAULT_QUEUE.mode;
927
954
  const queue = {
928
955
  enabled: readBoolean(rawQueue.enabled) ?? DEFAULT_QUEUE.enabled,
@@ -932,7 +959,7 @@ function normalizeConfig(rawConfig) {
932
959
  priority: { ...DEFAULT_QUEUE.priority },
933
960
  providerBuckets: {}
934
961
  };
935
- const rawPriority = readObject(rawQueue.priority);
962
+ const rawPriority = readObject2(rawQueue.priority);
936
963
  if (rawPriority) {
937
964
  for (const jobType of VALID_JOB_TYPES) {
938
965
  const prio = readNumber(rawPriority[jobType]);
@@ -941,10 +968,10 @@ function normalizeConfig(rawConfig) {
941
968
  }
942
969
  }
943
970
  }
944
- const rawProviderBuckets = readObject(rawQueue.providerBuckets);
971
+ const rawProviderBuckets = readObject2(rawQueue.providerBuckets);
945
972
  if (rawProviderBuckets) {
946
973
  for (const [bucketKey, bucketVal] of Object.entries(rawProviderBuckets)) {
947
- const rawBucket = readObject(bucketVal);
974
+ const rawBucket = readObject2(bucketVal);
948
975
  if (rawBucket) {
949
976
  const maxConcurrency = readNumber(rawBucket.maxConcurrency);
950
977
  if (maxConcurrency !== void 0) {
@@ -1274,9 +1301,113 @@ function getDefaultConfig() {
1274
1301
  merger: { ...DEFAULT_MERGER },
1275
1302
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
1276
1303
  providerScheduleOverrides: [...DEFAULT_PROVIDER_SCHEDULE_OVERRIDES],
1277
- queue: { ...DEFAULT_QUEUE }
1304
+ queue: { ...DEFAULT_QUEUE },
1305
+ pausedJobs: {},
1306
+ webhookTriggers: cloneWebhookTriggers(DEFAULT_WEBHOOK_TRIGGERS)
1307
+ };
1308
+ }
1309
+ function cloneWebhookTriggers(config) {
1310
+ return {
1311
+ ...config,
1312
+ allowedJobIds: [...config.allowedJobIds],
1313
+ github: {
1314
+ ...config.github,
1315
+ events: [...config.github.events],
1316
+ rules: config.github.rules.map((rule) => {
1317
+ const cloned = { ...rule };
1318
+ if (rule.branchPatterns) {
1319
+ cloned.branchPatterns = [...rule.branchPatterns];
1320
+ }
1321
+ return cloned;
1322
+ })
1323
+ }
1278
1324
  };
1279
1325
  }
1326
+ function isPlainObject(value) {
1327
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1328
+ }
1329
+ function isValidJobType(value) {
1330
+ return typeof value === "string" && getJobDef(value) !== void 0;
1331
+ }
1332
+ function readStringArray(value) {
1333
+ if (!Array.isArray(value))
1334
+ return void 0;
1335
+ const strings = value.filter((item) => typeof item === "string");
1336
+ return strings.length > 0 ? strings : [];
1337
+ }
1338
+ function normalizeWebhookGithubRules(value) {
1339
+ if (!Array.isArray(value))
1340
+ return void 0;
1341
+ const rules = [];
1342
+ for (const item of value) {
1343
+ if (!isPlainObject(item))
1344
+ continue;
1345
+ const event = typeof item.event === "string" ? item.event.trim() : "";
1346
+ if (!event || !isValidJobType(item.jobId))
1347
+ continue;
1348
+ const rule = {
1349
+ event,
1350
+ jobId: item.jobId
1351
+ };
1352
+ if (typeof item.action === "string" && item.action.trim()) {
1353
+ rule.action = item.action.trim();
1354
+ }
1355
+ const branchPatterns = readStringArray(item.branchPatterns);
1356
+ if (branchPatterns !== void 0) {
1357
+ rule.branchPatterns = branchPatterns;
1358
+ }
1359
+ if (typeof item.onlyOnFailure === "boolean") {
1360
+ rule.onlyOnFailure = item.onlyOnFailure;
1361
+ }
1362
+ rules.push(rule);
1363
+ }
1364
+ return rules;
1365
+ }
1366
+ function normalizeWebhookTriggersConfig(value) {
1367
+ if (!isPlainObject(value))
1368
+ return void 0;
1369
+ const config = cloneWebhookTriggers(DEFAULT_WEBHOOK_TRIGGERS);
1370
+ if (typeof value.enabled === "boolean") {
1371
+ config.enabled = value.enabled;
1372
+ }
1373
+ if (typeof value.secretEnv === "string") {
1374
+ config.secretEnv = value.secretEnv.trim();
1375
+ }
1376
+ const allowedJobIds = readStringArray(value.allowedJobIds);
1377
+ if (allowedJobIds !== void 0) {
1378
+ config.allowedJobIds = allowedJobIds.filter(isValidJobType);
1379
+ }
1380
+ if (typeof value.requireTimestamp === "boolean") {
1381
+ config.requireTimestamp = value.requireTimestamp;
1382
+ }
1383
+ if (typeof value.maxSkewSeconds === "number" && Number.isFinite(value.maxSkewSeconds)) {
1384
+ const n = Math.floor(value.maxSkewSeconds);
1385
+ config.maxSkewSeconds = n > 0 ? n : DEFAULT_WEBHOOK_TRIGGERS.maxSkewSeconds;
1386
+ }
1387
+ if (isPlainObject(value.github)) {
1388
+ if (typeof value.github.enabled === "boolean") {
1389
+ config.github.enabled = value.github.enabled;
1390
+ }
1391
+ const events = readStringArray(value.github.events);
1392
+ if (events !== void 0) {
1393
+ config.github.events = events.map((event) => event.trim()).filter(Boolean);
1394
+ }
1395
+ const rules = normalizeWebhookGithubRules(value.github.rules);
1396
+ if (rules !== void 0) {
1397
+ config.github.rules = rules;
1398
+ }
1399
+ }
1400
+ return config;
1401
+ }
1402
+ function validateWebhookTriggers(config) {
1403
+ const validated = cloneWebhookTriggers(config);
1404
+ validated.allowedJobIds = validated.allowedJobIds.filter(isValidJobType);
1405
+ validated.github.rules = validated.github.rules.filter((rule) => isValidJobType(rule.jobId));
1406
+ if (validated.enabled && validated.secretEnv.trim().length === 0) {
1407
+ throw new Error("webhookTriggers.secretEnv must be non-empty when webhook triggers are enabled");
1408
+ }
1409
+ return validated;
1410
+ }
1280
1411
  function loadConfigFile(configPath) {
1281
1412
  try {
1282
1413
  if (!fs.existsSync(configPath)) {
@@ -1284,7 +1415,12 @@ function loadConfigFile(configPath) {
1284
1415
  }
1285
1416
  const content = fs.readFileSync(configPath, "utf-8");
1286
1417
  const rawConfig = JSON.parse(content);
1287
- return normalizeConfig(rawConfig);
1418
+ const normalized = normalizeConfig(rawConfig);
1419
+ const webhookTriggers = normalizeWebhookTriggersConfig(rawConfig.webhookTriggers);
1420
+ if (webhookTriggers) {
1421
+ normalized.webhookTriggers = webhookTriggers;
1422
+ }
1423
+ return normalized;
1288
1424
  } catch (error2) {
1289
1425
  console.warn(`Warning: Could not parse config file at ${configPath}: ${error2 instanceof Error ? error2.message : String(error2)}`);
1290
1426
  return null;
@@ -1339,6 +1475,26 @@ function mergeConfigLayer(base, layer) {
1339
1475
  ...layerQueue,
1340
1476
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
1341
1477
  };
1478
+ } else if (_key === "webhookTriggers") {
1479
+ const baseWebhook = base[_key];
1480
+ const layerWebhook = value;
1481
+ base[_key] = {
1482
+ ...baseWebhook,
1483
+ ...layerWebhook,
1484
+ allowedJobIds: [...layerWebhook.allowedJobIds],
1485
+ github: {
1486
+ ...baseWebhook.github,
1487
+ ...layerWebhook.github,
1488
+ events: [...layerWebhook.github.events],
1489
+ rules: layerWebhook.github.rules.map((rule) => {
1490
+ const cloned = { ...rule };
1491
+ if (rule.branchPatterns) {
1492
+ cloned.branchPatterns = [...rule.branchPatterns];
1493
+ }
1494
+ return cloned;
1495
+ })
1496
+ }
1497
+ };
1342
1498
  } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics" || _key === "prResolver" || _key === "merger") {
1343
1499
  base[_key] = {
1344
1500
  ...base[_key],
@@ -1379,6 +1535,7 @@ function mergeConfigs(base, fileConfig, envConfig) {
1379
1535
  if (merged.claudeModel === void 0) {
1380
1536
  merged.claudeModel = merged.primaryFallbackModel === void 0 ? DEFAULT_CLAUDE_MODEL : merged.primaryFallbackModel;
1381
1537
  }
1538
+ merged.webhookTriggers = validateWebhookTriggers(merged.webhookTriggers);
1382
1539
  return merged;
1383
1540
  }
1384
1541
  function loadConfig(projectDir) {
@@ -1492,6 +1649,7 @@ var init_config = __esm({
1492
1649
  init_constants();
1493
1650
  init_config_normalize();
1494
1651
  init_config_env();
1652
+ init_job_registry();
1495
1653
  init_config_normalize();
1496
1654
  }
1497
1655
  });
@@ -1895,14 +2053,350 @@ var init_roadmap_state_repository = __esm({
1895
2053
  }
1896
2054
  });
1897
2055
 
2056
+ // ../core/src/board/types.ts
2057
+ var BOARD_COLUMNS2;
2058
+ var init_types3 = __esm({
2059
+ "../core/src/board/types.ts"() {
2060
+ "use strict";
2061
+ BOARD_COLUMNS2 = ["Draft", "Ready", "In Progress", "Review", "Done"];
2062
+ }
2063
+ });
2064
+
2065
+ // ../core/src/jobs/job-registry.ts
2066
+ function getValidJobTypes2() {
2067
+ return JOB_REGISTRY2.map((job) => job.id);
2068
+ }
2069
+ function getDefaultQueuePriority2() {
2070
+ const result = {};
2071
+ for (const job of JOB_REGISTRY2) {
2072
+ result[job.id] = job.queuePriority;
2073
+ }
2074
+ return result;
2075
+ }
2076
+ function getLogFileNames2() {
2077
+ const result = {};
2078
+ for (const job of JOB_REGISTRY2) {
2079
+ result[job.id] = job.logName;
2080
+ if (job.cliCommand !== job.id) {
2081
+ result[job.cliCommand] = job.logName;
2082
+ }
2083
+ }
2084
+ return result;
2085
+ }
2086
+ var JOB_REGISTRY2, JOB_MAP2;
2087
+ var init_job_registry2 = __esm({
2088
+ "../core/src/jobs/job-registry.ts"() {
2089
+ "use strict";
2090
+ init_types3();
2091
+ JOB_REGISTRY2 = [
2092
+ {
2093
+ id: "executor",
2094
+ name: "Executor",
2095
+ description: "Creates implementation PRs from PRDs",
2096
+ cliCommand: "run",
2097
+ logName: "executor",
2098
+ lockSuffix: ".lock",
2099
+ queuePriority: 50,
2100
+ envPrefix: "NW_EXECUTOR",
2101
+ defaultConfig: {
2102
+ enabled: true,
2103
+ schedule: "5 */2 * * *",
2104
+ maxRuntime: 7200
2105
+ }
2106
+ },
2107
+ {
2108
+ id: "reviewer",
2109
+ name: "Reviewer",
2110
+ description: "Reviews and improves PRs on night-watch branches",
2111
+ cliCommand: "review",
2112
+ logName: "reviewer",
2113
+ lockSuffix: "-r.lock",
2114
+ queuePriority: 40,
2115
+ envPrefix: "NW_REVIEWER",
2116
+ defaultConfig: {
2117
+ enabled: true,
2118
+ schedule: "25 */3 * * *",
2119
+ maxRuntime: 3600
2120
+ }
2121
+ },
2122
+ {
2123
+ id: "pr-resolver",
2124
+ name: "PR Conflict Solver",
2125
+ description: "Resolves merge conflicts via AI rebase; optionally addresses review comments and labels PRs ready-to-merge",
2126
+ cliCommand: "resolve",
2127
+ logName: "pr-resolver",
2128
+ lockSuffix: "-pr-resolver.lock",
2129
+ queuePriority: 35,
2130
+ envPrefix: "NW_PR_RESOLVER",
2131
+ extraFields: [
2132
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
2133
+ { name: "maxPrsPerRun", type: "number", defaultValue: 0 },
2134
+ { name: "perPrTimeout", type: "number", defaultValue: 600 },
2135
+ { name: "aiConflictResolution", type: "boolean", defaultValue: true },
2136
+ { name: "aiReviewResolution", type: "boolean", defaultValue: false },
2137
+ { name: "readyLabel", type: "string", defaultValue: "ready-to-merge" }
2138
+ ],
2139
+ defaultConfig: {
2140
+ enabled: true,
2141
+ schedule: "15 6,14,22 * * *",
2142
+ maxRuntime: 3600,
2143
+ branchPatterns: [],
2144
+ maxPrsPerRun: 0,
2145
+ perPrTimeout: 600,
2146
+ aiConflictResolution: true,
2147
+ aiReviewResolution: false,
2148
+ readyLabel: "ready-to-merge"
2149
+ }
2150
+ },
2151
+ {
2152
+ id: "slicer",
2153
+ name: "Slicer",
2154
+ description: "Generates PRDs from roadmap items",
2155
+ cliCommand: "planner",
2156
+ logName: "slicer",
2157
+ lockSuffix: "-slicer.lock",
2158
+ queuePriority: 30,
2159
+ envPrefix: "NW_SLICER",
2160
+ defaultConfig: {
2161
+ enabled: true,
2162
+ schedule: "35 */6 * * *",
2163
+ maxRuntime: 600
2164
+ }
2165
+ },
2166
+ {
2167
+ id: "qa",
2168
+ name: "QA",
2169
+ description: "Runs end-to-end tests on PRs",
2170
+ cliCommand: "qa",
2171
+ logName: "night-watch-qa",
2172
+ lockSuffix: "-qa.lock",
2173
+ queuePriority: 20,
2174
+ envPrefix: "NW_QA",
2175
+ extraFields: [
2176
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
2177
+ {
2178
+ name: "artifacts",
2179
+ type: "enum",
2180
+ enumValues: ["screenshot", "video", "both"],
2181
+ defaultValue: "both"
2182
+ },
2183
+ { name: "skipLabel", type: "string", defaultValue: "skip-qa" },
2184
+ { name: "autoInstallPlaywright", type: "boolean", defaultValue: true },
2185
+ { name: "validatedLabel", type: "string", defaultValue: "e2e-validated" }
2186
+ ],
2187
+ defaultConfig: {
2188
+ enabled: true,
2189
+ schedule: "45 2,10,18 * * *",
2190
+ maxRuntime: 3600,
2191
+ branchPatterns: [],
2192
+ artifacts: "both",
2193
+ skipLabel: "skip-qa",
2194
+ autoInstallPlaywright: true,
2195
+ validatedLabel: "e2e-validated"
2196
+ }
2197
+ },
2198
+ {
2199
+ id: "audit",
2200
+ name: "Auditor",
2201
+ description: "Performs code audits and creates issues for findings",
2202
+ cliCommand: "audit",
2203
+ logName: "audit",
2204
+ lockSuffix: "-audit.lock",
2205
+ queuePriority: 10,
2206
+ envPrefix: "NW_AUDIT",
2207
+ extraFields: [
2208
+ {
2209
+ name: "targetColumn",
2210
+ type: "enum",
2211
+ enumValues: [...BOARD_COLUMNS2],
2212
+ defaultValue: "Draft"
2213
+ }
2214
+ ],
2215
+ defaultConfig: {
2216
+ enabled: true,
2217
+ schedule: "50 3 * * 1",
2218
+ maxRuntime: 1800,
2219
+ targetColumn: "Draft"
2220
+ }
2221
+ },
2222
+ {
2223
+ id: "analytics",
2224
+ name: "Analytics",
2225
+ description: "Analyzes product analytics and creates issues for trends",
2226
+ cliCommand: "analytics",
2227
+ logName: "analytics",
2228
+ lockSuffix: "-analytics.lock",
2229
+ queuePriority: 10,
2230
+ envPrefix: "NW_ANALYTICS",
2231
+ extraFields: [
2232
+ { name: "lookbackDays", type: "number", defaultValue: 7 },
2233
+ {
2234
+ name: "targetColumn",
2235
+ type: "enum",
2236
+ enumValues: [...BOARD_COLUMNS2],
2237
+ defaultValue: "Draft"
2238
+ },
2239
+ { name: "analysisPrompt", type: "string", defaultValue: "" }
2240
+ ],
2241
+ defaultConfig: {
2242
+ enabled: false,
2243
+ schedule: "0 6 * * 1",
2244
+ maxRuntime: 900,
2245
+ lookbackDays: 7,
2246
+ targetColumn: "Draft",
2247
+ analysisPrompt: ""
2248
+ }
2249
+ },
2250
+ {
2251
+ id: "merger",
2252
+ name: "Merge Orchestrator",
2253
+ description: "Repo-wide PR merge coordinator \u2014 scans, rebases, and merges in FIFO order",
2254
+ cliCommand: "merge",
2255
+ logName: "merger",
2256
+ lockSuffix: "-merger.lock",
2257
+ queuePriority: 45,
2258
+ envPrefix: "NW_MERGER",
2259
+ extraFields: [
2260
+ {
2261
+ name: "mergeMethod",
2262
+ type: "enum",
2263
+ enumValues: ["squash", "merge", "rebase"],
2264
+ defaultValue: "squash"
2265
+ },
2266
+ { name: "minReviewScore", type: "number", defaultValue: 80 },
2267
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
2268
+ { name: "rebaseBeforeMerge", type: "boolean", defaultValue: true },
2269
+ { name: "maxPrsPerRun", type: "number", defaultValue: 0 }
2270
+ ],
2271
+ defaultConfig: {
2272
+ enabled: false,
2273
+ schedule: "55 */4 * * *",
2274
+ maxRuntime: 1800,
2275
+ mergeMethod: "squash",
2276
+ minReviewScore: 80,
2277
+ branchPatterns: [],
2278
+ rebaseBeforeMerge: true,
2279
+ maxPrsPerRun: 0
2280
+ }
2281
+ }
2282
+ ];
2283
+ JOB_MAP2 = new Map(JOB_REGISTRY2.map((job) => [job.id, job]));
2284
+ }
2285
+ });
2286
+
2287
+ // ../core/src/constants.ts
2288
+ var DEFAULT_LOCAL_BOARD_INFO2, VALID_JOB_TYPES2, DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV2, DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS2, DEFAULT_WEBHOOK_TRIGGERS2, BUILT_IN_PRESETS2, BUILT_IN_PRESET_IDS2, PROVIDER_COMMANDS2, LOG_FILE_NAMES2, GLOBAL_CONFIG_DIR2, STATE_DB_FILE_NAME2, DEFAULT_QUEUE_ENABLED2, DEFAULT_QUEUE_MODE2, DEFAULT_QUEUE_MAX_CONCURRENCY2, DEFAULT_QUEUE_MAX_WAIT_TIME2, DEFAULT_QUEUE_PRIORITY2, DEFAULT_QUEUE2;
2289
+ var init_constants2 = __esm({
2290
+ "../core/src/constants.ts"() {
2291
+ "use strict";
2292
+ init_job_registry2();
2293
+ DEFAULT_LOCAL_BOARD_INFO2 = { id: "local", number: 0, title: "Local Kanban", url: "" };
2294
+ VALID_JOB_TYPES2 = getValidJobTypes2();
2295
+ DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV2 = "NIGHT_WATCH_WEBHOOK_SECRET";
2296
+ DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS2 = 300;
2297
+ DEFAULT_WEBHOOK_TRIGGERS2 = {
2298
+ enabled: false,
2299
+ secretEnv: DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV2,
2300
+ allowedJobIds: getValidJobTypes2(),
2301
+ requireTimestamp: false,
2302
+ maxSkewSeconds: DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS2,
2303
+ github: {
2304
+ enabled: false,
2305
+ events: [],
2306
+ rules: []
2307
+ }
2308
+ };
2309
+ BUILT_IN_PRESETS2 = {
2310
+ claude: {
2311
+ name: "Claude",
2312
+ command: "claude",
2313
+ promptFlag: "-p",
2314
+ autoApproveFlag: "--dangerously-skip-permissions"
2315
+ },
2316
+ "claude-sonnet-4-6": {
2317
+ name: "Claude Sonnet 4.6",
2318
+ command: "claude",
2319
+ promptFlag: "-p",
2320
+ autoApproveFlag: "--dangerously-skip-permissions",
2321
+ modelFlag: "--model",
2322
+ model: "claude-sonnet-4-6"
2323
+ },
2324
+ "claude-opus-4-6": {
2325
+ name: "Claude Opus 4.6",
2326
+ command: "claude",
2327
+ promptFlag: "-p",
2328
+ autoApproveFlag: "--dangerously-skip-permissions",
2329
+ modelFlag: "--model",
2330
+ model: "claude-opus-4-6"
2331
+ },
2332
+ codex: {
2333
+ name: "Codex",
2334
+ command: "codex",
2335
+ subcommand: "exec",
2336
+ autoApproveFlag: "--yolo",
2337
+ workdirFlag: "-C"
2338
+ },
2339
+ "glm-47": {
2340
+ name: "GLM-4.7",
2341
+ command: "claude",
2342
+ promptFlag: "-p",
2343
+ autoApproveFlag: "--dangerously-skip-permissions",
2344
+ modelFlag: "--model",
2345
+ model: "glm-4.7",
2346
+ envVars: {
2347
+ ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
2348
+ API_TIMEOUT_MS: "3000000",
2349
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
2350
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7"
2351
+ }
2352
+ },
2353
+ "glm-5": {
2354
+ name: "GLM-5",
2355
+ command: "claude",
2356
+ promptFlag: "-p",
2357
+ autoApproveFlag: "--dangerously-skip-permissions",
2358
+ modelFlag: "--model",
2359
+ model: "glm-5",
2360
+ envVars: {
2361
+ ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
2362
+ API_TIMEOUT_MS: "3000000",
2363
+ ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5",
2364
+ ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5"
2365
+ }
2366
+ }
2367
+ };
2368
+ BUILT_IN_PRESET_IDS2 = Object.keys(BUILT_IN_PRESETS2);
2369
+ PROVIDER_COMMANDS2 = {
2370
+ claude: BUILT_IN_PRESETS2.claude.command,
2371
+ codex: BUILT_IN_PRESETS2.codex.command
2372
+ };
2373
+ LOG_FILE_NAMES2 = getLogFileNames2();
2374
+ GLOBAL_CONFIG_DIR2 = ".night-watch";
2375
+ STATE_DB_FILE_NAME2 = "state.db";
2376
+ DEFAULT_QUEUE_ENABLED2 = true;
2377
+ DEFAULT_QUEUE_MODE2 = "auto";
2378
+ DEFAULT_QUEUE_MAX_CONCURRENCY2 = 3;
2379
+ DEFAULT_QUEUE_MAX_WAIT_TIME2 = 7200;
2380
+ DEFAULT_QUEUE_PRIORITY2 = getDefaultQueuePriority2();
2381
+ DEFAULT_QUEUE2 = {
2382
+ enabled: DEFAULT_QUEUE_ENABLED2,
2383
+ mode: DEFAULT_QUEUE_MODE2,
2384
+ maxConcurrency: DEFAULT_QUEUE_MAX_CONCURRENCY2,
2385
+ maxWaitTime: DEFAULT_QUEUE_MAX_WAIT_TIME2,
2386
+ priority: { ...DEFAULT_QUEUE_PRIORITY2 },
2387
+ providerBuckets: {}
2388
+ };
2389
+ }
2390
+ });
2391
+
1898
2392
  // ../core/dist/storage/sqlite/client.js
1899
2393
  import * as fs2 from "fs";
1900
2394
  import * as os from "os";
1901
2395
  import * as path2 from "path";
1902
2396
  import Database6 from "better-sqlite3";
1903
2397
  function getDbPath() {
1904
- const base = process.env.NIGHT_WATCH_HOME || path2.join(os.homedir(), GLOBAL_CONFIG_DIR);
1905
- return path2.join(base, STATE_DB_FILE_NAME);
2398
+ const base = process.env.NIGHT_WATCH_HOME || path2.join(os.homedir(), GLOBAL_CONFIG_DIR2);
2399
+ return path2.join(base, STATE_DB_FILE_NAME2);
1906
2400
  }
1907
2401
  function getDb() {
1908
2402
  if (_db) {
@@ -1924,7 +2418,7 @@ function closeDb() {
1924
2418
  }
1925
2419
  function createDbForDir(projectDir) {
1926
2420
  fs2.mkdirSync(projectDir, { recursive: true });
1927
- const dbPath = path2.join(projectDir, STATE_DB_FILE_NAME);
2421
+ const dbPath = path2.join(projectDir, STATE_DB_FILE_NAME2);
1928
2422
  const db = new Database6(dbPath);
1929
2423
  db.pragma("journal_mode = WAL");
1930
2424
  db.pragma("busy_timeout = 5000");
@@ -1934,7 +2428,7 @@ var _db;
1934
2428
  var init_client = __esm({
1935
2429
  "../core/dist/storage/sqlite/client.js"() {
1936
2430
  "use strict";
1937
- init_constants();
2431
+ init_constants2();
1938
2432
  _db = null;
1939
2433
  }
1940
2434
  });
@@ -2896,21 +3390,21 @@ var LocalKanbanProvider;
2896
3390
  var init_local_kanban = __esm({
2897
3391
  "../core/dist/board/providers/local-kanban.js"() {
2898
3392
  "use strict";
2899
- init_types2();
2900
- init_constants();
3393
+ init_types3();
3394
+ init_constants2();
2901
3395
  LocalKanbanProvider = class {
2902
3396
  repo;
2903
3397
  constructor(repo) {
2904
3398
  this.repo = repo;
2905
3399
  }
2906
3400
  async setupBoard(title) {
2907
- return { ...DEFAULT_LOCAL_BOARD_INFO, title };
3401
+ return { ...DEFAULT_LOCAL_BOARD_INFO2, title };
2908
3402
  }
2909
3403
  async getBoard() {
2910
- return DEFAULT_LOCAL_BOARD_INFO;
3404
+ return DEFAULT_LOCAL_BOARD_INFO2;
2911
3405
  }
2912
3406
  async getColumns() {
2913
- return BOARD_COLUMNS.map((name, i) => ({ id: String(i), name }));
3407
+ return BOARD_COLUMNS2.map((name, i) => ({ id: String(i), name }));
2914
3408
  }
2915
3409
  async createIssue(input) {
2916
3410
  const row = this.repo.create({
@@ -4491,11 +4985,11 @@ var init_checks = __esm({
4491
4985
  // ../core/dist/utils/config-writer.js
4492
4986
  import * as fs9 from "fs";
4493
4987
  import * as path8 from "path";
4494
- function isPlainObject(value) {
4988
+ function isPlainObject2(value) {
4495
4989
  return value !== null && typeof value === "object" && !Array.isArray(value);
4496
4990
  }
4497
4991
  function cleanJobProviders(value) {
4498
- if (!isPlainObject(value)) {
4992
+ if (!isPlainObject2(value)) {
4499
4993
  return {};
4500
4994
  }
4501
4995
  const entries = [];
@@ -4519,7 +5013,7 @@ function saveConfig(projectDir, changes) {
4519
5013
  if (value !== void 0) {
4520
5014
  if (key === "jobProviders") {
4521
5015
  merged[key] = cleanJobProviders(value);
4522
- } else if (PARTIAL_MERGE_KEYS.has(key) && isPlainObject(existing[key]) && isPlainObject(value)) {
5016
+ } else if (PARTIAL_MERGE_KEYS.has(key) && isPlainObject2(existing[key]) && isPlainObject2(value)) {
4523
5017
  merged[key] = { ...existing[key], ...value };
4524
5018
  } else {
4525
5019
  merged[key] = value;
@@ -4535,28 +5029,147 @@ function saveConfig(projectDir, changes) {
4535
5029
  };
4536
5030
  }
4537
5031
  }
4538
- var PARTIAL_MERGE_KEYS;
4539
- var init_config_writer = __esm({
4540
- "../core/dist/utils/config-writer.js"() {
5032
+ var PARTIAL_MERGE_KEYS;
5033
+ var init_config_writer = __esm({
5034
+ "../core/dist/utils/config-writer.js"() {
5035
+ "use strict";
5036
+ init_constants();
5037
+ PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set([
5038
+ "notifications",
5039
+ "qa",
5040
+ "audit",
5041
+ "roadmapScanner",
5042
+ "queue",
5043
+ "providerPresets",
5044
+ "pausedJobs"
5045
+ ]);
5046
+ }
5047
+ });
5048
+
5049
+ // ../core/dist/utils/config-path.js
5050
+ import * as fs10 from "fs";
5051
+ import * as path9 from "path";
5052
+ function isPlainObject3(value) {
5053
+ return value !== null && typeof value === "object" && !Array.isArray(value);
5054
+ }
5055
+ function splitConfigPath(dotPath) {
5056
+ const parts = dotPath.split(".").map((part) => part.trim()).filter(Boolean);
5057
+ if (parts.length === 0 || parts.some((part) => part === "__proto__" || part === "constructor")) {
5058
+ throw new Error(`Invalid config path: ${dotPath}`);
5059
+ }
5060
+ return parts;
5061
+ }
5062
+ function readRawConfig(projectDir) {
5063
+ const configPath = path9.join(projectDir, CONFIG_FILE_NAME);
5064
+ if (!fs10.existsSync(configPath)) {
5065
+ return {};
5066
+ }
5067
+ return JSON.parse(fs10.readFileSync(configPath, "utf-8"));
5068
+ }
5069
+ function writeRawConfig(projectDir, rawConfig) {
5070
+ const configPath = path9.join(projectDir, CONFIG_FILE_NAME);
5071
+ fs10.writeFileSync(configPath, `${JSON.stringify(rawConfig, null, 2)}
5072
+ `);
5073
+ }
5074
+ function getValueAtPath(source, parts) {
5075
+ let current = source;
5076
+ for (const part of parts) {
5077
+ if (!isPlainObject3(current) || !(part in current)) {
5078
+ return void 0;
5079
+ }
5080
+ current = current[part];
5081
+ }
5082
+ return current;
5083
+ }
5084
+ function setValueAtPath(target, parts, value) {
5085
+ let current = target;
5086
+ for (const part of parts.slice(0, -1)) {
5087
+ const next = current[part];
5088
+ if (next === void 0) {
5089
+ current[part] = {};
5090
+ } else if (!isPlainObject3(next)) {
5091
+ throw new Error(`Cannot set ${parts.join(".")}: ${part} is not an object`);
5092
+ }
5093
+ current = current[part];
5094
+ }
5095
+ current[parts[parts.length - 1]] = value;
5096
+ }
5097
+ function hasKnownConfigPath(parts) {
5098
+ if (parts[0] === "pausedJobs" && parts.length === 2) {
5099
+ return getValidJobTypes().includes(parts[1]);
5100
+ }
5101
+ const defaults = getDefaultConfig();
5102
+ let current = defaults;
5103
+ for (const part of parts) {
5104
+ if (!isPlainObject3(current) || !(part in current)) {
5105
+ return false;
5106
+ }
5107
+ current = current[part];
5108
+ }
5109
+ return true;
5110
+ }
5111
+ function parseConfigValue(rawValue) {
5112
+ const trimmed = rawValue.trim();
5113
+ if (trimmed === "true")
5114
+ return true;
5115
+ if (trimmed === "false")
5116
+ return false;
5117
+ if (trimmed === "null")
5118
+ return null;
5119
+ if (/^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(trimmed)) {
5120
+ return Number(trimmed);
5121
+ }
5122
+ if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
5123
+ return JSON.parse(trimmed);
5124
+ }
5125
+ return rawValue;
5126
+ }
5127
+ function getConfigValue(projectDir, dotPath) {
5128
+ const parts = splitConfigPath(dotPath);
5129
+ if (!hasKnownConfigPath(parts)) {
5130
+ throw new Error(`Unknown config path: ${dotPath}`);
5131
+ }
5132
+ const config = loadConfig(projectDir);
5133
+ return { path: parts.join("."), value: getValueAtPath(config, parts) };
5134
+ }
5135
+ function setConfigValue(projectDir, dotPath, value) {
5136
+ const parts = splitConfigPath(dotPath);
5137
+ if (!hasKnownConfigPath(parts)) {
5138
+ throw new Error(`Unknown config path: ${dotPath}`);
5139
+ }
5140
+ const rawConfig = readRawConfig(projectDir);
5141
+ const originalRawConfig = JSON.parse(JSON.stringify(rawConfig));
5142
+ const currentConfig = loadConfig(projectDir);
5143
+ const previousValue = getValueAtPath(currentConfig, parts);
5144
+ setValueAtPath(rawConfig, parts, value);
5145
+ const result = saveConfig(projectDir, rawConfig);
5146
+ if (!result.success) {
5147
+ throw new Error(`Failed to save config: ${result.error}`);
5148
+ }
5149
+ const reloaded = loadConfig(projectDir);
5150
+ const reloadedValue = getValueAtPath(reloaded, parts);
5151
+ if (JSON.stringify(reloadedValue) !== JSON.stringify(value)) {
5152
+ writeRawConfig(projectDir, originalRawConfig);
5153
+ throw new Error(`Invalid value for config path: ${parts.join(".")}`);
5154
+ }
5155
+ return { path: parts.join("."), previousValue, value: reloadedValue };
5156
+ }
5157
+ var init_config_path = __esm({
5158
+ "../core/dist/utils/config-path.js"() {
4541
5159
  "use strict";
4542
5160
  init_constants();
4543
- PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set([
4544
- "notifications",
4545
- "qa",
4546
- "audit",
4547
- "roadmapScanner",
4548
- "queue",
4549
- "providerPresets"
4550
- ]);
5161
+ init_config();
5162
+ init_job_registry();
5163
+ init_config_writer();
4551
5164
  }
4552
5165
  });
4553
5166
 
4554
5167
  // ../core/dist/utils/execution-history.js
4555
5168
  import * as os4 from "os";
4556
- import * as path9 from "path";
5169
+ import * as path10 from "path";
4557
5170
  function getHistoryPath() {
4558
- const base = process.env.NIGHT_WATCH_HOME || path9.join(os4.homedir(), GLOBAL_CONFIG_DIR);
4559
- return path9.join(base, HISTORY_FILE_NAME);
5171
+ const base = process.env.NIGHT_WATCH_HOME || path10.join(os4.homedir(), GLOBAL_CONFIG_DIR);
5172
+ return path10.join(base, HISTORY_FILE_NAME);
4560
5173
  }
4561
5174
  function loadHistory() {
4562
5175
  const { executionHistory } = getRepositories();
@@ -4567,7 +5180,7 @@ function saveHistory(history) {
4567
5180
  executionHistory.replaceAll(history);
4568
5181
  }
4569
5182
  function recordExecution(projectDir, prdFile, outcome, exitCode, attempt = 1) {
4570
- const resolved = path9.resolve(projectDir);
5183
+ const resolved = path10.resolve(projectDir);
4571
5184
  const { executionHistory } = getRepositories();
4572
5185
  const record = {
4573
5186
  timestamp: Math.floor(Date.now() / 1e3),
@@ -4579,7 +5192,7 @@ function recordExecution(projectDir, prdFile, outcome, exitCode, attempt = 1) {
4579
5192
  executionHistory.trimRecords(resolved, prdFile, MAX_HISTORY_RECORDS_PER_PRD);
4580
5193
  }
4581
5194
  function getLastExecution(projectDir, prdFile) {
4582
- const resolved = path9.resolve(projectDir);
5195
+ const resolved = path10.resolve(projectDir);
4583
5196
  const { executionHistory } = getRepositories();
4584
5197
  const records = executionHistory.getRecords(resolved, prdFile);
4585
5198
  return records.length > 0 ? records[0] : null;
@@ -4880,16 +5493,16 @@ var init_github = __esm({
4880
5493
  });
4881
5494
 
4882
5495
  // ../core/dist/utils/log-utils.js
4883
- import * as fs10 from "fs";
5496
+ import * as fs11 from "fs";
4884
5497
  function rotateLog(logFile, maxSize = DEFAULT_MAX_LOG_SIZE) {
4885
- if (!fs10.existsSync(logFile)) {
5498
+ if (!fs11.existsSync(logFile)) {
4886
5499
  return false;
4887
5500
  }
4888
5501
  try {
4889
- const stats = fs10.statSync(logFile);
5502
+ const stats = fs11.statSync(logFile);
4890
5503
  if (stats.size > maxSize) {
4891
5504
  const oldPath = `${logFile}.old`;
4892
- fs10.renameSync(logFile, oldPath);
5505
+ fs11.renameSync(logFile, oldPath);
4893
5506
  return true;
4894
5507
  }
4895
5508
  } catch {
@@ -4897,11 +5510,11 @@ function rotateLog(logFile, maxSize = DEFAULT_MAX_LOG_SIZE) {
4897
5510
  return false;
4898
5511
  }
4899
5512
  function checkRateLimited(logFile, startLine) {
4900
- if (!fs10.existsSync(logFile)) {
5513
+ if (!fs11.existsSync(logFile)) {
4901
5514
  return false;
4902
5515
  }
4903
5516
  try {
4904
- const content = fs10.readFileSync(logFile, "utf-8");
5517
+ const content = fs11.readFileSync(logFile, "utf-8");
4905
5518
  const lines = content.split("\n");
4906
5519
  let linesToCheck;
4907
5520
  if (startLine !== void 0 && startLine > 0) {
@@ -4922,18 +5535,18 @@ var init_log_utils = __esm({
4922
5535
  });
4923
5536
 
4924
5537
  // ../core/dist/utils/global-config.js
4925
- import * as fs11 from "fs";
5538
+ import * as fs12 from "fs";
4926
5539
  import * as os5 from "os";
4927
- import * as path10 from "path";
5540
+ import * as path11 from "path";
4928
5541
  function getGlobalNotificationsPath() {
4929
- return path10.join(os5.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_NOTIFICATIONS_FILE_NAME);
5542
+ return path11.join(os5.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_NOTIFICATIONS_FILE_NAME);
4930
5543
  }
4931
5544
  function loadGlobalNotificationsConfig() {
4932
5545
  const filePath = getGlobalNotificationsPath();
4933
5546
  try {
4934
- if (!fs11.existsSync(filePath))
5547
+ if (!fs12.existsSync(filePath))
4935
5548
  return { webhook: null };
4936
- const raw = fs11.readFileSync(filePath, "utf-8");
5549
+ const raw = fs12.readFileSync(filePath, "utf-8");
4937
5550
  return JSON.parse(raw);
4938
5551
  } catch {
4939
5552
  return { webhook: null };
@@ -4941,8 +5554,8 @@ function loadGlobalNotificationsConfig() {
4941
5554
  }
4942
5555
  function saveGlobalNotificationsConfig(config) {
4943
5556
  const filePath = getGlobalNotificationsPath();
4944
- fs11.mkdirSync(path10.dirname(filePath), { recursive: true });
4945
- fs11.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
5557
+ fs12.mkdirSync(path11.dirname(filePath), { recursive: true });
5558
+ fs12.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
4946
5559
  }
4947
5560
  var init_global_config = __esm({
4948
5561
  "../core/dist/utils/global-config.js"() {
@@ -5415,15 +6028,15 @@ var init_prd_discovery = __esm({
5415
6028
  });
5416
6029
 
5417
6030
  // ../core/dist/utils/prd-utils.js
5418
- import * as fs12 from "fs";
5419
- import * as path11 from "path";
6031
+ import * as fs13 from "fs";
6032
+ import * as path12 from "path";
5420
6033
  function slugify(name) {
5421
6034
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
5422
6035
  }
5423
6036
  function getNextPrdNumber(prdDir) {
5424
- if (!fs12.existsSync(prdDir))
6037
+ if (!fs13.existsSync(prdDir))
5425
6038
  return 1;
5426
- const files = fs12.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
6039
+ const files = fs13.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
5427
6040
  const numbers = files.map((f) => {
5428
6041
  const match = f.match(/^(\d+)-/);
5429
6042
  return match ? parseInt(match[1], 10) : 0;
@@ -5431,16 +6044,16 @@ function getNextPrdNumber(prdDir) {
5431
6044
  return Math.max(0, ...numbers) + 1;
5432
6045
  }
5433
6046
  function markPrdDone(prdDir, prdFile) {
5434
- const sourcePath = path11.join(prdDir, prdFile);
5435
- if (!fs12.existsSync(sourcePath)) {
6047
+ const sourcePath = path12.join(prdDir, prdFile);
6048
+ if (!fs13.existsSync(sourcePath)) {
5436
6049
  return false;
5437
6050
  }
5438
- const doneDir = path11.join(prdDir, "done");
5439
- if (!fs12.existsSync(doneDir)) {
5440
- fs12.mkdirSync(doneDir, { recursive: true });
6051
+ const doneDir = path12.join(prdDir, "done");
6052
+ if (!fs13.existsSync(doneDir)) {
6053
+ fs13.mkdirSync(doneDir, { recursive: true });
5441
6054
  }
5442
- const destPath = path11.join(doneDir, prdFile);
5443
- fs12.renameSync(sourcePath, destPath);
6055
+ const destPath = path12.join(doneDir, prdFile);
6056
+ fs13.renameSync(sourcePath, destPath);
5444
6057
  return true;
5445
6058
  }
5446
6059
  var init_prd_utils = __esm({
@@ -5450,16 +6063,16 @@ var init_prd_utils = __esm({
5450
6063
  });
5451
6064
 
5452
6065
  // ../core/dist/utils/registry.js
5453
- import * as fs13 from "fs";
6066
+ import * as fs14 from "fs";
5454
6067
  import * as os6 from "os";
5455
- import * as path12 from "path";
6068
+ import * as path13 from "path";
5456
6069
  function readLegacyRegistryEntries() {
5457
6070
  const registryPath = getRegistryPath();
5458
- if (!fs13.existsSync(registryPath)) {
6071
+ if (!fs14.existsSync(registryPath)) {
5459
6072
  return [];
5460
6073
  }
5461
6074
  try {
5462
- const raw = fs13.readFileSync(registryPath, "utf-8");
6075
+ const raw = fs14.readFileSync(registryPath, "utf-8");
5463
6076
  const parsed = JSON.parse(raw);
5464
6077
  if (!Array.isArray(parsed)) {
5465
6078
  return [];
@@ -5494,8 +6107,8 @@ function loadRegistryEntriesWithLegacyFallback() {
5494
6107
  return projectRegistry.getAll();
5495
6108
  }
5496
6109
  function getRegistryPath() {
5497
- const base = process.env.NIGHT_WATCH_HOME || path12.join(os6.homedir(), GLOBAL_CONFIG_DIR);
5498
- return path12.join(base, REGISTRY_FILE_NAME);
6110
+ const base = process.env.NIGHT_WATCH_HOME || path13.join(os6.homedir(), GLOBAL_CONFIG_DIR);
6111
+ return path13.join(base, REGISTRY_FILE_NAME);
5499
6112
  }
5500
6113
  function loadRegistry() {
5501
6114
  return loadRegistryEntriesWithLegacyFallback();
@@ -5508,7 +6121,7 @@ function saveRegistry(entries) {
5508
6121
  }
5509
6122
  }
5510
6123
  function registerProject(projectDir) {
5511
- const resolvedPath = path12.resolve(projectDir);
6124
+ const resolvedPath = path13.resolve(projectDir);
5512
6125
  const { projectRegistry } = getRepositories();
5513
6126
  const entries = loadRegistryEntriesWithLegacyFallback();
5514
6127
  const existing = entries.find((e) => e.path === resolvedPath);
@@ -5517,13 +6130,13 @@ function registerProject(projectDir) {
5517
6130
  }
5518
6131
  const name = getProjectName(resolvedPath);
5519
6132
  const nameExists = entries.some((e) => e.name === name);
5520
- const finalName = nameExists ? `${name}-${path12.basename(resolvedPath)}` : name;
6133
+ const finalName = nameExists ? `${name}-${path13.basename(resolvedPath)}` : name;
5521
6134
  const entry = { name: finalName, path: resolvedPath };
5522
6135
  projectRegistry.upsert(entry);
5523
6136
  return entry;
5524
6137
  }
5525
6138
  function unregisterProject(projectDir) {
5526
- const resolvedPath = path12.resolve(projectDir);
6139
+ const resolvedPath = path13.resolve(projectDir);
5527
6140
  loadRegistryEntriesWithLegacyFallback();
5528
6141
  const { projectRegistry } = getRepositories();
5529
6142
  return projectRegistry.remove(resolvedPath);
@@ -5533,7 +6146,7 @@ function validateRegistry() {
5533
6146
  const valid = [];
5534
6147
  const invalid = [];
5535
6148
  for (const entry of entries) {
5536
- if (fs13.existsSync(entry.path) && fs13.existsSync(path12.join(entry.path, CONFIG_FILE_NAME))) {
6149
+ if (fs14.existsSync(entry.path) && fs14.existsSync(path13.join(entry.path, CONFIG_FILE_NAME))) {
5537
6150
  valid.push(entry);
5538
6151
  } else {
5539
6152
  invalid.push(entry);
@@ -5542,7 +6155,7 @@ function validateRegistry() {
5542
6155
  return { valid, invalid };
5543
6156
  }
5544
6157
  function pruneProjectData(projectDir) {
5545
- const resolvedPath = path12.resolve(projectDir);
6158
+ const resolvedPath = path13.resolve(projectDir);
5546
6159
  const db = getDb();
5547
6160
  db.transaction(() => {
5548
6161
  db.prepare("DELETE FROM execution_history WHERE project_path = ?").run(resolvedPath);
@@ -5553,7 +6166,7 @@ function pruneProjectData(projectDir) {
5553
6166
  })();
5554
6167
  }
5555
6168
  function removeProject(projectDir) {
5556
- const resolvedPath = path12.resolve(projectDir);
6169
+ const resolvedPath = path13.resolve(projectDir);
5557
6170
  const projectName = getProjectName(resolvedPath);
5558
6171
  const marker = generateMarker(projectName);
5559
6172
  const cronEntriesRemoved = removeEntriesForProject(resolvedPath, marker);
@@ -5742,8 +6355,8 @@ var init_roadmap_parser = __esm({
5742
6355
  });
5743
6356
 
5744
6357
  // ../core/dist/audit/report.js
5745
- import * as fs14 from "fs";
5746
- import * as path13 from "path";
6358
+ import * as fs15 from "fs";
6359
+ import * as path14 from "path";
5747
6360
  function normalizeAuditSeverity(raw) {
5748
6361
  const normalized = raw.trim().toLowerCase();
5749
6362
  if (normalized === "critical")
@@ -5802,11 +6415,11 @@ function parseAuditFindings(reportContent) {
5802
6415
  return findings;
5803
6416
  }
5804
6417
  function loadAuditFindings(projectDir) {
5805
- const reportPath = path13.join(projectDir, "logs", "audit-report.md");
5806
- if (!fs14.existsSync(reportPath)) {
6418
+ const reportPath = path14.join(projectDir, "logs", "audit-report.md");
6419
+ if (!fs15.existsSync(reportPath)) {
5807
6420
  return [];
5808
6421
  }
5809
- const reportContent = fs14.readFileSync(reportPath, "utf-8");
6422
+ const reportContent = fs15.readFileSync(reportPath, "utf-8");
5810
6423
  if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
5811
6424
  return [];
5812
6425
  }
@@ -5819,18 +6432,18 @@ var init_report = __esm({
5819
6432
  });
5820
6433
 
5821
6434
  // ../core/dist/utils/roadmap-state.js
5822
- import * as fs15 from "fs";
5823
- import * as path14 from "path";
6435
+ import * as fs16 from "fs";
6436
+ import * as path15 from "path";
5824
6437
  function getStateFilePath(prdDir) {
5825
- return path14.join(prdDir, STATE_FILE_NAME);
6438
+ return path15.join(prdDir, STATE_FILE_NAME);
5826
6439
  }
5827
6440
  function readJsonState(prdDir) {
5828
6441
  const statePath = getStateFilePath(prdDir);
5829
- if (!fs15.existsSync(statePath)) {
6442
+ if (!fs16.existsSync(statePath)) {
5830
6443
  return null;
5831
6444
  }
5832
6445
  try {
5833
- const content = fs15.readFileSync(statePath, "utf-8");
6446
+ const content = fs16.readFileSync(statePath, "utf-8");
5834
6447
  const parsed = JSON.parse(content);
5835
6448
  if (typeof parsed !== "object" || parsed === null) {
5836
6449
  return null;
@@ -5868,11 +6481,11 @@ function saveRoadmapState(prdDir, state) {
5868
6481
  const { roadmapState } = getRepositories();
5869
6482
  roadmapState.save(prdDir, state);
5870
6483
  const statePath = getStateFilePath(prdDir);
5871
- const dir = path14.dirname(statePath);
5872
- if (!fs15.existsSync(dir)) {
5873
- fs15.mkdirSync(dir, { recursive: true });
6484
+ const dir = path15.dirname(statePath);
6485
+ if (!fs16.existsSync(dir)) {
6486
+ fs16.mkdirSync(dir, { recursive: true });
5874
6487
  }
5875
- fs15.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
6488
+ fs16.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5876
6489
  }
5877
6490
  function createEmptyState() {
5878
6491
  return {
@@ -5912,21 +6525,21 @@ var init_roadmap_state = __esm({
5912
6525
  });
5913
6526
 
5914
6527
  // ../core/dist/templates/slicer-prompt.js
5915
- import * as fs16 from "fs";
5916
- import * as path15 from "path";
6528
+ import * as fs17 from "fs";
6529
+ import * as path16 from "path";
5917
6530
  import { fileURLToPath as fileURLToPath2 } from "url";
5918
6531
  function loadSlicerTemplate(templateDir) {
5919
6532
  if (cachedTemplate) {
5920
6533
  return cachedTemplate;
5921
6534
  }
5922
- const candidatePaths = templateDir ? [path15.join(templateDir, "slicer.md")] : [
5923
- path15.resolve(__dirname, "slicer.md"),
5924
- path15.resolve(__dirname, "..", "templates", "slicer.md"),
5925
- path15.resolve(__dirname, "..", "..", "..", "..", "templates", "slicer.md")
6535
+ const candidatePaths = templateDir ? [path16.join(templateDir, "slicer.md")] : [
6536
+ path16.resolve(__dirname, "slicer.md"),
6537
+ path16.resolve(__dirname, "..", "templates", "slicer.md"),
6538
+ path16.resolve(__dirname, "..", "..", "..", "..", "templates", "slicer.md")
5926
6539
  ];
5927
- const templatePath = candidatePaths.find((candidate) => fs16.existsSync(candidate)) ?? candidatePaths[0];
6540
+ const templatePath = candidatePaths.find((candidate) => fs17.existsSync(candidate)) ?? candidatePaths[0];
5928
6541
  try {
5929
- cachedTemplate = fs16.readFileSync(templatePath, "utf-8");
6542
+ cachedTemplate = fs17.readFileSync(templatePath, "utf-8");
5930
6543
  return cachedTemplate;
5931
6544
  } catch (error2) {
5932
6545
  console.warn(`Warning: Could not load slicer template from ${templatePath}, using default:`, error2 instanceof Error ? error2.message : String(error2));
@@ -5951,7 +6564,7 @@ function createSlicerPromptVars(title, section, description, prdDir, prdFilename
5951
6564
  title,
5952
6565
  section,
5953
6566
  description: description || "(No description provided)",
5954
- outputFilePath: path15.join(prdDir, prdFilename),
6567
+ outputFilePath: path16.join(prdDir, prdFilename),
5955
6568
  prdDir
5956
6569
  };
5957
6570
  }
@@ -5960,7 +6573,7 @@ var init_slicer_prompt = __esm({
5960
6573
  "../core/dist/templates/slicer-prompt.js"() {
5961
6574
  "use strict";
5962
6575
  __filename = fileURLToPath2(import.meta.url);
5963
- __dirname = path15.dirname(__filename);
6576
+ __dirname = path16.dirname(__filename);
5964
6577
  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.
5965
6578
 
5966
6579
  When this activates: \`Planning Mode: Principal Architect\`
@@ -6093,8 +6706,8 @@ DO NOT forget to write the file.
6093
6706
  });
6094
6707
 
6095
6708
  // ../core/dist/utils/roadmap-scanner.js
6096
- import * as fs17 from "fs";
6097
- import * as path16 from "path";
6709
+ import * as fs18 from "fs";
6710
+ import * as path17 from "path";
6098
6711
  import { spawn } from "child_process";
6099
6712
  import { createHash as createHash3 } from "crypto";
6100
6713
  function auditSeverityRank(severity) {
@@ -6145,9 +6758,9 @@ function collectAuditPlannerItems(projectDir) {
6145
6758
  return findings.map(auditFindingToRoadmapItem);
6146
6759
  }
6147
6760
  function getRoadmapStatus(projectDir, config) {
6148
- const roadmapPath = path16.join(projectDir, config.roadmapScanner.roadmapPath);
6761
+ const roadmapPath = path17.join(projectDir, config.roadmapScanner.roadmapPath);
6149
6762
  const scannerEnabled = config.roadmapScanner.enabled;
6150
- if (!fs17.existsSync(roadmapPath)) {
6763
+ if (!fs18.existsSync(roadmapPath)) {
6151
6764
  return {
6152
6765
  found: false,
6153
6766
  enabled: scannerEnabled,
@@ -6158,9 +6771,9 @@ function getRoadmapStatus(projectDir, config) {
6158
6771
  items: []
6159
6772
  };
6160
6773
  }
6161
- const content = fs17.readFileSync(roadmapPath, "utf-8");
6774
+ const content = fs18.readFileSync(roadmapPath, "utf-8");
6162
6775
  const items = parseRoadmap(content);
6163
- const prdDir = path16.join(projectDir, config.prdDir);
6776
+ const prdDir = path17.join(projectDir, config.prdDir);
6164
6777
  const state = loadRoadmapState(prdDir);
6165
6778
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
6166
6779
  const statusItems = items.map((item) => {
@@ -6197,10 +6810,10 @@ function getRoadmapStatus(projectDir, config) {
6197
6810
  }
6198
6811
  function scanExistingPrdSlugs(prdDir) {
6199
6812
  const slugs = /* @__PURE__ */ new Set();
6200
- if (!fs17.existsSync(prdDir)) {
6813
+ if (!fs18.existsSync(prdDir)) {
6201
6814
  return slugs;
6202
6815
  }
6203
- const files = fs17.readdirSync(prdDir);
6816
+ const files = fs18.readdirSync(prdDir);
6204
6817
  for (const file of files) {
6205
6818
  if (!file.endsWith(".md")) {
6206
6819
  continue;
@@ -6252,21 +6865,21 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
6252
6865
  const nextNum = getNextPrdNumber(prdDir);
6253
6866
  const padded = String(nextNum).padStart(2, "0");
6254
6867
  const filename = `${padded}-${itemSlug}.md`;
6255
- const filePath = path16.join(prdDir, filename);
6256
- if (!fs17.existsSync(prdDir)) {
6257
- fs17.mkdirSync(prdDir, { recursive: true });
6868
+ const filePath = path17.join(prdDir, filename);
6869
+ if (!fs18.existsSync(prdDir)) {
6870
+ fs18.mkdirSync(prdDir, { recursive: true });
6258
6871
  }
6259
6872
  const promptVars = createSlicerPromptVars(item.title, item.section, item.description, prdDir, filename);
6260
6873
  const prompt = renderSlicerPrompt(promptVars);
6261
6874
  const providerId = resolveJobProvider(config, "slicer");
6262
6875
  const preset = resolvePreset(config, providerId);
6263
6876
  const providerArgs = buildProviderArgs(preset, prompt, projectDir);
6264
- const logDir = path16.join(projectDir, "logs");
6265
- if (!fs17.existsSync(logDir)) {
6266
- fs17.mkdirSync(logDir, { recursive: true });
6877
+ const logDir = path17.join(projectDir, "logs");
6878
+ if (!fs18.existsSync(logDir)) {
6879
+ fs18.mkdirSync(logDir, { recursive: true });
6267
6880
  }
6268
- const logFile = path16.join(logDir, `slicer-${itemSlug}.log`);
6269
- const logStream = fs17.createWriteStream(logFile, { flags: "w" });
6881
+ const logFile = path17.join(logDir, `slicer-${itemSlug}.log`);
6882
+ const logStream = fs18.createWriteStream(logFile, { flags: "w" });
6270
6883
  logStream.on("error", () => {
6271
6884
  });
6272
6885
  return new Promise((resolve9) => {
@@ -6304,7 +6917,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
6304
6917
  });
6305
6918
  return;
6306
6919
  }
6307
- if (!fs17.existsSync(filePath)) {
6920
+ if (!fs18.existsSync(filePath)) {
6308
6921
  resolve9({
6309
6922
  sliced: false,
6310
6923
  error: `Provider did not create expected file: ${filePath}`,
@@ -6327,23 +6940,23 @@ async function sliceNextItem(projectDir, config) {
6327
6940
  error: "Roadmap scanner is disabled"
6328
6941
  };
6329
6942
  }
6330
- const roadmapPath = path16.join(projectDir, config.roadmapScanner.roadmapPath);
6943
+ const roadmapPath = path17.join(projectDir, config.roadmapScanner.roadmapPath);
6331
6944
  const auditItems = collectAuditPlannerItems(projectDir);
6332
- const roadmapExists = fs17.existsSync(roadmapPath);
6945
+ const roadmapExists = fs18.existsSync(roadmapPath);
6333
6946
  if (!roadmapExists && auditItems.length === 0) {
6334
6947
  return {
6335
6948
  sliced: false,
6336
6949
  error: "No pending items to process"
6337
6950
  };
6338
6951
  }
6339
- const roadmapItems = roadmapExists ? parseRoadmap(fs17.readFileSync(roadmapPath, "utf-8")) : [];
6952
+ const roadmapItems = roadmapExists ? parseRoadmap(fs18.readFileSync(roadmapPath, "utf-8")) : [];
6340
6953
  if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
6341
6954
  return {
6342
6955
  sliced: false,
6343
6956
  error: "No items in roadmap"
6344
6957
  };
6345
6958
  }
6346
- const prdDir = path16.join(projectDir, config.prdDir);
6959
+ const prdDir = path17.join(projectDir, config.prdDir);
6347
6960
  const state = loadRoadmapState(prdDir);
6348
6961
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
6349
6962
  const pickEligibleItem = (items) => {
@@ -6515,8 +7128,8 @@ var init_shell = __esm({
6515
7128
  });
6516
7129
 
6517
7130
  // ../core/dist/utils/scheduling.js
6518
- import * as fs18 from "fs";
6519
- import * as path17 from "path";
7131
+ import * as fs19 from "fs";
7132
+ import * as path18 from "path";
6520
7133
  function normalizeSchedulingPriority(priority) {
6521
7134
  if (!Number.isFinite(priority)) {
6522
7135
  return DEFAULT_SCHEDULING_PRIORITY;
@@ -6551,7 +7164,7 @@ function getJobSchedule(config, jobType) {
6551
7164
  }
6552
7165
  }
6553
7166
  function loadPeerConfig(projectPath) {
6554
- if (!fs18.existsSync(projectPath) || !fs18.existsSync(path17.join(projectPath, CONFIG_FILE_NAME))) {
7167
+ if (!fs19.existsSync(projectPath) || !fs19.existsSync(path18.join(projectPath, CONFIG_FILE_NAME))) {
6555
7168
  return null;
6556
7169
  }
6557
7170
  try {
@@ -6562,10 +7175,10 @@ function loadPeerConfig(projectPath) {
6562
7175
  }
6563
7176
  function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6564
7177
  const peers = /* @__PURE__ */ new Map();
6565
- const currentPath = path17.resolve(currentProjectDir);
7178
+ const currentPath = path18.resolve(currentProjectDir);
6566
7179
  const currentSchedule = getJobSchedule(currentConfig, jobType);
6567
7180
  const addPeer = (projectPath, config) => {
6568
- const resolvedPath = path17.resolve(projectPath);
7181
+ const resolvedPath = path18.resolve(projectPath);
6569
7182
  if (!isJobTypeEnabled(config, jobType)) {
6570
7183
  return;
6571
7184
  }
@@ -6576,12 +7189,12 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6576
7189
  path: resolvedPath,
6577
7190
  config,
6578
7191
  schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
6579
- sortKey: `${path17.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
7192
+ sortKey: `${path18.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
6580
7193
  });
6581
7194
  };
6582
7195
  addPeer(currentPath, currentConfig);
6583
7196
  for (const entry of loadRegistry()) {
6584
- const resolvedPath = path17.resolve(entry.path);
7197
+ const resolvedPath = path18.resolve(entry.path);
6585
7198
  if (resolvedPath === currentPath || peers.has(resolvedPath)) {
6586
7199
  continue;
6587
7200
  }
@@ -6599,7 +7212,7 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
6599
7212
  }
6600
7213
  function getSchedulingPlan(projectDir, config, jobType) {
6601
7214
  const peers = collectSchedulingPeers(projectDir, config, jobType);
6602
- const currentPath = path17.resolve(projectDir);
7215
+ const currentPath = path18.resolve(projectDir);
6603
7216
  const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
6604
7217
  const peerCount = Math.max(1, peers.length);
6605
7218
  const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
@@ -6691,8 +7304,8 @@ var init_webhook_validator = __esm({
6691
7304
 
6692
7305
  // ../core/dist/utils/worktree-manager.js
6693
7306
  import { execFileSync as execFileSync4 } from "child_process";
6694
- import * as fs19 from "fs";
6695
- import * as path18 from "path";
7307
+ import * as fs20 from "fs";
7308
+ import * as path19 from "path";
6696
7309
  function gitExec(args, cwd, logFile) {
6697
7310
  try {
6698
7311
  const result = execFileSync4("git", args, {
@@ -6702,7 +7315,7 @@ function gitExec(args, cwd, logFile) {
6702
7315
  });
6703
7316
  if (logFile && result) {
6704
7317
  try {
6705
- fs19.appendFileSync(logFile, result);
7318
+ fs20.appendFileSync(logFile, result);
6706
7319
  } catch {
6707
7320
  }
6708
7321
  }
@@ -6711,7 +7324,7 @@ function gitExec(args, cwd, logFile) {
6711
7324
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
6712
7325
  if (logFile) {
6713
7326
  try {
6714
- fs19.appendFileSync(logFile, errorMessage + "\n");
7327
+ fs20.appendFileSync(logFile, errorMessage + "\n");
6715
7328
  } catch {
6716
7329
  }
6717
7330
  }
@@ -6736,11 +7349,11 @@ function branchExistsRemotely(projectDir, branchName) {
6736
7349
  }
6737
7350
  function prepareBranchWorktree(options) {
6738
7351
  const { projectDir, worktreeDir, branchName, defaultBranch, logFile } = options;
6739
- if (fs19.existsSync(worktreeDir)) {
7352
+ if (fs20.existsSync(worktreeDir)) {
6740
7353
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
6741
7354
  if (!isRegistered) {
6742
7355
  try {
6743
- fs19.rmSync(worktreeDir, { recursive: true, force: true });
7356
+ fs20.rmSync(worktreeDir, { recursive: true, force: true });
6744
7357
  } catch {
6745
7358
  }
6746
7359
  }
@@ -6779,11 +7392,11 @@ function prepareBranchWorktree(options) {
6779
7392
  }
6780
7393
  function prepareDetachedWorktree(options) {
6781
7394
  const { projectDir, worktreeDir, defaultBranch, logFile } = options;
6782
- if (fs19.existsSync(worktreeDir)) {
7395
+ if (fs20.existsSync(worktreeDir)) {
6783
7396
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
6784
7397
  if (!isRegistered) {
6785
7398
  try {
6786
- fs19.rmSync(worktreeDir, { recursive: true, force: true });
7399
+ fs20.rmSync(worktreeDir, { recursive: true, force: true });
6787
7400
  } catch {
6788
7401
  }
6789
7402
  }
@@ -6825,7 +7438,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
6825
7438
  }
6826
7439
  }
6827
7440
  function cleanupWorktrees(projectDir, scope) {
6828
- const projectName = path18.basename(projectDir);
7441
+ const projectName = path19.basename(projectDir);
6829
7442
  const matchToken = scope ? scope : `${projectName}-nw`;
6830
7443
  const removed = [];
6831
7444
  try {
@@ -6860,13 +7473,13 @@ var init_worktree_manager = __esm({
6860
7473
  });
6861
7474
 
6862
7475
  // ../core/dist/utils/job-queue.js
6863
- import * as fs20 from "fs";
7476
+ import * as fs21 from "fs";
6864
7477
  import * as os7 from "os";
6865
- import * as path19 from "path";
7478
+ import * as path20 from "path";
6866
7479
  import Database7 from "better-sqlite3";
6867
7480
  function getStateDbPath() {
6868
- const base = process.env.NIGHT_WATCH_HOME || path19.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6869
- return path19.join(base, STATE_DB_FILE_NAME);
7481
+ const base = process.env.NIGHT_WATCH_HOME || path20.join(os7.homedir(), GLOBAL_CONFIG_DIR);
7482
+ return path20.join(base, STATE_DB_FILE_NAME);
6870
7483
  }
6871
7484
  function openDb() {
6872
7485
  const dbPath = getStateDbPath();
@@ -6918,7 +7531,7 @@ function getLockPathForJob(projectPath, jobType) {
6918
7531
  }
6919
7532
  function isRunningEntryStale(entry) {
6920
7533
  const lockPath = getLockPathForJob(entry.projectPath, entry.jobType);
6921
- if (fs20.existsSync(lockPath)) {
7534
+ if (fs21.existsSync(lockPath)) {
6922
7535
  const lockInfo = checkLockFile(lockPath);
6923
7536
  return { isStale: !lockInfo.running, lockPid: lockInfo.pid };
6924
7537
  }
@@ -7640,9 +8253,9 @@ var init_amplitude_client = __esm({
7640
8253
  });
7641
8254
 
7642
8255
  // ../core/dist/analytics/analytics-runner.js
7643
- import * as fs21 from "fs";
8256
+ import * as fs22 from "fs";
7644
8257
  import * as os8 from "os";
7645
- import * as path20 from "path";
8258
+ import * as path21 from "path";
7646
8259
  function parseIssuesFromResponse(text) {
7647
8260
  const start = text.indexOf("[");
7648
8261
  const end = text.lastIndexOf("]");
@@ -7671,9 +8284,9 @@ async function runAnalytics(config, projectDir) {
7671
8284
 
7672
8285
  --- AMPLITUDE DATA ---
7673
8286
  ${JSON.stringify(data, null, 2)}`;
7674
- const tmpDir = fs21.mkdtempSync(path20.join(os8.tmpdir(), "nw-analytics-"));
7675
- const promptFile = path20.join(tmpDir, "analytics-prompt.md");
7676
- fs21.writeFileSync(promptFile, prompt, "utf-8");
8287
+ const tmpDir = fs22.mkdtempSync(path21.join(os8.tmpdir(), "nw-analytics-"));
8288
+ const promptFile = path21.join(tmpDir, "analytics-prompt.md");
8289
+ fs22.writeFileSync(promptFile, prompt, "utf-8");
7677
8290
  try {
7678
8291
  const provider = resolveJobProvider(config, "analytics");
7679
8292
  let providerCmd = PROVIDER_COMMANDS[provider];
@@ -7694,8 +8307,8 @@ set -euo pipefail
7694
8307
  ${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
7695
8308
  `;
7696
8309
  }
7697
- const scriptFile = path20.join(tmpDir, "run-analytics.sh");
7698
- fs21.writeFileSync(scriptFile, scriptContent, { mode: 493 });
8310
+ const scriptFile = path21.join(tmpDir, "run-analytics.sh");
8311
+ fs22.writeFileSync(scriptFile, scriptContent, { mode: 493 });
7699
8312
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
7700
8313
  if (exitCode !== 0) {
7701
8314
  throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
@@ -7742,7 +8355,7 @@ ${stderr}`;
7742
8355
  };
7743
8356
  } finally {
7744
8357
  try {
7745
- fs21.rmSync(tmpDir, { recursive: true, force: true });
8358
+ fs22.rmSync(tmpDir, { recursive: true, force: true });
7746
8359
  } catch {
7747
8360
  }
7748
8361
  }
@@ -8200,6 +8813,9 @@ __export(dist_exports, {
8200
8813
  DEFAULT_SLICER_SCHEDULE: () => DEFAULT_SLICER_SCHEDULE,
8201
8814
  DEFAULT_SUMMARY_WINDOW_HOURS: () => DEFAULT_SUMMARY_WINDOW_HOURS,
8202
8815
  DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
8816
+ DEFAULT_WEBHOOK_TRIGGERS: () => DEFAULT_WEBHOOK_TRIGGERS,
8817
+ DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS: () => DEFAULT_WEBHOOK_TRIGGER_MAX_SKEW_SECONDS,
8818
+ DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV: () => DEFAULT_WEBHOOK_TRIGGER_SECRET_ENV,
8203
8819
  EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
8204
8820
  EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
8205
8821
  EXECUTOR_PARTIAL_LABEL: () => EXECUTOR_PARTIAL_LABEL,
@@ -8314,6 +8930,7 @@ __export(dist_exports, {
8314
8930
  generateMarker: () => generateMarker,
8315
8931
  getAllJobDefs: () => getAllJobDefs,
8316
8932
  getBranchTipTimestamp: () => getBranchTipTimestamp,
8933
+ getConfigValue: () => getConfigValue,
8317
8934
  getCrontabInfo: () => getCrontabInfo,
8318
8935
  getDb: () => getDb,
8319
8936
  getDbPath: () => getDbPath,
@@ -8390,6 +9007,7 @@ __export(dist_exports, {
8390
9007
  normalizeJobConfig: () => normalizeJobConfig,
8391
9008
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
8392
9009
  parseAuditFindings: () => parseAuditFindings,
9010
+ parseConfigValue: () => parseConfigValue,
8393
9011
  parsePrdDependencies: () => parsePrdDependencies,
8394
9012
  parseRoadmap: () => parseRoadmap,
8395
9013
  parseScriptResult: () => parseScriptResult,
@@ -8432,6 +9050,7 @@ __export(dist_exports, {
8432
9050
  scanRoadmap: () => scanRoadmap,
8433
9051
  sendNotifications: () => sendNotifications,
8434
9052
  sendWebhook: () => sendWebhook,
9053
+ setConfigValue: () => setConfigValue,
8435
9054
  sleep: () => sleep,
8436
9055
  sliceNextItem: () => sliceNextItem,
8437
9056
  sliceRoadmapItem: () => sliceRoadmapItem,
@@ -8471,6 +9090,7 @@ var init_dist = __esm({
8471
9090
  init_cancel();
8472
9091
  init_checks();
8473
9092
  init_config_writer();
9093
+ init_config_path();
8474
9094
  init_crontab();
8475
9095
  init_execution_history();
8476
9096
  init_git_utils();
@@ -8506,30 +9126,30 @@ var init_dist = __esm({
8506
9126
  // src/cli.ts
8507
9127
  import "reflect-metadata";
8508
9128
  import { Command as Command3 } from "commander";
8509
- import { existsSync as existsSync33, readFileSync as readFileSync20 } from "fs";
9129
+ import { existsSync as existsSync34, readFileSync as readFileSync21 } from "fs";
8510
9130
  import { fileURLToPath as fileURLToPath6 } from "url";
8511
- import { dirname as dirname12, join as join37 } from "path";
9131
+ import { dirname as dirname12, join as join38 } from "path";
8512
9132
 
8513
9133
  // src/commands/init.ts
8514
9134
  init_dist();
8515
- import fs22 from "fs";
8516
- import path21 from "path";
9135
+ import fs23 from "fs";
9136
+ import path22 from "path";
8517
9137
  import { execSync as execSync3 } from "child_process";
8518
9138
  import { fileURLToPath as fileURLToPath3 } from "url";
8519
- import { dirname as dirname6, join as join19 } from "path";
9139
+ import { dirname as dirname6, join as join20 } from "path";
8520
9140
  import * as readline from "readline";
8521
9141
  var __filename2 = fileURLToPath3(import.meta.url);
8522
9142
  var __dirname2 = dirname6(__filename2);
8523
9143
  function findTemplatesDir(startDir) {
8524
9144
  let d = startDir;
8525
9145
  for (let i = 0; i < 8; i++) {
8526
- const candidate = join19(d, "templates");
8527
- if (fs22.existsSync(candidate) && fs22.statSync(candidate).isDirectory()) {
9146
+ const candidate = join20(d, "templates");
9147
+ if (fs23.existsSync(candidate) && fs23.statSync(candidate).isDirectory()) {
8528
9148
  return candidate;
8529
9149
  }
8530
9150
  d = dirname6(d);
8531
9151
  }
8532
- return join19(startDir, "templates");
9152
+ return join20(startDir, "templates");
8533
9153
  }
8534
9154
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
8535
9155
  var NW_SKILLS = [
@@ -8541,12 +9161,12 @@ var NW_SKILLS = [
8541
9161
  "nw-review"
8542
9162
  ];
8543
9163
  function hasPlaywrightDependency(cwd) {
8544
- const packageJsonPath = path21.join(cwd, "package.json");
8545
- if (!fs22.existsSync(packageJsonPath)) {
9164
+ const packageJsonPath = path22.join(cwd, "package.json");
9165
+ if (!fs23.existsSync(packageJsonPath)) {
8546
9166
  return false;
8547
9167
  }
8548
9168
  try {
8549
- const packageJson2 = JSON.parse(fs22.readFileSync(packageJsonPath, "utf-8"));
9169
+ const packageJson2 = JSON.parse(fs23.readFileSync(packageJsonPath, "utf-8"));
8550
9170
  return Boolean(
8551
9171
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
8552
9172
  );
@@ -8558,7 +9178,7 @@ function detectPlaywright(cwd) {
8558
9178
  if (hasPlaywrightDependency(cwd)) {
8559
9179
  return true;
8560
9180
  }
8561
- if (fs22.existsSync(path21.join(cwd, "node_modules", ".bin", "playwright"))) {
9181
+ if (fs23.existsSync(path22.join(cwd, "node_modules", ".bin", "playwright"))) {
8562
9182
  return true;
8563
9183
  }
8564
9184
  try {
@@ -8574,10 +9194,10 @@ function detectPlaywright(cwd) {
8574
9194
  }
8575
9195
  }
8576
9196
  function resolvePlaywrightInstallCommand(cwd) {
8577
- if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) {
9197
+ if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) {
8578
9198
  return "pnpm add -D @playwright/test";
8579
9199
  }
8580
- if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) {
9200
+ if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) {
8581
9201
  return "yarn add -D @playwright/test";
8582
9202
  }
8583
9203
  return "npm install -D @playwright/test";
@@ -8723,8 +9343,8 @@ function promptProviderSelection(providers) {
8723
9343
  });
8724
9344
  }
8725
9345
  function ensureDir(dirPath) {
8726
- if (!fs22.existsSync(dirPath)) {
8727
- fs22.mkdirSync(dirPath, { recursive: true });
9346
+ if (!fs23.existsSync(dirPath)) {
9347
+ fs23.mkdirSync(dirPath, { recursive: true });
8728
9348
  }
8729
9349
  }
8730
9350
  function buildInitConfig(params) {
@@ -8780,35 +9400,47 @@ function buildInitConfig(params) {
8780
9400
  queue: {
8781
9401
  ...defaults.queue,
8782
9402
  priority: { ...defaults.queue.priority }
9403
+ },
9404
+ webhookTriggers: {
9405
+ ...defaults.webhookTriggers,
9406
+ allowedJobIds: [...defaults.webhookTriggers.allowedJobIds],
9407
+ github: {
9408
+ ...defaults.webhookTriggers.github,
9409
+ events: [...defaults.webhookTriggers.github.events],
9410
+ rules: defaults.webhookTriggers.github.rules.map((rule) => ({
9411
+ ...rule,
9412
+ branchPatterns: rule.branchPatterns ? [...rule.branchPatterns] : void 0
9413
+ }))
9414
+ }
8783
9415
  }
8784
9416
  };
8785
9417
  }
8786
9418
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
8787
9419
  if (customTemplatesDir !== null) {
8788
- const customPath = join19(customTemplatesDir, templateName);
8789
- if (fs22.existsSync(customPath)) {
9420
+ const customPath = join20(customTemplatesDir, templateName);
9421
+ if (fs23.existsSync(customPath)) {
8790
9422
  return { path: customPath, source: "custom" };
8791
9423
  }
8792
9424
  }
8793
- return { path: join19(bundledTemplatesDir, templateName), source: "bundled" };
9425
+ return { path: join20(bundledTemplatesDir, templateName), source: "bundled" };
8794
9426
  }
8795
9427
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
8796
- if (fs22.existsSync(targetPath) && !force) {
9428
+ if (fs23.existsSync(targetPath) && !force) {
8797
9429
  console.log(` Skipped (exists): ${targetPath}`);
8798
9430
  return { created: false, source: source ?? "bundled" };
8799
9431
  }
8800
- const templatePath = sourcePath ?? join19(TEMPLATES_DIR, templateName);
9432
+ const templatePath = sourcePath ?? join20(TEMPLATES_DIR, templateName);
8801
9433
  const resolvedSource = source ?? "bundled";
8802
- let content = fs22.readFileSync(templatePath, "utf-8");
9434
+ let content = fs23.readFileSync(templatePath, "utf-8");
8803
9435
  for (const [key, value] of Object.entries(replacements)) {
8804
9436
  content = content.replaceAll(key, value);
8805
9437
  }
8806
- fs22.writeFileSync(targetPath, content);
9438
+ fs23.writeFileSync(targetPath, content);
8807
9439
  console.log(` Created: ${targetPath} (${resolvedSource})`);
8808
9440
  return { created: true, source: resolvedSource };
8809
9441
  }
8810
9442
  function addToGitignore(cwd) {
8811
- const gitignorePath = path21.join(cwd, ".gitignore");
9443
+ const gitignorePath = path22.join(cwd, ".gitignore");
8812
9444
  const entries = [
8813
9445
  {
8814
9446
  pattern: "/logs/",
@@ -8822,13 +9454,13 @@ function addToGitignore(cwd) {
8822
9454
  },
8823
9455
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
8824
9456
  ];
8825
- if (!fs22.existsSync(gitignorePath)) {
9457
+ if (!fs23.existsSync(gitignorePath)) {
8826
9458
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
8827
- fs22.writeFileSync(gitignorePath, lines.join("\n"));
9459
+ fs23.writeFileSync(gitignorePath, lines.join("\n"));
8828
9460
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
8829
9461
  return;
8830
9462
  }
8831
- const content = fs22.readFileSync(gitignorePath, "utf-8");
9463
+ const content = fs23.readFileSync(gitignorePath, "utf-8");
8832
9464
  const missing = entries.filter((e) => !e.check(content));
8833
9465
  if (missing.length === 0) {
8834
9466
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -8836,59 +9468,59 @@ function addToGitignore(cwd) {
8836
9468
  }
8837
9469
  const additions = missing.map((e) => e.pattern).join("\n");
8838
9470
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
8839
- fs22.writeFileSync(gitignorePath, newContent);
9471
+ fs23.writeFileSync(gitignorePath, newContent);
8840
9472
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
8841
9473
  }
8842
9474
  function installSkills(cwd, provider, force, templatesDir) {
8843
- const skillsTemplatesDir = path21.join(templatesDir, "skills");
8844
- if (!fs22.existsSync(skillsTemplatesDir)) {
9475
+ const skillsTemplatesDir = path22.join(templatesDir, "skills");
9476
+ if (!fs23.existsSync(skillsTemplatesDir)) {
8845
9477
  return { location: "", installed: 0, skipped: 0, type: "none" };
8846
9478
  }
8847
9479
  const isClaudeProvider = provider === "claude" || provider.startsWith("claude");
8848
9480
  const isCodexProvider = provider === "codex";
8849
- const claudeDir = path21.join(cwd, ".claude");
8850
- if (isClaudeProvider || fs22.existsSync(claudeDir)) {
9481
+ const claudeDir = path22.join(cwd, ".claude");
9482
+ if (isClaudeProvider || fs23.existsSync(claudeDir)) {
8851
9483
  ensureDir(claudeDir);
8852
- const skillsDir = path21.join(claudeDir, "skills");
9484
+ const skillsDir = path22.join(claudeDir, "skills");
8853
9485
  ensureDir(skillsDir);
8854
9486
  let installed = 0;
8855
9487
  let skipped = 0;
8856
9488
  for (const skillName of NW_SKILLS) {
8857
- const templateFile = path21.join(skillsTemplatesDir, `${skillName}.md`);
8858
- if (!fs22.existsSync(templateFile)) continue;
8859
- const skillDir = path21.join(skillsDir, skillName);
9489
+ const templateFile = path22.join(skillsTemplatesDir, `${skillName}.md`);
9490
+ if (!fs23.existsSync(templateFile)) continue;
9491
+ const skillDir = path22.join(skillsDir, skillName);
8860
9492
  ensureDir(skillDir);
8861
- const target = path21.join(skillDir, "SKILL.md");
8862
- if (fs22.existsSync(target) && !force) {
9493
+ const target = path22.join(skillDir, "SKILL.md");
9494
+ if (fs23.existsSync(target) && !force) {
8863
9495
  skipped++;
8864
9496
  continue;
8865
9497
  }
8866
- fs22.copyFileSync(templateFile, target);
9498
+ fs23.copyFileSync(templateFile, target);
8867
9499
  installed++;
8868
9500
  }
8869
9501
  return { location: ".claude/skills/", installed, skipped, type: "claude" };
8870
9502
  }
8871
9503
  if (isCodexProvider) {
8872
- const agentsFile = path21.join(cwd, "AGENTS.md");
8873
- const blockFile = path21.join(skillsTemplatesDir, "_codex-block.md");
8874
- if (!fs22.existsSync(blockFile)) {
9504
+ const agentsFile = path22.join(cwd, "AGENTS.md");
9505
+ const blockFile = path22.join(skillsTemplatesDir, "_codex-block.md");
9506
+ if (!fs23.existsSync(blockFile)) {
8875
9507
  return { location: "", installed: 0, skipped: 0, type: "none" };
8876
9508
  }
8877
- const block = fs22.readFileSync(blockFile, "utf-8");
9509
+ const block = fs23.readFileSync(blockFile, "utf-8");
8878
9510
  const marker = "## Night Watch Skills";
8879
- if (!fs22.existsSync(agentsFile)) {
8880
- fs22.writeFileSync(agentsFile, block);
9511
+ if (!fs23.existsSync(agentsFile)) {
9512
+ fs23.writeFileSync(agentsFile, block);
8881
9513
  return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
8882
9514
  }
8883
- const existing = fs22.readFileSync(agentsFile, "utf-8");
9515
+ const existing = fs23.readFileSync(agentsFile, "utf-8");
8884
9516
  if (existing.includes(marker)) {
8885
9517
  if (!force) {
8886
9518
  return { location: "AGENTS.md", installed: 0, skipped: NW_SKILLS.length, type: "codex" };
8887
9519
  }
8888
9520
  const withoutSection = existing.replace(/\n\n## Night Watch Skills[\s\S]*$/, "");
8889
- fs22.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
9521
+ fs23.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
8890
9522
  } else {
8891
- fs22.appendFileSync(agentsFile, "\n\n" + block);
9523
+ fs23.appendFileSync(agentsFile, "\n\n" + block);
8892
9524
  }
8893
9525
  return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
8894
9526
  }
@@ -9012,28 +9644,28 @@ function initCommand(program2) {
9012
9644
  "${DEFAULT_BRANCH}": defaultBranch
9013
9645
  };
9014
9646
  step(6, totalSteps, "Creating PRD directory structure...");
9015
- const prdDirPath = path21.join(cwd, prdDir);
9016
- const doneDirPath = path21.join(prdDirPath, "done");
9647
+ const prdDirPath = path22.join(cwd, prdDir);
9648
+ const doneDirPath = path22.join(prdDirPath, "done");
9017
9649
  ensureDir(doneDirPath);
9018
9650
  success(`Created ${prdDirPath}/`);
9019
9651
  success(`Created ${doneDirPath}/`);
9020
9652
  step(7, totalSteps, "Creating logs directory...");
9021
- const logsPath = path21.join(cwd, LOG_DIR);
9653
+ const logsPath = path22.join(cwd, LOG_DIR);
9022
9654
  ensureDir(logsPath);
9023
9655
  success(`Created ${logsPath}/`);
9024
9656
  addToGitignore(cwd);
9025
9657
  step(8, totalSteps, "Creating instructions directory...");
9026
- const instructionsDir = path21.join(cwd, "instructions");
9658
+ const instructionsDir = path22.join(cwd, "instructions");
9027
9659
  ensureDir(instructionsDir);
9028
9660
  success(`Created ${instructionsDir}/`);
9029
9661
  const existingConfig = loadConfig(cwd);
9030
- const customTemplatesDirPath = path21.join(cwd, existingConfig.templatesDir);
9031
- const customTemplatesDir = fs22.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
9662
+ const customTemplatesDirPath = path22.join(cwd, existingConfig.templatesDir);
9663
+ const customTemplatesDir = fs23.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
9032
9664
  const templateSources = [];
9033
9665
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
9034
9666
  const nwResult = processTemplate(
9035
9667
  "executor.md",
9036
- path21.join(instructionsDir, "executor.md"),
9668
+ path22.join(instructionsDir, "executor.md"),
9037
9669
  replacements,
9038
9670
  force,
9039
9671
  nwResolution.path,
@@ -9047,7 +9679,7 @@ function initCommand(program2) {
9047
9679
  );
9048
9680
  const peResult = processTemplate(
9049
9681
  "prd-executor.md",
9050
- path21.join(instructionsDir, "prd-executor.md"),
9682
+ path22.join(instructionsDir, "prd-executor.md"),
9051
9683
  replacements,
9052
9684
  force,
9053
9685
  peResolution.path,
@@ -9057,7 +9689,7 @@ function initCommand(program2) {
9057
9689
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
9058
9690
  const prResult = processTemplate(
9059
9691
  "pr-reviewer.md",
9060
- path21.join(instructionsDir, "pr-reviewer.md"),
9692
+ path22.join(instructionsDir, "pr-reviewer.md"),
9061
9693
  replacements,
9062
9694
  force,
9063
9695
  prResolution.path,
@@ -9067,7 +9699,7 @@ function initCommand(program2) {
9067
9699
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
9068
9700
  const qaResult = processTemplate(
9069
9701
  "qa.md",
9070
- path21.join(instructionsDir, "qa.md"),
9702
+ path22.join(instructionsDir, "qa.md"),
9071
9703
  replacements,
9072
9704
  force,
9073
9705
  qaResolution.path,
@@ -9077,7 +9709,7 @@ function initCommand(program2) {
9077
9709
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
9078
9710
  const auditResult = processTemplate(
9079
9711
  "audit.md",
9080
- path21.join(instructionsDir, "audit.md"),
9712
+ path22.join(instructionsDir, "audit.md"),
9081
9713
  replacements,
9082
9714
  force,
9083
9715
  auditResolution.path,
@@ -9091,7 +9723,7 @@ function initCommand(program2) {
9091
9723
  );
9092
9724
  const plannerResult = processTemplate(
9093
9725
  "prd-creator.md",
9094
- path21.join(instructionsDir, "prd-creator.md"),
9726
+ path22.join(instructionsDir, "prd-creator.md"),
9095
9727
  replacements,
9096
9728
  force,
9097
9729
  plannerResolution.path,
@@ -9099,8 +9731,8 @@ function initCommand(program2) {
9099
9731
  );
9100
9732
  templateSources.push({ name: "prd-creator.md", source: plannerResult.source });
9101
9733
  step(9, totalSteps, "Creating configuration file...");
9102
- const configPath = path21.join(cwd, CONFIG_FILE_NAME);
9103
- if (fs22.existsSync(configPath) && !force) {
9734
+ const configPath = path22.join(cwd, CONFIG_FILE_NAME);
9735
+ if (fs23.existsSync(configPath) && !force) {
9104
9736
  console.log(` Skipped (exists): ${configPath}`);
9105
9737
  } else {
9106
9738
  const config = buildInitConfig({
@@ -9110,11 +9742,11 @@ function initCommand(program2) {
9110
9742
  reviewerEnabled,
9111
9743
  prdDir
9112
9744
  });
9113
- fs22.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
9745
+ fs23.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
9114
9746
  success(`Created ${configPath}`);
9115
9747
  }
9116
9748
  step(10, totalSteps, "Setting up GitHub Project board...");
9117
- const existingRaw = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
9749
+ const existingRaw = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
9118
9750
  const existingBoard = existingRaw.boardProvider;
9119
9751
  let boardSetupStatus = "Skipped";
9120
9752
  if (existingBoard?.projectNumber && !force) {
@@ -9136,13 +9768,13 @@ function initCommand(program2) {
9136
9768
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
9137
9769
  const boardTitle = `${projectName} Night Watch`;
9138
9770
  const board = await provider.setupBoard(boardTitle);
9139
- const rawConfig = JSON.parse(fs22.readFileSync(configPath, "utf-8"));
9771
+ const rawConfig = JSON.parse(fs23.readFileSync(configPath, "utf-8"));
9140
9772
  rawConfig.boardProvider = {
9141
9773
  enabled: true,
9142
9774
  provider: "github",
9143
9775
  projectNumber: board.number
9144
9776
  };
9145
- fs22.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
9777
+ fs23.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
9146
9778
  boardSetupStatus = `Created (#${board.number})`;
9147
9779
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
9148
9780
  } catch (boardErr) {
@@ -9278,6 +9910,7 @@ function buildBaseEnvVars(config, jobType, isDryRun) {
9278
9910
  if (config.defaultBranch) {
9279
9911
  env.NW_DEFAULT_BRANCH = config.defaultBranch;
9280
9912
  }
9913
+ env.NW_PR_RESOLVER_READY_LABEL = config.prResolver?.readyLabel ?? "ready-to-merge";
9281
9914
  env.NW_GIT_PUSH_NO_VERIFY = config.gitPushNoVerify ? "1" : "0";
9282
9915
  if (config.providerEnv) {
9283
9916
  Object.assign(env, config.providerEnv);
@@ -9326,8 +9959,8 @@ function getTelegramStatusWebhooks(config) {
9326
9959
  }
9327
9960
 
9328
9961
  // src/commands/run.ts
9329
- import * as fs23 from "fs";
9330
- import * as path22 from "path";
9962
+ import * as fs24 from "fs";
9963
+ import * as path23 from "path";
9331
9964
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
9332
9965
  if (exitCode === 124) {
9333
9966
  return "run_timeout";
@@ -9359,12 +9992,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
9359
9992
  return scriptStatus === "skip_no_eligible_prd";
9360
9993
  }
9361
9994
  function getCrossProjectFallbackCandidates(currentProjectDir) {
9362
- const current = path22.resolve(currentProjectDir);
9995
+ const current = path23.resolve(currentProjectDir);
9363
9996
  const { valid, invalid } = validateRegistry();
9364
9997
  for (const entry of invalid) {
9365
9998
  warn(`Skipping invalid registry entry: ${entry.path}`);
9366
9999
  }
9367
- return valid.filter((entry) => path22.resolve(entry.path) !== current);
10000
+ return valid.filter((entry) => path23.resolve(entry.path) !== current);
9368
10001
  }
9369
10002
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
9370
10003
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -9374,7 +10007,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
9374
10007
  if (nonTelegramWebhooks.length > 0) {
9375
10008
  const _rateLimitCtx = {
9376
10009
  event: "rate_limit_fallback",
9377
- projectName: path22.basename(projectDir),
10010
+ projectName: path23.basename(projectDir),
9378
10011
  exitCode,
9379
10012
  provider: config.provider
9380
10013
  };
@@ -9406,7 +10039,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
9406
10039
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
9407
10040
  const _ctx = {
9408
10041
  event,
9409
- projectName: path22.basename(projectDir),
10042
+ projectName: path23.basename(projectDir),
9410
10043
  exitCode,
9411
10044
  provider: config.provider,
9412
10045
  prdName: scriptResult?.data.prd,
@@ -9570,20 +10203,20 @@ function applyCliOverrides(config, options) {
9570
10203
  return overridden;
9571
10204
  }
9572
10205
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
9573
- const absolutePrdDir = path22.join(projectDir, prdDir);
9574
- const doneDir = path22.join(absolutePrdDir, "done");
10206
+ const absolutePrdDir = path23.join(projectDir, prdDir);
10207
+ const doneDir = path23.join(absolutePrdDir, "done");
9575
10208
  const pending = [];
9576
10209
  const completed = [];
9577
- if (fs23.existsSync(absolutePrdDir)) {
9578
- const entries = fs23.readdirSync(absolutePrdDir, { withFileTypes: true });
10210
+ if (fs24.existsSync(absolutePrdDir)) {
10211
+ const entries = fs24.readdirSync(absolutePrdDir, { withFileTypes: true });
9579
10212
  for (const entry of entries) {
9580
10213
  if (entry.isFile() && entry.name.endsWith(".md")) {
9581
- const claimPath = path22.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
10214
+ const claimPath = path23.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
9582
10215
  let claimed = false;
9583
10216
  let claimInfo = null;
9584
- if (fs23.existsSync(claimPath)) {
10217
+ if (fs24.existsSync(claimPath)) {
9585
10218
  try {
9586
- const content = fs23.readFileSync(claimPath, "utf-8");
10219
+ const content = fs24.readFileSync(claimPath, "utf-8");
9587
10220
  const data = JSON.parse(content);
9588
10221
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
9589
10222
  if (age < maxRuntime) {
@@ -9597,8 +10230,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
9597
10230
  }
9598
10231
  }
9599
10232
  }
9600
- if (fs23.existsSync(doneDir)) {
9601
- const entries = fs23.readdirSync(doneDir, { withFileTypes: true });
10233
+ if (fs24.existsSync(doneDir)) {
10234
+ const entries = fs24.readdirSync(doneDir, { withFileTypes: true });
9602
10235
  for (const entry of entries) {
9603
10236
  if (entry.isFile() && entry.name.endsWith(".md")) {
9604
10237
  completed.push(entry.name);
@@ -9758,7 +10391,7 @@ ${stderr}`);
9758
10391
  // src/commands/review.ts
9759
10392
  init_dist();
9760
10393
  import { execFileSync as execFileSync5 } from "child_process";
9761
- import * as path23 from "path";
10394
+ import * as path24 from "path";
9762
10395
  function shouldSendReviewNotification(scriptStatus) {
9763
10396
  if (!scriptStatus) {
9764
10397
  return true;
@@ -9824,76 +10457,6 @@ function parseFinalReviewScore(raw) {
9824
10457
  }
9825
10458
  return parsed;
9826
10459
  }
9827
- function postReadyForHumanReviewComment(prNumber, finalScore, cwd) {
9828
- const markerName = "night-watch-ready-for-review";
9829
- let headRefOid = "";
9830
- try {
9831
- headRefOid = execFileSync5(
9832
- "gh",
9833
- ["pr", "view", String(prNumber), "--json", "headRefOid", "--jq", ".headRefOid"],
9834
- {
9835
- cwd,
9836
- encoding: "utf-8",
9837
- stdio: ["pipe", "pipe", "pipe"]
9838
- }
9839
- ).trim();
9840
- } catch {
9841
- headRefOid = "";
9842
- }
9843
- if (headRefOid) {
9844
- try {
9845
- const existingComments = execFileSync5(
9846
- "gh",
9847
- ["api", `repos/{owner}/{repo}/issues/${prNumber}/comments`, "--jq", ".[].body"],
9848
- {
9849
- cwd,
9850
- encoding: "utf-8",
9851
- stdio: ["pipe", "pipe", "pipe"]
9852
- }
9853
- );
9854
- const marker2 = `<!-- ${markerName} headRefOid:${headRefOid} -->`;
9855
- if (existingComments.includes(marker2)) {
9856
- try {
9857
- execFileSync5("gh", ["pr", "edit", String(prNumber), "--add-label", "ready-for-review"], {
9858
- cwd,
9859
- encoding: "utf-8",
9860
- stdio: ["pipe", "pipe", "pipe"]
9861
- });
9862
- } catch {
9863
- }
9864
- return;
9865
- }
9866
- } catch {
9867
- }
9868
- }
9869
- const scoreNote = finalScore !== void 0 ? ` (score: ${finalScore}/100)` : "";
9870
- const shortSha = headRefOid ? headRefOid.slice(0, 12) : "";
9871
- const marker = headRefOid ? `<!-- ${markerName} headRefOid:${headRefOid} -->
9872
-
9873
- ` : "";
9874
- const shaNote = shortSha ? ` at commit \`${shortSha}\`` : "";
9875
- const body = `${marker}## \u2705 Ready for Human Review
9876
-
9877
- Night Watch has reviewed this PR${scoreNote}${shaNote} and found no issues requiring automated fixes for the current head.
9878
-
9879
- This PR is ready for human code review and merge.`;
9880
- try {
9881
- execFileSync5("gh", ["pr", "comment", String(prNumber), "--body", body], {
9882
- cwd,
9883
- encoding: "utf-8",
9884
- stdio: ["pipe", "pipe", "pipe"]
9885
- });
9886
- } catch {
9887
- }
9888
- try {
9889
- execFileSync5("gh", ["pr", "edit", String(prNumber), "--add-label", "ready-for-review"], {
9890
- cwd,
9891
- encoding: "utf-8",
9892
- stdio: ["pipe", "pipe", "pipe"]
9893
- });
9894
- } catch {
9895
- }
9896
- }
9897
10460
  function buildEnvVars2(config, options) {
9898
10461
  const env = buildBaseEnvVars(config, "reviewer", options.dryRun);
9899
10462
  env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
@@ -9903,6 +10466,7 @@ function buildEnvVars2(config, options) {
9903
10466
  env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
9904
10467
  env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
9905
10468
  env.NW_PRD_DIR = config.prdDir;
10469
+ env.NW_PR_RESOLVER_READY_LABEL = config.prResolver.readyLabel;
9906
10470
  env.NW_CLAUDE_MODEL_ID = CLAUDE_MODEL_IDS[config.primaryFallbackModel ?? config.claudeModel ?? "sonnet"];
9907
10471
  return env;
9908
10472
  }
@@ -10108,7 +10672,7 @@ ${stderr}`);
10108
10672
  const reviewEvent = legacyNoChangesNeeded ? "review_ready_for_human" : "review_completed";
10109
10673
  await sendNotifications(config, {
10110
10674
  event: reviewEvent,
10111
- projectName: path23.basename(projectDir),
10675
+ projectName: path24.basename(projectDir),
10112
10676
  exitCode,
10113
10677
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
10114
10678
  prUrl: fallbackPrDetails?.url,
@@ -10124,13 +10688,10 @@ ${stderr}`);
10124
10688
  } else {
10125
10689
  for (const target of notificationTargets) {
10126
10690
  const prDetails = fallbackPrDetails?.number === target.prNumber ? fallbackPrDetails : fetchPrDetailsByNumber(target.prNumber, projectDir);
10127
- if (target.noChangesNeeded && prDetails?.number) {
10128
- postReadyForHumanReviewComment(prDetails.number, finalScore, projectDir);
10129
- }
10130
10691
  const reviewEvent = target.noChangesNeeded ? "review_ready_for_human" : "review_completed";
10131
10692
  await sendNotifications(config, {
10132
10693
  event: reviewEvent,
10133
- projectName: path23.basename(projectDir),
10694
+ projectName: path24.basename(projectDir),
10134
10695
  exitCode,
10135
10696
  provider: formatProviderDisplay(
10136
10697
  envVars.NW_PROVIDER_CMD,
@@ -10155,7 +10716,7 @@ ${stderr}`);
10155
10716
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
10156
10717
  const _mergeCtx = {
10157
10718
  event: "pr_auto_merged",
10158
- projectName: path23.basename(projectDir),
10719
+ projectName: path24.basename(projectDir),
10159
10720
  exitCode,
10160
10721
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
10161
10722
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -10180,7 +10741,7 @@ ${stderr}`);
10180
10741
 
10181
10742
  // src/commands/qa.ts
10182
10743
  init_dist();
10183
- import * as path24 from "path";
10744
+ import * as path25 from "path";
10184
10745
  function shouldSendQaNotification(scriptStatus) {
10185
10746
  if (!scriptStatus) {
10186
10747
  return true;
@@ -10321,7 +10882,7 @@ ${stderr}`);
10321
10882
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
10322
10883
  const _qaCtx = {
10323
10884
  event: "qa_completed",
10324
- projectName: path24.basename(projectDir),
10885
+ projectName: path25.basename(projectDir),
10325
10886
  exitCode,
10326
10887
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
10327
10888
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -10347,8 +10908,8 @@ ${stderr}`);
10347
10908
 
10348
10909
  // src/commands/audit.ts
10349
10910
  init_dist();
10350
- import * as fs24 from "fs";
10351
- import * as path25 from "path";
10911
+ import * as fs25 from "fs";
10912
+ import * as path26 from "path";
10352
10913
  function buildEnvVars4(config, options) {
10353
10914
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
10354
10915
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -10392,7 +10953,7 @@ function auditCommand(program2) {
10392
10953
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
10393
10954
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
10394
10955
  configTable.push(["Target Column", config.audit.targetColumn]);
10395
- configTable.push(["Report File", path25.join(projectDir, "logs", "audit-report.md")]);
10956
+ configTable.push(["Report File", path26.join(projectDir, "logs", "audit-report.md")]);
10396
10957
  console.log(configTable.toString());
10397
10958
  header("Provider Invocation");
10398
10959
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -10427,8 +10988,8 @@ ${stderr}`);
10427
10988
  } else if (scriptResult?.status?.startsWith("skip_")) {
10428
10989
  spinner.succeed("Code audit skipped");
10429
10990
  } else {
10430
- const reportPath = path25.join(projectDir, "logs", "audit-report.md");
10431
- if (!fs24.existsSync(reportPath)) {
10991
+ const reportPath = path26.join(projectDir, "logs", "audit-report.md");
10992
+ if (!fs25.existsSync(reportPath)) {
10432
10993
  spinner.fail("Code audit finished without a report file");
10433
10994
  process.exit(1);
10434
10995
  }
@@ -10445,9 +11006,9 @@ ${stderr}`);
10445
11006
  const providerExit = scriptResult?.data?.provider_exit;
10446
11007
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
10447
11008
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
10448
- const logPath = path25.join(projectDir, "logs", "audit.log");
10449
- if (fs24.existsSync(logPath)) {
10450
- const logLines = fs24.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
11009
+ const logPath = path26.join(projectDir, "logs", "audit.log");
11010
+ if (fs25.existsSync(logPath)) {
11011
+ const logLines = fs25.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
10451
11012
  if (logLines.length > 0) {
10452
11013
  process.stderr.write(logLines.join("\n") + "\n");
10453
11014
  }
@@ -10521,16 +11082,16 @@ function analyticsCommand(program2) {
10521
11082
  // src/commands/install.ts
10522
11083
  init_dist();
10523
11084
  import { execSync as execSync4 } from "child_process";
10524
- import * as path26 from "path";
10525
- import * as fs25 from "fs";
11085
+ import * as path27 from "path";
11086
+ import * as fs26 from "fs";
10526
11087
  function shellQuote(value) {
10527
11088
  return `'${value.replace(/'/g, `'"'"'`)}'`;
10528
11089
  }
10529
11090
  function getNightWatchBinPath() {
10530
11091
  try {
10531
11092
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
10532
- const binPath = path26.join(npmBin, "night-watch");
10533
- if (fs25.existsSync(binPath)) {
11093
+ const binPath = path27.join(npmBin, "night-watch");
11094
+ if (fs26.existsSync(binPath)) {
10534
11095
  return binPath;
10535
11096
  }
10536
11097
  } catch {
@@ -10543,17 +11104,17 @@ function getNightWatchBinPath() {
10543
11104
  }
10544
11105
  function getNodeBinDir() {
10545
11106
  if (process.execPath && process.execPath !== "node") {
10546
- return path26.dirname(process.execPath);
11107
+ return path27.dirname(process.execPath);
10547
11108
  }
10548
11109
  try {
10549
11110
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
10550
- return path26.dirname(nodePath);
11111
+ return path27.dirname(nodePath);
10551
11112
  } catch {
10552
11113
  return "";
10553
11114
  }
10554
11115
  }
10555
11116
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
10556
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path26.dirname(nightWatchBin) : "";
11117
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path27.dirname(nightWatchBin) : "";
10557
11118
  const pathParts = Array.from(
10558
11119
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
10559
11120
  );
@@ -10569,12 +11130,12 @@ function performInstall(projectDir, config, options) {
10569
11130
  const nightWatchBin = getNightWatchBinPath();
10570
11131
  const projectName = getProjectName(projectDir);
10571
11132
  const marker = generateMarker(projectName);
10572
- const logDir = path26.join(projectDir, LOG_DIR);
10573
- if (!fs25.existsSync(logDir)) {
10574
- fs25.mkdirSync(logDir, { recursive: true });
11133
+ const logDir = path27.join(projectDir, LOG_DIR);
11134
+ if (!fs26.existsSync(logDir)) {
11135
+ fs26.mkdirSync(logDir, { recursive: true });
10575
11136
  }
10576
- const executorLog = path26.join(logDir, "executor.log");
10577
- const reviewerLog = path26.join(logDir, "reviewer.log");
11137
+ const executorLog = path27.join(logDir, "executor.log");
11138
+ const reviewerLog = path27.join(logDir, "reviewer.log");
10578
11139
  if (!options?.force) {
10579
11140
  const existingEntries2 = Array.from(
10580
11141
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -10611,7 +11172,7 @@ function performInstall(projectDir, config, options) {
10611
11172
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
10612
11173
  if (installSlicer) {
10613
11174
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
10614
- const slicerLog = path26.join(logDir, "slicer.log");
11175
+ const slicerLog = path27.join(logDir, "slicer.log");
10615
11176
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
10616
11177
  entries.push(slicerEntry);
10617
11178
  }
@@ -10619,7 +11180,7 @@ function performInstall(projectDir, config, options) {
10619
11180
  const installQa = disableQa ? false : config.qa.enabled;
10620
11181
  if (installQa) {
10621
11182
  const qaSchedule = config.qa.schedule;
10622
- const qaLog = path26.join(logDir, "qa.log");
11183
+ const qaLog = path27.join(logDir, "qa.log");
10623
11184
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
10624
11185
  entries.push(qaEntry);
10625
11186
  }
@@ -10627,7 +11188,7 @@ function performInstall(projectDir, config, options) {
10627
11188
  const installAudit = disableAudit ? false : config.audit.enabled;
10628
11189
  if (installAudit) {
10629
11190
  const auditSchedule = config.audit.schedule;
10630
- const auditLog = path26.join(logDir, "audit.log");
11191
+ const auditLog = path27.join(logDir, "audit.log");
10631
11192
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
10632
11193
  entries.push(auditEntry);
10633
11194
  }
@@ -10635,7 +11196,7 @@ function performInstall(projectDir, config, options) {
10635
11196
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
10636
11197
  if (installAnalytics) {
10637
11198
  const analyticsSchedule = config.analytics.schedule;
10638
- const analyticsLog = path26.join(logDir, "analytics.log");
11199
+ const analyticsLog = path27.join(logDir, "analytics.log");
10639
11200
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
10640
11201
  entries.push(analyticsEntry);
10641
11202
  }
@@ -10643,7 +11204,7 @@ function performInstall(projectDir, config, options) {
10643
11204
  const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10644
11205
  if (installPrResolver) {
10645
11206
  const prResolverSchedule = config.prResolver.schedule;
10646
- const prResolverLog = path26.join(logDir, "pr-resolver.log");
11207
+ const prResolverLog = path27.join(logDir, "pr-resolver.log");
10647
11208
  const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10648
11209
  entries.push(prResolverEntry);
10649
11210
  }
@@ -10651,7 +11212,7 @@ function performInstall(projectDir, config, options) {
10651
11212
  const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
10652
11213
  if (installMerger) {
10653
11214
  const mergerSchedule = config.merger.schedule;
10654
- const mergerLog = path26.join(logDir, "merger.log");
11215
+ const mergerLog = path27.join(logDir, "merger.log");
10655
11216
  const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
10656
11217
  entries.push(mergerEntry);
10657
11218
  }
@@ -10682,12 +11243,12 @@ function installCommand(program2) {
10682
11243
  const nightWatchBin = getNightWatchBinPath();
10683
11244
  const projectName = getProjectName(projectDir);
10684
11245
  const marker = generateMarker(projectName);
10685
- const logDir = path26.join(projectDir, LOG_DIR);
10686
- if (!fs25.existsSync(logDir)) {
10687
- fs25.mkdirSync(logDir, { recursive: true });
11246
+ const logDir = path27.join(projectDir, LOG_DIR);
11247
+ if (!fs26.existsSync(logDir)) {
11248
+ fs26.mkdirSync(logDir, { recursive: true });
10688
11249
  }
10689
- const executorLog = path26.join(logDir, "executor.log");
10690
- const reviewerLog = path26.join(logDir, "reviewer.log");
11250
+ const executorLog = path27.join(logDir, "executor.log");
11251
+ const reviewerLog = path27.join(logDir, "reviewer.log");
10691
11252
  const existingEntries = Array.from(
10692
11253
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
10693
11254
  );
@@ -10723,7 +11284,7 @@ function installCommand(program2) {
10723
11284
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
10724
11285
  let slicerLog;
10725
11286
  if (installSlicer) {
10726
- slicerLog = path26.join(logDir, "slicer.log");
11287
+ slicerLog = path27.join(logDir, "slicer.log");
10727
11288
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
10728
11289
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
10729
11290
  entries.push(slicerEntry);
@@ -10732,7 +11293,7 @@ function installCommand(program2) {
10732
11293
  const installQa = disableQa ? false : config.qa.enabled;
10733
11294
  let qaLog;
10734
11295
  if (installQa) {
10735
- qaLog = path26.join(logDir, "qa.log");
11296
+ qaLog = path27.join(logDir, "qa.log");
10736
11297
  const qaSchedule = config.qa.schedule;
10737
11298
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
10738
11299
  entries.push(qaEntry);
@@ -10741,7 +11302,7 @@ function installCommand(program2) {
10741
11302
  const installAudit = disableAudit ? false : config.audit.enabled;
10742
11303
  let auditLog;
10743
11304
  if (installAudit) {
10744
- auditLog = path26.join(logDir, "audit.log");
11305
+ auditLog = path27.join(logDir, "audit.log");
10745
11306
  const auditSchedule = config.audit.schedule;
10746
11307
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
10747
11308
  entries.push(auditEntry);
@@ -10750,7 +11311,7 @@ function installCommand(program2) {
10750
11311
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
10751
11312
  let analyticsLog;
10752
11313
  if (installAnalytics) {
10753
- analyticsLog = path26.join(logDir, "analytics.log");
11314
+ analyticsLog = path27.join(logDir, "analytics.log");
10754
11315
  const analyticsSchedule = config.analytics.schedule;
10755
11316
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
10756
11317
  entries.push(analyticsEntry);
@@ -10759,7 +11320,7 @@ function installCommand(program2) {
10759
11320
  const installPrResolver = disablePrResolver ? false : config.prResolver.enabled;
10760
11321
  let prResolverLog;
10761
11322
  if (installPrResolver) {
10762
- prResolverLog = path26.join(logDir, "pr-resolver.log");
11323
+ prResolverLog = path27.join(logDir, "pr-resolver.log");
10763
11324
  const prResolverSchedule = config.prResolver.schedule;
10764
11325
  const prResolverEntry = `${prResolverSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} resolve >> ${shellQuote(prResolverLog)} 2>&1 ${marker}`;
10765
11326
  entries.push(prResolverEntry);
@@ -10768,7 +11329,7 @@ function installCommand(program2) {
10768
11329
  const installMerger = disableMerger ? false : config.merger?.enabled ?? false;
10769
11330
  let mergerLog;
10770
11331
  if (installMerger) {
10771
- mergerLog = path26.join(logDir, "merger.log");
11332
+ mergerLog = path27.join(logDir, "merger.log");
10772
11333
  const mergerSchedule = config.merger.schedule;
10773
11334
  const mergerEntry = `${mergerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} merge >> ${shellQuote(mergerLog)} 2>&1 ${marker}`;
10774
11335
  entries.push(mergerEntry);
@@ -10822,8 +11383,8 @@ function installCommand(program2) {
10822
11383
 
10823
11384
  // src/commands/uninstall.ts
10824
11385
  init_dist();
10825
- import * as path27 from "path";
10826
- import * as fs26 from "fs";
11386
+ import * as path28 from "path";
11387
+ import * as fs27 from "fs";
10827
11388
  function performUninstall(projectDir, options) {
10828
11389
  try {
10829
11390
  const projectName = getProjectName(projectDir);
@@ -10838,8 +11399,8 @@ function performUninstall(projectDir, options) {
10838
11399
  const removedCount = removeEntriesForProject(projectDir, marker);
10839
11400
  unregisterProject(projectDir);
10840
11401
  if (!options?.keepLogs) {
10841
- const logDir = path27.join(projectDir, "logs");
10842
- if (fs26.existsSync(logDir)) {
11402
+ const logDir = path28.join(projectDir, "logs");
11403
+ if (fs27.existsSync(logDir)) {
10843
11404
  const logFiles = [
10844
11405
  "executor.log",
10845
11406
  "reviewer.log",
@@ -10848,15 +11409,15 @@ function performUninstall(projectDir, options) {
10848
11409
  "pr-resolver.log"
10849
11410
  ];
10850
11411
  logFiles.forEach((logFile) => {
10851
- const logPath = path27.join(logDir, logFile);
10852
- if (fs26.existsSync(logPath)) {
10853
- fs26.unlinkSync(logPath);
11412
+ const logPath = path28.join(logDir, logFile);
11413
+ if (fs27.existsSync(logPath)) {
11414
+ fs27.unlinkSync(logPath);
10854
11415
  }
10855
11416
  });
10856
11417
  try {
10857
- const remainingFiles = fs26.readdirSync(logDir);
11418
+ const remainingFiles = fs27.readdirSync(logDir);
10858
11419
  if (remainingFiles.length === 0) {
10859
- fs26.rmdirSync(logDir);
11420
+ fs27.rmdirSync(logDir);
10860
11421
  }
10861
11422
  } catch {
10862
11423
  }
@@ -10889,8 +11450,8 @@ function uninstallCommand(program2) {
10889
11450
  existingEntries.forEach((entry) => dim(` ${entry}`));
10890
11451
  const removedCount = removeEntriesForProject(projectDir, marker);
10891
11452
  if (!options.keepLogs) {
10892
- const logDir = path27.join(projectDir, "logs");
10893
- if (fs26.existsSync(logDir)) {
11453
+ const logDir = path28.join(projectDir, "logs");
11454
+ if (fs27.existsSync(logDir)) {
10894
11455
  const logFiles = [
10895
11456
  "executor.log",
10896
11457
  "reviewer.log",
@@ -10900,16 +11461,16 @@ function uninstallCommand(program2) {
10900
11461
  ];
10901
11462
  let logsRemoved = 0;
10902
11463
  logFiles.forEach((logFile) => {
10903
- const logPath = path27.join(logDir, logFile);
10904
- if (fs26.existsSync(logPath)) {
10905
- fs26.unlinkSync(logPath);
11464
+ const logPath = path28.join(logDir, logFile);
11465
+ if (fs27.existsSync(logPath)) {
11466
+ fs27.unlinkSync(logPath);
10906
11467
  logsRemoved++;
10907
11468
  }
10908
11469
  });
10909
11470
  try {
10910
- const remainingFiles = fs26.readdirSync(logDir);
11471
+ const remainingFiles = fs27.readdirSync(logDir);
10911
11472
  if (remainingFiles.length === 0) {
10912
- fs26.rmdirSync(logDir);
11473
+ fs27.rmdirSync(logDir);
10913
11474
  }
10914
11475
  } catch {
10915
11476
  }
@@ -11195,14 +11756,14 @@ function statusCommand(program2) {
11195
11756
  // src/commands/logs.ts
11196
11757
  init_dist();
11197
11758
  import { spawn as spawn3 } from "child_process";
11198
- import * as path28 from "path";
11199
- import * as fs27 from "fs";
11759
+ import * as path29 from "path";
11760
+ import * as fs28 from "fs";
11200
11761
  function getLastLines(filePath, lineCount) {
11201
- if (!fs27.existsSync(filePath)) {
11762
+ if (!fs28.existsSync(filePath)) {
11202
11763
  return `Log file not found: ${filePath}`;
11203
11764
  }
11204
11765
  try {
11205
- const content = fs27.readFileSync(filePath, "utf-8");
11766
+ const content = fs28.readFileSync(filePath, "utf-8");
11206
11767
  const lines = content.trim().split("\n");
11207
11768
  return lines.slice(-lineCount).join("\n");
11208
11769
  } catch (error2) {
@@ -11210,7 +11771,7 @@ function getLastLines(filePath, lineCount) {
11210
11771
  }
11211
11772
  }
11212
11773
  function followLog(filePath) {
11213
- if (!fs27.existsSync(filePath)) {
11774
+ if (!fs28.existsSync(filePath)) {
11214
11775
  console.log(`Log file not found: ${filePath}`);
11215
11776
  console.log("The log file will be created when the first execution runs.");
11216
11777
  return;
@@ -11234,15 +11795,15 @@ function logsCommand(program2) {
11234
11795
  ).action(async (options) => {
11235
11796
  try {
11236
11797
  const projectDir = process.cwd();
11237
- const logDir = path28.join(projectDir, LOG_DIR);
11798
+ const logDir = path29.join(projectDir, LOG_DIR);
11238
11799
  const lineCount = parseInt(options.lines || "50", 10);
11239
- const executorLog = path28.join(logDir, EXECUTOR_LOG_FILE);
11240
- const reviewerLog = path28.join(logDir, REVIEWER_LOG_FILE);
11241
- const qaLog = path28.join(logDir, `${QA_LOG_NAME}.log`);
11242
- const auditLog = path28.join(logDir, `${AUDIT_LOG_NAME}.log`);
11243
- const plannerLog = path28.join(logDir, `${PLANNER_LOG_NAME}.log`);
11244
- const analyticsLog = path28.join(logDir, `${ANALYTICS_LOG_NAME}.log`);
11245
- const mergerLog = path28.join(logDir, `${MERGER_LOG_NAME}.log`);
11800
+ const executorLog = path29.join(logDir, EXECUTOR_LOG_FILE);
11801
+ const reviewerLog = path29.join(logDir, REVIEWER_LOG_FILE);
11802
+ const qaLog = path29.join(logDir, `${QA_LOG_NAME}.log`);
11803
+ const auditLog = path29.join(logDir, `${AUDIT_LOG_NAME}.log`);
11804
+ const plannerLog = path29.join(logDir, `${PLANNER_LOG_NAME}.log`);
11805
+ const analyticsLog = path29.join(logDir, `${ANALYTICS_LOG_NAME}.log`);
11806
+ const mergerLog = path29.join(logDir, `${MERGER_LOG_NAME}.log`);
11246
11807
  const logType = options.type?.toLowerCase() || "all";
11247
11808
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
11248
11809
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
@@ -11325,8 +11886,8 @@ function logsCommand(program2) {
11325
11886
  // src/commands/prd.ts
11326
11887
  init_dist();
11327
11888
  import { execSync as execSync5, spawn as spawn4, spawnSync } from "child_process";
11328
- import * as fs28 from "fs";
11329
- import * as path29 from "path";
11889
+ import * as fs29 from "fs";
11890
+ import * as path30 from "path";
11330
11891
  import { fileURLToPath as fileURLToPath4 } from "url";
11331
11892
  import { dirname as dirname9 } from "path";
11332
11893
  var __filename3 = fileURLToPath4(import.meta.url);
@@ -11334,21 +11895,21 @@ var __dirname3 = dirname9(__filename3);
11334
11895
  function findTemplatesDir2(startDir) {
11335
11896
  let current = startDir;
11336
11897
  for (let i = 0; i < 8; i++) {
11337
- const candidate = path29.join(current, "templates");
11338
- if (fs28.existsSync(candidate) && fs28.statSync(candidate).isDirectory()) {
11898
+ const candidate = path30.join(current, "templates");
11899
+ if (fs29.existsSync(candidate) && fs29.statSync(candidate).isDirectory()) {
11339
11900
  return candidate;
11340
11901
  }
11341
- current = path29.dirname(current);
11902
+ current = path30.dirname(current);
11342
11903
  }
11343
- return path29.join(startDir, "templates");
11904
+ return path30.join(startDir, "templates");
11344
11905
  }
11345
11906
  var TEMPLATES_DIR2 = findTemplatesDir2(__dirname3);
11346
11907
  function slugify2(name) {
11347
11908
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
11348
11909
  }
11349
11910
  function getNextPrdNumber2(prdDir) {
11350
- if (!fs28.existsSync(prdDir)) return 1;
11351
- const files = fs28.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
11911
+ if (!fs29.existsSync(prdDir)) return 1;
11912
+ const files = fs29.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
11352
11913
  const numbers = files.map((f) => {
11353
11914
  const match = f.match(/^(\d+)-/);
11354
11915
  return match ? parseInt(match[1], 10) : 0;
@@ -11429,13 +11990,13 @@ function resolveGitHubBlobUrl(projectDir, relPath) {
11429
11990
  return null;
11430
11991
  }
11431
11992
  const ref = branch && branch !== "HEAD" ? branch : "main";
11432
- return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path29.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
11993
+ return `${httpsBase}/blob/${encodeURIComponent(ref).replace(/%2F/g, "/")}/${relPath.split(path30.sep).map((segment) => encodeURIComponent(segment)).join("/")}`;
11433
11994
  } catch {
11434
11995
  return null;
11435
11996
  }
11436
11997
  }
11437
11998
  function buildGithubIssueBody(prdPath, projectDir, prdContent) {
11438
- const relPath = path29.relative(projectDir, prdPath);
11999
+ const relPath = path30.relative(projectDir, prdPath);
11439
12000
  const blobUrl = resolveGitHubBlobUrl(projectDir, relPath);
11440
12001
  const fileLine = blobUrl ? `PRD file: [\`${relPath}\`](${blobUrl})` : `PRD file: \`${relPath}\``;
11441
12002
  return `${fileLine}
@@ -11446,13 +12007,13 @@ ${prdContent}
11446
12007
  Created via \`night-watch prd create\`.`;
11447
12008
  }
11448
12009
  async function generatePrdWithClaude(description, projectDir, model) {
11449
- const bundledTemplatePath = path29.join(TEMPLATES_DIR2, "prd-creator.md");
11450
- const installedTemplatePath = path29.join(projectDir, "instructions", "prd-creator.md");
11451
- const templatePath = fs28.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
11452
- if (!fs28.existsSync(templatePath)) {
12010
+ const bundledTemplatePath = path30.join(TEMPLATES_DIR2, "prd-creator.md");
12011
+ const installedTemplatePath = path30.join(projectDir, "instructions", "prd-creator.md");
12012
+ const templatePath = fs29.existsSync(installedTemplatePath) ? installedTemplatePath : bundledTemplatePath;
12013
+ if (!fs29.existsSync(templatePath)) {
11453
12014
  return null;
11454
12015
  }
11455
- const planningPrinciples = fs28.readFileSync(templatePath, "utf-8");
12016
+ const planningPrinciples = fs29.readFileSync(templatePath, "utf-8");
11456
12017
  const prompt = buildPrdPrompt(description, projectDir, planningPrinciples);
11457
12018
  const modelId = model ?? CLAUDE_MODEL_IDS.opus;
11458
12019
  const env = buildNativeClaudeEnv(process.env);
@@ -11520,17 +12081,17 @@ function runGh(args, cwd) {
11520
12081
  return null;
11521
12082
  }
11522
12083
  function createGithubIssue(title, prdPath, projectDir, prdContent) {
11523
- const tmpFile = path29.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
12084
+ const tmpFile = path30.join(projectDir, `.prd-issue-body-${Date.now()}.tmp`);
11524
12085
  try {
11525
12086
  const body = buildGithubIssueBody(prdPath, projectDir, prdContent);
11526
- fs28.writeFileSync(tmpFile, body, "utf-8");
12087
+ fs29.writeFileSync(tmpFile, body, "utf-8");
11527
12088
  const baseArgs = ["issue", "create", "--title", `PRD: ${title}`, "--body-file", tmpFile];
11528
12089
  return runGh([...baseArgs, "--label", "prd"], projectDir) ?? runGh(baseArgs, projectDir);
11529
12090
  } catch {
11530
12091
  return null;
11531
12092
  } finally {
11532
12093
  try {
11533
- fs28.unlinkSync(tmpFile);
12094
+ fs29.unlinkSync(tmpFile);
11534
12095
  } catch {
11535
12096
  }
11536
12097
  }
@@ -11542,10 +12103,10 @@ function parseDependencies(content) {
11542
12103
  }
11543
12104
  function isClaimActive(claimPath, maxRuntime) {
11544
12105
  try {
11545
- if (!fs28.existsSync(claimPath)) {
12106
+ if (!fs29.existsSync(claimPath)) {
11546
12107
  return { active: false };
11547
12108
  }
11548
- const content = fs28.readFileSync(claimPath, "utf-8");
12109
+ const content = fs29.readFileSync(claimPath, "utf-8");
11549
12110
  const claim = JSON.parse(content);
11550
12111
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
11551
12112
  if (age < maxRuntime) {
@@ -11560,9 +12121,9 @@ function prdCommand(program2) {
11560
12121
  const prd = program2.command("prd").description("Manage PRD files");
11561
12122
  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) => {
11562
12123
  const projectDir = process.cwd();
11563
- const prdDir = path29.join(projectDir, resolvePrdCreateDir());
11564
- if (!fs28.existsSync(prdDir)) {
11565
- fs28.mkdirSync(prdDir, { recursive: true });
12124
+ const prdDir = path30.join(projectDir, resolvePrdCreateDir());
12125
+ if (!fs29.existsSync(prdDir)) {
12126
+ fs29.mkdirSync(prdDir, { recursive: true });
11566
12127
  }
11567
12128
  const resolvedModel = options.model ? CLAUDE_MODEL_IDS[options.model] ?? options.model : void 0;
11568
12129
  const modelLabel = resolvedModel ?? CLAUDE_MODEL_IDS.opus;
@@ -11576,13 +12137,13 @@ function prdCommand(program2) {
11576
12137
  const prdTitle = extractPrdTitle(generated) ?? name;
11577
12138
  const slug = slugify2(prdTitle);
11578
12139
  const filename = options.number ? `${String(getNextPrdNumber2(prdDir)).padStart(2, "0")}-${slug}.md` : `${slug}.md`;
11579
- const filePath = path29.join(prdDir, filename);
11580
- if (fs28.existsSync(filePath)) {
12140
+ const filePath = path30.join(prdDir, filename);
12141
+ if (fs29.existsSync(filePath)) {
11581
12142
  error(`File already exists: ${filePath}`);
11582
12143
  dim("Use a different name or remove the existing file.");
11583
12144
  process.exit(1);
11584
12145
  }
11585
- fs28.writeFileSync(filePath, generated, "utf-8");
12146
+ fs29.writeFileSync(filePath, generated, "utf-8");
11586
12147
  header("PRD Created");
11587
12148
  success(`Created: ${filePath}`);
11588
12149
  const issueUrl = createGithubIssue(prdTitle, filePath, projectDir, generated);
@@ -11595,15 +12156,15 @@ function prdCommand(program2) {
11595
12156
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
11596
12157
  const projectDir = process.cwd();
11597
12158
  const config = loadConfig(projectDir);
11598
- const absolutePrdDir = path29.join(projectDir, config.prdDir);
11599
- const doneDir = path29.join(absolutePrdDir, "done");
12159
+ const absolutePrdDir = path30.join(projectDir, config.prdDir);
12160
+ const doneDir = path30.join(absolutePrdDir, "done");
11600
12161
  const pending = [];
11601
- if (fs28.existsSync(absolutePrdDir)) {
11602
- const files = fs28.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
12162
+ if (fs29.existsSync(absolutePrdDir)) {
12163
+ const files = fs29.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
11603
12164
  for (const file of files) {
11604
- const content = fs28.readFileSync(path29.join(absolutePrdDir, file), "utf-8");
12165
+ const content = fs29.readFileSync(path30.join(absolutePrdDir, file), "utf-8");
11605
12166
  const deps = parseDependencies(content);
11606
- const claimPath = path29.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
12167
+ const claimPath = path30.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
11607
12168
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
11608
12169
  pending.push({
11609
12170
  name: file,
@@ -11614,10 +12175,10 @@ function prdCommand(program2) {
11614
12175
  }
11615
12176
  }
11616
12177
  const done = [];
11617
- if (fs28.existsSync(doneDir)) {
11618
- const files = fs28.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
12178
+ if (fs29.existsSync(doneDir)) {
12179
+ const files = fs29.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
11619
12180
  for (const file of files) {
11620
- const content = fs28.readFileSync(path29.join(doneDir, file), "utf-8");
12181
+ const content = fs29.readFileSync(path30.join(doneDir, file), "utf-8");
11621
12182
  const deps = parseDependencies(content);
11622
12183
  done.push({ name: file, dependencies: deps });
11623
12184
  }
@@ -11654,7 +12215,7 @@ import blessed6 from "blessed";
11654
12215
  // src/commands/dashboard/tab-status.ts
11655
12216
  init_dist();
11656
12217
  import blessed from "blessed";
11657
- import * as fs29 from "fs";
12218
+ import * as fs30 from "fs";
11658
12219
  function sortPrdsByPriority(prds, priority) {
11659
12220
  if (priority.length === 0) return prds;
11660
12221
  const priorityMap = /* @__PURE__ */ new Map();
@@ -11750,7 +12311,7 @@ function renderLogPane(projectDir, logs) {
11750
12311
  let newestMtime = 0;
11751
12312
  for (const log of existingLogs) {
11752
12313
  try {
11753
- const stat = fs29.statSync(log.path);
12314
+ const stat = fs30.statSync(log.path);
11754
12315
  if (stat.mtimeMs > newestMtime) {
11755
12316
  newestMtime = stat.mtimeMs;
11756
12317
  newestLog = log;
@@ -13406,8 +13967,8 @@ function createActionsTab() {
13406
13967
  // src/commands/dashboard/tab-logs.ts
13407
13968
  init_dist();
13408
13969
  import blessed5 from "blessed";
13409
- import * as fs30 from "fs";
13410
- import * as path30 from "path";
13970
+ import * as fs31 from "fs";
13971
+ import * as path31 from "path";
13411
13972
  var LOG_NAMES = ["executor", "reviewer"];
13412
13973
  var LOG_LINES = 200;
13413
13974
  function createLogsTab() {
@@ -13448,7 +14009,7 @@ function createLogsTab() {
13448
14009
  let activeKeyHandlers = [];
13449
14010
  let activeCtx = null;
13450
14011
  function getLogPath(projectDir, logName) {
13451
- return path30.join(projectDir, "logs", `${logName}.log`);
14012
+ return path31.join(projectDir, "logs", `${logName}.log`);
13452
14013
  }
13453
14014
  function updateSelector() {
13454
14015
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -13462,7 +14023,7 @@ function createLogsTab() {
13462
14023
  function loadLog(ctx) {
13463
14024
  const logName = LOG_NAMES[selectedLogIndex];
13464
14025
  const logPath = getLogPath(ctx.projectDir, logName);
13465
- if (!fs30.existsSync(logPath)) {
14026
+ if (!fs31.existsSync(logPath)) {
13466
14027
  logContent.setContent(
13467
14028
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
13468
14029
 
@@ -13472,7 +14033,7 @@ Log will appear here once the ${logName} runs.`
13472
14033
  return;
13473
14034
  }
13474
14035
  try {
13475
- const stat = fs30.statSync(logPath);
14036
+ const stat = fs31.statSync(logPath);
13476
14037
  const sizeKB = (stat.size / 1024).toFixed(1);
13477
14038
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
13478
14039
  } catch {
@@ -13970,12 +14531,12 @@ function doctorCommand(program2) {
13970
14531
 
13971
14532
  // src/commands/serve.ts
13972
14533
  init_dist();
13973
- import * as fs35 from "fs";
14534
+ import * as fs36 from "fs";
13974
14535
 
13975
14536
  // ../server/dist/index.js
13976
14537
  init_dist();
13977
- import * as fs34 from "fs";
13978
- import * as path36 from "path";
14538
+ import * as fs35 from "fs";
14539
+ import * as path37 from "path";
13979
14540
  import { dirname as dirname11 } from "path";
13980
14541
  import { fileURLToPath as fileURLToPath5 } from "url";
13981
14542
  import cors from "cors";
@@ -14060,8 +14621,8 @@ function setupGracefulShutdown(server, beforeClose) {
14060
14621
 
14061
14622
  // ../server/dist/middleware/project-resolver.middleware.js
14062
14623
  init_dist();
14063
- import * as fs31 from "fs";
14064
- import * as path31 from "path";
14624
+ import * as fs32 from "fs";
14625
+ import * as path32 from "path";
14065
14626
  function resolveProject(req, res, next) {
14066
14627
  const projectId = req.params.projectId;
14067
14628
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -14071,7 +14632,7 @@ function resolveProject(req, res, next) {
14071
14632
  res.status(404).json({ error: `Project not found: ${decodedId}` });
14072
14633
  return;
14073
14634
  }
14074
- if (!fs31.existsSync(entry.path) || !fs31.existsSync(path31.join(entry.path, CONFIG_FILE_NAME))) {
14635
+ if (!fs32.existsSync(entry.path) || !fs32.existsSync(path32.join(entry.path, CONFIG_FILE_NAME))) {
14075
14636
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
14076
14637
  return;
14077
14638
  }
@@ -14116,8 +14677,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
14116
14677
 
14117
14678
  // ../server/dist/routes/action.routes.js
14118
14679
  init_dist();
14119
- import * as fs32 from "fs";
14120
- import * as path32 from "path";
14680
+ import * as fs33 from "fs";
14681
+ import * as path33 from "path";
14121
14682
  import { execSync as execSync6, spawn as spawn6 } from "child_process";
14122
14683
  import { Router } from "express";
14123
14684
 
@@ -14155,17 +14716,17 @@ function getBoardProvider(config, projectDir) {
14155
14716
  function cleanOrphanedClaims(dir) {
14156
14717
  let entries;
14157
14718
  try {
14158
- entries = fs32.readdirSync(dir, { withFileTypes: true });
14719
+ entries = fs33.readdirSync(dir, { withFileTypes: true });
14159
14720
  } catch {
14160
14721
  return;
14161
14722
  }
14162
14723
  for (const entry of entries) {
14163
- const fullPath = path32.join(dir, entry.name);
14724
+ const fullPath = path33.join(dir, entry.name);
14164
14725
  if (entry.isDirectory() && entry.name !== "done") {
14165
14726
  cleanOrphanedClaims(fullPath);
14166
14727
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
14167
14728
  try {
14168
- fs32.unlinkSync(fullPath);
14729
+ fs33.unlinkSync(fullPath);
14169
14730
  } catch {
14170
14731
  }
14171
14732
  }
@@ -14320,19 +14881,19 @@ function createActionRouteHandlers(ctx) {
14320
14881
  res.status(400).json({ error: "Invalid PRD name" });
14321
14882
  return;
14322
14883
  }
14323
- const prdDir = path32.join(projectDir, config.prdDir);
14884
+ const prdDir = path33.join(projectDir, config.prdDir);
14324
14885
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
14325
- const pendingPath = path32.join(prdDir, normalized);
14326
- const donePath = path32.join(prdDir, "done", normalized);
14327
- if (fs32.existsSync(pendingPath)) {
14886
+ const pendingPath = path33.join(prdDir, normalized);
14887
+ const donePath = path33.join(prdDir, "done", normalized);
14888
+ if (fs33.existsSync(pendingPath)) {
14328
14889
  res.json({ message: `"${normalized}" is already pending` });
14329
14890
  return;
14330
14891
  }
14331
- if (!fs32.existsSync(donePath)) {
14892
+ if (!fs33.existsSync(donePath)) {
14332
14893
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
14333
14894
  return;
14334
14895
  }
14335
- fs32.renameSync(donePath, pendingPath);
14896
+ fs33.renameSync(donePath, pendingPath);
14336
14897
  res.json({ message: `Moved "${normalized}" back to pending` });
14337
14898
  } catch (error2) {
14338
14899
  res.status(500).json({
@@ -14350,11 +14911,11 @@ function createActionRouteHandlers(ctx) {
14350
14911
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
14351
14912
  return;
14352
14913
  }
14353
- if (fs32.existsSync(lockPath)) {
14354
- fs32.unlinkSync(lockPath);
14914
+ if (fs33.existsSync(lockPath)) {
14915
+ fs33.unlinkSync(lockPath);
14355
14916
  }
14356
- const prdDir = path32.join(projectDir, config.prdDir);
14357
- if (fs32.existsSync(prdDir)) {
14917
+ const prdDir = path33.join(projectDir, config.prdDir);
14918
+ if (fs33.existsSync(prdDir)) {
14358
14919
  cleanOrphanedClaims(prdDir);
14359
14920
  }
14360
14921
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -15116,8 +15677,8 @@ function createProjectConfigRoutes() {
15116
15677
 
15117
15678
  // ../server/dist/routes/doctor.routes.js
15118
15679
  init_dist();
15119
- import * as fs33 from "fs";
15120
- import * as path33 from "path";
15680
+ import * as fs34 from "fs";
15681
+ import * as path34 from "path";
15121
15682
  import { execSync as execSync7 } from "child_process";
15122
15683
  import { Router as Router4 } from "express";
15123
15684
  function runDoctorChecks(projectDir, config) {
@@ -15150,7 +15711,7 @@ function runDoctorChecks(projectDir, config) {
15150
15711
  });
15151
15712
  }
15152
15713
  try {
15153
- const projectName = path33.basename(projectDir);
15714
+ const projectName = path34.basename(projectDir);
15154
15715
  const marker = generateMarker(projectName);
15155
15716
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
15156
15717
  if (crontabEntries.length > 0) {
@@ -15173,8 +15734,8 @@ function runDoctorChecks(projectDir, config) {
15173
15734
  detail: "Failed to check crontab"
15174
15735
  });
15175
15736
  }
15176
- const configPath = path33.join(projectDir, CONFIG_FILE_NAME);
15177
- if (fs33.existsSync(configPath)) {
15737
+ const configPath = path34.join(projectDir, CONFIG_FILE_NAME);
15738
+ if (fs34.existsSync(configPath)) {
15178
15739
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
15179
15740
  } else {
15180
15741
  checks.push({
@@ -15183,56 +15744,374 @@ function runDoctorChecks(projectDir, config) {
15183
15744
  detail: "Config file not found (using defaults)"
15184
15745
  });
15185
15746
  }
15186
- const prdDir = path33.join(projectDir, config.prdDir);
15187
- if (fs33.existsSync(prdDir)) {
15188
- const prds = fs33.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
15189
- checks.push({
15190
- name: "prdDir",
15191
- status: "pass",
15192
- detail: `PRD directory exists (${prds.length} PRDs)`
15193
- });
15194
- } else {
15195
- checks.push({
15196
- name: "prdDir",
15197
- status: "warn",
15198
- detail: `PRD directory not found: ${config.prdDir}`
15199
- });
15747
+ const prdDir = path34.join(projectDir, config.prdDir);
15748
+ if (fs34.existsSync(prdDir)) {
15749
+ const prds = fs34.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
15750
+ checks.push({
15751
+ name: "prdDir",
15752
+ status: "pass",
15753
+ detail: `PRD directory exists (${prds.length} PRDs)`
15754
+ });
15755
+ } else {
15756
+ checks.push({
15757
+ name: "prdDir",
15758
+ status: "warn",
15759
+ detail: `PRD directory not found: ${config.prdDir}`
15760
+ });
15761
+ }
15762
+ return checks;
15763
+ }
15764
+ function createDoctorRoutes(deps) {
15765
+ const { projectDir, getConfig } = deps;
15766
+ const router = Router4();
15767
+ router.get("/", (_req, res) => {
15768
+ try {
15769
+ const checks = runDoctorChecks(projectDir, getConfig());
15770
+ res.json(checks);
15771
+ } catch (error2) {
15772
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
15773
+ }
15774
+ });
15775
+ return router;
15776
+ }
15777
+ function createProjectDoctorRoutes() {
15778
+ const router = Router4({ mergeParams: true });
15779
+ router.get("/doctor", (req, res) => {
15780
+ try {
15781
+ const checks = runDoctorChecks(req.projectDir, req.projectConfig);
15782
+ res.json(checks);
15783
+ } catch (error2) {
15784
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
15785
+ }
15786
+ });
15787
+ return router;
15788
+ }
15789
+
15790
+ // ../server/dist/routes/job.routes.js
15791
+ init_dist();
15792
+ import { spawn as spawn7 } from "child_process";
15793
+ import { createHmac, randomUUID, timingSafeEqual } from "crypto";
15794
+ import { Router as Router5 } from "express";
15795
+ var SUPPORTED_GITHUB_EVENTS = [
15796
+ "workflow_run",
15797
+ "check_suite",
15798
+ "pull_request",
15799
+ "repository_dispatch"
15800
+ ];
15801
+ function isSupportedGithubEvent(value) {
15802
+ return SUPPORTED_GITHUB_EVENTS.some((event) => event === value);
15803
+ }
15804
+ function getRawBody(req) {
15805
+ if (Buffer.isBuffer(req.body)) {
15806
+ return req.body;
15807
+ }
15808
+ if (typeof req.body === "string") {
15809
+ return Buffer.from(req.body, "utf-8");
15810
+ }
15811
+ return Buffer.alloc(0);
15812
+ }
15813
+ function verifyHmacSignature(rawBody, header2, secret) {
15814
+ const match = header2?.match(/^sha256=([a-f0-9]{64})$/i);
15815
+ if (!match) {
15816
+ return false;
15817
+ }
15818
+ const expected = createHmac("sha256", secret).update(rawBody).digest();
15819
+ const actual = Buffer.from(match[1], "hex");
15820
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
15821
+ }
15822
+ function getSignatureHeaders(req) {
15823
+ return [req.get("X-Night-Watch-Signature"), req.get("X-Hub-Signature-256")];
15824
+ }
15825
+ function getGithubWebhookHeaders(req) {
15826
+ return {
15827
+ event: req.get("X-GitHub-Event"),
15828
+ delivery: req.get("X-GitHub-Delivery"),
15829
+ signature: req.get("X-Hub-Signature-256")
15830
+ };
15831
+ }
15832
+ function hasGithubHeaders(headers) {
15833
+ return Boolean(headers.event && headers.signature);
15834
+ }
15835
+ function isObjectRecord(value) {
15836
+ return value !== null && typeof value === "object" && !Array.isArray(value);
15837
+ }
15838
+ function readObject(value) {
15839
+ return isObjectRecord(value) ? value : void 0;
15840
+ }
15841
+ function readString(value) {
15842
+ return typeof value === "string" && value.length > 0 ? value : void 0;
15843
+ }
15844
+ function readNumberString(value) {
15845
+ if (typeof value === "number" && Number.isFinite(value))
15846
+ return String(value);
15847
+ return readString(value);
15848
+ }
15849
+ function readFirstPullRequestNumber(value) {
15850
+ if (!Array.isArray(value))
15851
+ return void 0;
15852
+ for (const item of value) {
15853
+ const pr = readObject(item);
15854
+ const number = readNumberString(pr?.number);
15855
+ if (number)
15856
+ return number;
15857
+ }
15858
+ return void 0;
15859
+ }
15860
+ function isFailureConclusion(value) {
15861
+ const conclusion = readString(value)?.toLowerCase();
15862
+ if (!conclusion)
15863
+ return false;
15864
+ return !["success", "neutral", "skipped"].includes(conclusion);
15865
+ }
15866
+ function parseJsonBody(rawBody) {
15867
+ if (rawBody.length === 0) {
15868
+ return {};
15869
+ }
15870
+ return JSON.parse(rawBody.toString("utf-8"));
15871
+ }
15872
+ function buildGithubContext(headers, payload) {
15873
+ if (!headers.event || !isSupportedGithubEvent(headers.event)) {
15874
+ return void 0;
15875
+ }
15876
+ const context = {
15877
+ event: headers.event,
15878
+ delivery: headers.delivery,
15879
+ action: readString(payload.action),
15880
+ failed: false
15881
+ };
15882
+ switch (headers.event) {
15883
+ case "workflow_run": {
15884
+ const workflowRun = readObject(payload.workflow_run);
15885
+ context.branch = readString(workflowRun?.head_branch);
15886
+ context.prNumber = readFirstPullRequestNumber(workflowRun?.pull_requests);
15887
+ context.failed = isFailureConclusion(workflowRun?.conclusion);
15888
+ break;
15889
+ }
15890
+ case "check_suite": {
15891
+ const checkSuite = readObject(payload.check_suite);
15892
+ context.branch = readString(checkSuite?.head_branch);
15893
+ context.prNumber = readFirstPullRequestNumber(checkSuite?.pull_requests);
15894
+ context.failed = isFailureConclusion(checkSuite?.conclusion);
15895
+ break;
15896
+ }
15897
+ case "pull_request": {
15898
+ const pullRequest = readObject(payload.pull_request);
15899
+ const head = readObject(pullRequest?.head);
15900
+ context.branch = readString(head?.ref);
15901
+ context.prNumber = readNumberString(payload.number) ?? readNumberString(pullRequest?.number);
15902
+ break;
15903
+ }
15904
+ case "repository_dispatch": {
15905
+ const clientPayload = readObject(payload.client_payload);
15906
+ context.action = readString(payload.action) ?? readString(payload.event_type);
15907
+ context.branch = readString(clientPayload?.branch) ?? readString(clientPayload?.ref) ?? readString(payload.ref) ?? readString(payload.branch);
15908
+ context.prNumber = readNumberString(clientPayload?.pr_number) ?? readNumberString(clientPayload?.pull_request_number) ?? readNumberString(payload.pr_number);
15909
+ context.failed = readString(clientPayload?.status)?.toLowerCase() === "failure" || readString(clientPayload?.conclusion)?.toLowerCase() === "failure";
15910
+ break;
15911
+ }
15912
+ }
15913
+ return context;
15914
+ }
15915
+ function globToRegExp(pattern) {
15916
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
15917
+ const wildcarded = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
15918
+ return new RegExp(`^${wildcarded}$`);
15919
+ }
15920
+ function matchesBranchPattern(branch, branchPatterns) {
15921
+ if (!branchPatterns || branchPatterns.length === 0)
15922
+ return true;
15923
+ if (!branch)
15924
+ return false;
15925
+ return branchPatterns.some((pattern) => globToRegExp(pattern).test(branch));
15926
+ }
15927
+ function findGithubWebhookMatch(config, headers, payload) {
15928
+ const context = buildGithubContext(headers, payload);
15929
+ if (!context)
15930
+ return void 0;
15931
+ if (config.webhookTriggers.github.events.length > 0) {
15932
+ const eventAllowed = config.webhookTriggers.github.events.includes(context.event);
15933
+ if (!eventAllowed)
15934
+ return void 0;
15935
+ }
15936
+ const rule = config.webhookTriggers.github.rules.find((candidate) => {
15937
+ if (candidate.event !== context.event)
15938
+ return false;
15939
+ if (candidate.action && candidate.action !== context.action)
15940
+ return false;
15941
+ if (candidate.onlyOnFailure && !context.failed)
15942
+ return false;
15943
+ return matchesBranchPattern(context.branch, candidate.branchPatterns);
15944
+ });
15945
+ if (!rule)
15946
+ return void 0;
15947
+ return { rule, context };
15948
+ }
15949
+ function buildGithubWebhookEnv(context) {
15950
+ const env = {
15951
+ NW_WEBHOOK_SOURCE: "github",
15952
+ NW_WEBHOOK_EVENT: context.event
15953
+ };
15954
+ if (context.delivery) {
15955
+ env.NW_WEBHOOK_DELIVERY = context.delivery;
15956
+ }
15957
+ if (context.prNumber) {
15958
+ env.NW_WEBHOOK_PR_NUMBER = context.prNumber;
15200
15959
  }
15201
- return checks;
15960
+ if (context.branch) {
15961
+ env.NW_WEBHOOK_BRANCH = context.branch;
15962
+ }
15963
+ return env;
15202
15964
  }
15203
- function createDoctorRoutes(deps) {
15204
- const { projectDir, getConfig } = deps;
15205
- const router = Router4();
15206
- router.get("/", (_req, res) => {
15965
+ function getLockPathForJob2(projectDir, jobId) {
15966
+ switch (jobId) {
15967
+ case "executor":
15968
+ return executorLockPath(projectDir);
15969
+ case "reviewer":
15970
+ return reviewerLockPath(projectDir);
15971
+ case "qa":
15972
+ return qaLockPath(projectDir);
15973
+ case "audit":
15974
+ return auditLockPath(projectDir);
15975
+ case "slicer":
15976
+ case "planner":
15977
+ return plannerLockPath(projectDir);
15978
+ case "analytics":
15979
+ return analyticsLockPath(projectDir);
15980
+ case "pr-resolver":
15981
+ return prResolverLockPath(projectDir);
15982
+ case "merger":
15983
+ return mergerLockPath(projectDir);
15984
+ }
15985
+ }
15986
+ function createJobRouteHandlers(ctx) {
15987
+ const router = Router5({ mergeParams: true });
15988
+ const p = ctx.pathPrefix;
15989
+ router.post(`/${p}:id/run`, (req, res) => {
15207
15990
  try {
15208
- const checks = runDoctorChecks(projectDir, getConfig());
15209
- res.json(checks);
15991
+ const config = ctx.getConfig(req);
15992
+ const webhookConfig = config.webhookTriggers;
15993
+ if (!webhookConfig.enabled) {
15994
+ res.status(403).json({ error: "Webhook triggers are disabled" });
15995
+ return;
15996
+ }
15997
+ const secret = process.env[webhookConfig.secretEnv];
15998
+ if (!secret) {
15999
+ res.status(403).json({ error: "Webhook signing secret is not configured" });
16000
+ return;
16001
+ }
16002
+ const rawBody = getRawBody(req);
16003
+ const hasValidSignature = getSignatureHeaders(req).some((header2) => verifyHmacSignature(rawBody, header2, secret));
16004
+ if (!hasValidSignature) {
16005
+ res.status(401).json({ error: "Invalid signature" });
16006
+ return;
16007
+ }
16008
+ const requestedJobId = req.params.id;
16009
+ const requestedJobDef = JOB_REGISTRY.find((job) => job.id === requestedJobId);
16010
+ if (!requestedJobDef) {
16011
+ res.status(404).json({ error: "Unknown job id" });
16012
+ return;
16013
+ }
16014
+ if (!webhookConfig.allowedJobIds.includes(requestedJobDef.id)) {
16015
+ res.status(403).json({ error: "Job is not allowed for webhook dispatch" });
16016
+ return;
16017
+ }
16018
+ const githubHeaders = getGithubWebhookHeaders(req);
16019
+ let githubEnv = {};
16020
+ let matchedGithubJobId;
16021
+ if (hasGithubHeaders(githubHeaders) && webhookConfig.github.enabled) {
16022
+ let payload;
16023
+ try {
16024
+ payload = parseJsonBody(rawBody);
16025
+ } catch {
16026
+ res.status(400).json({ error: "Malformed JSON payload" });
16027
+ return;
16028
+ }
16029
+ const match = findGithubWebhookMatch(config, githubHeaders, readObject(payload) ?? {});
16030
+ if (!match) {
16031
+ const response2 = {
16032
+ accepted: false,
16033
+ ignored: true,
16034
+ reason: "No matching GitHub webhook rule"
16035
+ };
16036
+ res.status(202).json(response2);
16037
+ return;
16038
+ }
16039
+ matchedGithubJobId = match.rule.jobId;
16040
+ githubEnv = buildGithubWebhookEnv(match.context);
16041
+ }
16042
+ const dispatchJobId = matchedGithubJobId ?? requestedJobDef.id;
16043
+ const jobDef = JOB_REGISTRY.find((job) => job.id === dispatchJobId);
16044
+ if (!jobDef) {
16045
+ res.status(404).json({ error: "Unknown job id" });
16046
+ return;
16047
+ }
16048
+ if (!webhookConfig.allowedJobIds.includes(jobDef.id)) {
16049
+ res.status(403).json({ error: "Job is not allowed for webhook dispatch" });
16050
+ return;
16051
+ }
16052
+ const projectDir = ctx.getProjectDir(req);
16053
+ const lock = checkLockFile(getLockPathForJob2(projectDir, jobDef.id));
16054
+ if (lock.running) {
16055
+ res.status(409).json({
16056
+ error: `${jobDef.name} is already running (PID ${lock.pid})`,
16057
+ pid: lock.pid
16058
+ });
16059
+ return;
16060
+ }
16061
+ const dispatchId = randomUUID();
16062
+ const child = spawn7("night-watch", [jobDef.cliCommand], {
16063
+ detached: true,
16064
+ stdio: "ignore",
16065
+ cwd: projectDir,
16066
+ env: {
16067
+ ...process.env,
16068
+ NW_WEBHOOK_DISPATCH_ID: dispatchId,
16069
+ NW_WEBHOOK_JOB_ID: jobDef.id,
16070
+ ...githubEnv
16071
+ }
16072
+ });
16073
+ child.unref();
16074
+ if (child.pid === void 0) {
16075
+ res.status(500).json({ error: "Failed to spawn process: no PID assigned" });
16076
+ return;
16077
+ }
16078
+ const response = {
16079
+ accepted: true,
16080
+ jobId: jobDef.id,
16081
+ pid: child.pid,
16082
+ dispatchId
16083
+ };
16084
+ res.status(202).json(response);
15210
16085
  } catch (error2) {
15211
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
16086
+ res.status(500).json({
16087
+ error: error2 instanceof Error ? error2.message : String(error2)
16088
+ });
15212
16089
  }
15213
16090
  });
15214
16091
  return router;
15215
16092
  }
15216
- function createProjectDoctorRoutes() {
15217
- const router = Router4({ mergeParams: true });
15218
- router.get("/doctor", (req, res) => {
15219
- try {
15220
- const checks = runDoctorChecks(req.projectDir, req.projectConfig);
15221
- res.json(checks);
15222
- } catch (error2) {
15223
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
15224
- }
16093
+ function createJobRoutes(deps) {
16094
+ return createJobRouteHandlers({
16095
+ getConfig: () => deps.getConfig(),
16096
+ getProjectDir: () => deps.projectDir,
16097
+ pathPrefix: ""
16098
+ });
16099
+ }
16100
+ function createProjectJobRoutes() {
16101
+ return createJobRouteHandlers({
16102
+ getConfig: (req) => req.projectConfig,
16103
+ getProjectDir: (req) => req.projectDir,
16104
+ pathPrefix: "jobs/"
15225
16105
  });
15226
- return router;
15227
16106
  }
15228
16107
 
15229
16108
  // ../server/dist/routes/log.routes.js
15230
16109
  init_dist();
15231
- import * as path34 from "path";
15232
- import { Router as Router5 } from "express";
16110
+ import * as path35 from "path";
16111
+ import { Router as Router6 } from "express";
15233
16112
  function createLogRoutes(deps) {
15234
16113
  const { projectDir } = deps;
15235
- const router = Router5();
16114
+ const router = Router6();
15236
16115
  router.get("/:name", (req, res) => {
15237
16116
  try {
15238
16117
  const { name } = req.params;
@@ -15247,7 +16126,7 @@ function createLogRoutes(deps) {
15247
16126
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
15248
16127
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
15249
16128
  const fileName = LOG_FILE_NAMES[name] || name;
15250
- const logPath = path34.join(projectDir, LOG_DIR, `${fileName}.log`);
16129
+ const logPath = path35.join(projectDir, LOG_DIR, `${fileName}.log`);
15251
16130
  const logLines = getLastLogLines(logPath, linesToRead);
15252
16131
  res.json({ name, lines: logLines });
15253
16132
  } catch (error2) {
@@ -15257,7 +16136,7 @@ function createLogRoutes(deps) {
15257
16136
  return router;
15258
16137
  }
15259
16138
  function createProjectLogRoutes() {
15260
- const router = Router5({ mergeParams: true });
16139
+ const router = Router6({ mergeParams: true });
15261
16140
  router.get("/logs/:name", (req, res) => {
15262
16141
  try {
15263
16142
  const projectDir = req.projectDir;
@@ -15273,7 +16152,7 @@ function createProjectLogRoutes() {
15273
16152
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
15274
16153
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
15275
16154
  const fileName = LOG_FILE_NAMES[name] || name;
15276
- const logPath = path34.join(projectDir, LOG_DIR, `${fileName}.log`);
16155
+ const logPath = path35.join(projectDir, LOG_DIR, `${fileName}.log`);
15277
16156
  const logLines = getLastLogLines(logPath, linesToRead);
15278
16157
  res.json({ name, lines: logLines });
15279
16158
  } catch (error2) {
@@ -15284,9 +16163,9 @@ function createProjectLogRoutes() {
15284
16163
  }
15285
16164
 
15286
16165
  // ../server/dist/routes/prd.routes.js
15287
- import { Router as Router6 } from "express";
16166
+ import { Router as Router7 } from "express";
15288
16167
  function createPrdRoutes(_deps) {
15289
- const router = Router6();
16168
+ const router = Router7();
15290
16169
  router.get("/", (_req, res) => {
15291
16170
  res.status(410).json({ error: "PRDs endpoint deprecated - use GitHub Board instead" });
15292
16171
  });
@@ -15296,7 +16175,7 @@ function createPrdRoutes(_deps) {
15296
16175
  return router;
15297
16176
  }
15298
16177
  function createProjectPrdRoutes() {
15299
- const router = Router6({ mergeParams: true });
16178
+ const router = Router7({ mergeParams: true });
15300
16179
  router.get("/prds", (_req, res) => {
15301
16180
  res.status(410).json({ error: "PRDs endpoint deprecated - use GitHub Board instead" });
15302
16181
  });
@@ -15308,17 +16187,17 @@ function createProjectPrdRoutes() {
15308
16187
 
15309
16188
  // ../server/dist/routes/roadmap.routes.js
15310
16189
  init_dist();
15311
- import * as path35 from "path";
15312
- import { Router as Router7 } from "express";
16190
+ import * as path36 from "path";
16191
+ import { Router as Router8 } from "express";
15313
16192
  function createRoadmapRouteHandlers(ctx) {
15314
- const router = Router7({ mergeParams: true });
16193
+ const router = Router8({ mergeParams: true });
15315
16194
  const p = ctx.pathPrefix;
15316
16195
  router.get(`/${p}`, (req, res) => {
15317
16196
  try {
15318
16197
  const config = ctx.getConfig(req);
15319
16198
  const projectDir = ctx.getProjectDir(req);
15320
16199
  const status = getRoadmapStatus(projectDir, config);
15321
- const prdDir = path35.join(projectDir, config.prdDir);
16200
+ const prdDir = path36.join(projectDir, config.prdDir);
15322
16201
  const state = loadRoadmapState(prdDir);
15323
16202
  res.json({
15324
16203
  ...status,
@@ -15392,11 +16271,11 @@ function createProjectRoadmapRoutes() {
15392
16271
 
15393
16272
  // ../server/dist/routes/status.routes.js
15394
16273
  init_dist();
15395
- import { Router as Router8 } from "express";
16274
+ import { Router as Router9 } from "express";
15396
16275
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
15397
16276
  function createStatusRoutes(deps) {
15398
16277
  const { projectDir, getConfig, sseClients } = deps;
15399
- const router = Router8();
16278
+ const router = Router9();
15400
16279
  router.get("/events", (req, res) => {
15401
16280
  res.setHeader("Content-Type", "text/event-stream");
15402
16281
  res.setHeader("Cache-Control", "no-cache");
@@ -15525,7 +16404,7 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
15525
16404
  }
15526
16405
  function createScheduleInfoRoutes(deps) {
15527
16406
  const { projectDir, getConfig } = deps;
15528
- const router = Router8();
16407
+ const router = Router9();
15529
16408
  router.get("/", async (_req, res) => {
15530
16409
  try {
15531
16410
  const config = getConfig();
@@ -15539,7 +16418,7 @@ function createScheduleInfoRoutes(deps) {
15539
16418
  }
15540
16419
  function createProjectSseRoutes(deps) {
15541
16420
  const { projectSseClients, projectSseWatchers } = deps;
15542
- const router = Router8({ mergeParams: true });
16421
+ const router = Router9({ mergeParams: true });
15543
16422
  router.get("/status/events", (req, res) => {
15544
16423
  const projectDir = req.projectDir;
15545
16424
  if (!projectSseClients.has(projectDir)) {
@@ -15596,9 +16475,9 @@ data: ${JSON.stringify(snapshot)}
15596
16475
 
15597
16476
  // ../server/dist/routes/queue.routes.js
15598
16477
  init_dist();
15599
- import { Router as Router9 } from "express";
16478
+ import { Router as Router10 } from "express";
15600
16479
  function createGlobalQueueRoutes() {
15601
- const router = Router9();
16480
+ const router = Router10();
15602
16481
  router.get("/status", async (_req, res) => {
15603
16482
  try {
15604
16483
  const status = getQueueStatus();
@@ -15633,7 +16512,7 @@ function createGlobalQueueRoutes() {
15633
16512
  }
15634
16513
  function createQueueRoutes(deps) {
15635
16514
  const { getConfig } = deps;
15636
- const router = Router9();
16515
+ const router = Router10();
15637
16516
  router.get("/status", async (_req, res) => {
15638
16517
  try {
15639
16518
  const config = getConfig();
@@ -15671,15 +16550,20 @@ function createQueueRoutes(deps) {
15671
16550
  // ../server/dist/index.js
15672
16551
  var __filename4 = fileURLToPath5(import.meta.url);
15673
16552
  var __dirname4 = dirname11(__filename4);
16553
+ var JOB_RAW_BODY_LIMIT = "1mb";
16554
+ function setupJobRawBodyParsing(app) {
16555
+ app.use("/api/jobs", express.raw({ type: "*/*", limit: JOB_RAW_BODY_LIMIT }));
16556
+ app.use("/api/projects/:projectId/jobs", express.raw({ type: "*/*", limit: JOB_RAW_BODY_LIMIT }));
16557
+ }
15674
16558
  function resolveWebDistPath() {
15675
- const bundled = path36.join(__dirname4, "web");
15676
- if (fs34.existsSync(path36.join(bundled, "index.html")))
16559
+ const bundled = path37.join(__dirname4, "web");
16560
+ if (fs35.existsSync(path37.join(bundled, "index.html")))
15677
16561
  return bundled;
15678
16562
  let d = __dirname4;
15679
16563
  for (let i = 0; i < 8; i++) {
15680
- if (fs34.existsSync(path36.join(d, "turbo.json"))) {
15681
- const dev = path36.join(d, "web/dist");
15682
- if (fs34.existsSync(path36.join(dev, "index.html")))
16564
+ if (fs35.existsSync(path37.join(d, "turbo.json"))) {
16565
+ const dev = path37.join(d, "web/dist");
16566
+ if (fs35.existsSync(path37.join(dev, "index.html")))
15683
16567
  return dev;
15684
16568
  break;
15685
16569
  }
@@ -15689,7 +16573,7 @@ function resolveWebDistPath() {
15689
16573
  }
15690
16574
  function setupStaticFiles(app) {
15691
16575
  const webDistPath = resolveWebDistPath();
15692
- if (fs34.existsSync(webDistPath)) {
16576
+ if (fs35.existsSync(webDistPath)) {
15693
16577
  app.use(express.static(webDistPath));
15694
16578
  }
15695
16579
  app.use((req, res, next) => {
@@ -15697,8 +16581,8 @@ function setupStaticFiles(app) {
15697
16581
  next();
15698
16582
  return;
15699
16583
  }
15700
- const indexPath = path36.resolve(webDistPath, "index.html");
15701
- if (fs34.existsSync(indexPath)) {
16584
+ const indexPath = path37.resolve(webDistPath, "index.html");
16585
+ if (fs35.existsSync(indexPath)) {
15702
16586
  res.sendFile(indexPath, (err) => {
15703
16587
  if (err)
15704
16588
  next();
@@ -15716,6 +16600,7 @@ function setupStaticFiles(app) {
15716
16600
  function createApp(projectDir) {
15717
16601
  const app = express();
15718
16602
  app.use(cors());
16603
+ setupJobRawBodyParsing(app);
15719
16604
  app.use(express.json());
15720
16605
  let config = loadConfig(projectDir);
15721
16606
  const reloadConfig = () => {
@@ -15732,6 +16617,7 @@ function createApp(projectDir) {
15732
16617
  app.use("/api/config", createConfigRoutes({ projectDir, getConfig: () => config, reloadConfig }));
15733
16618
  app.use("/api/board", createBoardRoutes({ projectDir, getConfig: () => config }));
15734
16619
  app.use("/api/actions", createActionRoutes({ projectDir, getConfig: () => config, sseClients }));
16620
+ app.use("/api/jobs", createJobRoutes({ projectDir, getConfig: () => config }));
15735
16621
  app.use("/api/roadmap", createRoadmapRoutes({ projectDir, getConfig: () => config, reloadConfig }));
15736
16622
  app.use("/api/logs", createLogRoutes({ projectDir }));
15737
16623
  app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
@@ -15781,6 +16667,7 @@ function createProjectRouter() {
15781
16667
  router.use(createProjectLogRoutes());
15782
16668
  router.use(createProjectBoardRoutes());
15783
16669
  router.use(createProjectActionRoutes({ projectSseClients }));
16670
+ router.use(createProjectJobRoutes());
15784
16671
  router.use(createProjectRoadmapRoutes());
15785
16672
  router.get("/prs", async (req, res) => {
15786
16673
  try {
@@ -15794,6 +16681,7 @@ function createProjectRouter() {
15794
16681
  function createGlobalApp() {
15795
16682
  const app = express();
15796
16683
  app.use(cors());
16684
+ setupJobRawBodyParsing(app);
15797
16685
  app.use(express.json());
15798
16686
  app.get("/api/mode", (_req, res) => {
15799
16687
  res.json({ globalMode: true });
@@ -15831,7 +16719,7 @@ function createGlobalApp() {
15831
16719
  return app;
15832
16720
  }
15833
16721
  function bootContainer() {
15834
- initContainer(path36.dirname(getDbPath()));
16722
+ initContainer(path37.dirname(getDbPath()));
15835
16723
  }
15836
16724
  function startServer(projectDir, port) {
15837
16725
  bootContainer();
@@ -15884,8 +16772,8 @@ function isProcessRunning2(pid) {
15884
16772
  }
15885
16773
  function readPid(lockPath) {
15886
16774
  try {
15887
- if (!fs35.existsSync(lockPath)) return null;
15888
- const raw = fs35.readFileSync(lockPath, "utf-8").trim();
16775
+ if (!fs36.existsSync(lockPath)) return null;
16776
+ const raw = fs36.readFileSync(lockPath, "utf-8").trim();
15889
16777
  const pid = parseInt(raw, 10);
15890
16778
  return Number.isFinite(pid) ? pid : null;
15891
16779
  } catch {
@@ -15897,10 +16785,10 @@ function acquireServeLock(mode, port) {
15897
16785
  let stalePidCleaned;
15898
16786
  for (let attempt = 0; attempt < 2; attempt++) {
15899
16787
  try {
15900
- const fd = fs35.openSync(lockPath, "wx");
15901
- fs35.writeFileSync(fd, `${process.pid}
16788
+ const fd = fs36.openSync(lockPath, "wx");
16789
+ fs36.writeFileSync(fd, `${process.pid}
15902
16790
  `);
15903
- fs35.closeSync(fd);
16791
+ fs36.closeSync(fd);
15904
16792
  return { acquired: true, lockPath, stalePidCleaned };
15905
16793
  } catch (error2) {
15906
16794
  const err = error2;
@@ -15921,7 +16809,7 @@ function acquireServeLock(mode, port) {
15921
16809
  };
15922
16810
  }
15923
16811
  try {
15924
- fs35.unlinkSync(lockPath);
16812
+ fs36.unlinkSync(lockPath);
15925
16813
  if (existingPid) {
15926
16814
  stalePidCleaned = existingPid;
15927
16815
  }
@@ -15944,10 +16832,10 @@ function acquireServeLock(mode, port) {
15944
16832
  }
15945
16833
  function releaseServeLock(lockPath) {
15946
16834
  try {
15947
- if (!fs35.existsSync(lockPath)) return;
16835
+ if (!fs36.existsSync(lockPath)) return;
15948
16836
  const lockPid = readPid(lockPath);
15949
16837
  if (lockPid !== null && lockPid !== process.pid) return;
15950
- fs35.unlinkSync(lockPath);
16838
+ fs36.unlinkSync(lockPath);
15951
16839
  } catch {
15952
16840
  }
15953
16841
  }
@@ -16043,14 +16931,14 @@ function historyCommand(program2) {
16043
16931
  // src/commands/update.ts
16044
16932
  init_dist();
16045
16933
  import { spawnSync as spawnSync2 } from "child_process";
16046
- import * as fs36 from "fs";
16047
- import * as path37 from "path";
16934
+ import * as fs37 from "fs";
16935
+ import * as path38 from "path";
16048
16936
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
16049
16937
  function parseProjectDirs(projects, cwd) {
16050
16938
  if (!projects || projects.trim().length === 0) {
16051
16939
  return [cwd];
16052
16940
  }
16053
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path37.resolve(cwd, entry));
16941
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path38.resolve(cwd, entry));
16054
16942
  return Array.from(new Set(dirs));
16055
16943
  }
16056
16944
  function shouldInstallGlobal(options) {
@@ -16092,7 +16980,7 @@ function updateCommand(program2) {
16092
16980
  }
16093
16981
  const nightWatchBin = resolveNightWatchBin();
16094
16982
  for (const projectDir of projectDirs) {
16095
- if (!fs36.existsSync(projectDir) || !fs36.statSync(projectDir).isDirectory()) {
16983
+ if (!fs37.existsSync(projectDir) || !fs37.statSync(projectDir).isDirectory()) {
16096
16984
  warn(`Skipping invalid project directory: ${projectDir}`);
16097
16985
  continue;
16098
16986
  }
@@ -16136,8 +17024,8 @@ function prdStateCommand(program2) {
16136
17024
 
16137
17025
  // src/commands/retry.ts
16138
17026
  init_dist();
16139
- import * as fs37 from "fs";
16140
- import * as path38 from "path";
17027
+ import * as fs38 from "fs";
17028
+ import * as path39 from "path";
16141
17029
  function normalizePrdName(name) {
16142
17030
  if (!name.endsWith(".md")) {
16143
17031
  return `${name}.md`;
@@ -16145,26 +17033,26 @@ function normalizePrdName(name) {
16145
17033
  return name;
16146
17034
  }
16147
17035
  function getDonePrds(doneDir) {
16148
- if (!fs37.existsSync(doneDir)) {
17036
+ if (!fs38.existsSync(doneDir)) {
16149
17037
  return [];
16150
17038
  }
16151
- return fs37.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
17039
+ return fs38.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
16152
17040
  }
16153
17041
  function retryCommand(program2) {
16154
17042
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
16155
17043
  const projectDir = process.cwd();
16156
17044
  const config = loadConfig(projectDir);
16157
- const prdDir = path38.join(projectDir, config.prdDir);
16158
- const doneDir = path38.join(prdDir, "done");
17045
+ const prdDir = path39.join(projectDir, config.prdDir);
17046
+ const doneDir = path39.join(prdDir, "done");
16159
17047
  const normalizedPrdName = normalizePrdName(prdName);
16160
- const pendingPath = path38.join(prdDir, normalizedPrdName);
16161
- if (fs37.existsSync(pendingPath)) {
17048
+ const pendingPath = path39.join(prdDir, normalizedPrdName);
17049
+ if (fs38.existsSync(pendingPath)) {
16162
17050
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
16163
17051
  return;
16164
17052
  }
16165
- const donePath = path38.join(doneDir, normalizedPrdName);
16166
- if (fs37.existsSync(donePath)) {
16167
- fs37.renameSync(donePath, pendingPath);
17053
+ const donePath = path39.join(doneDir, normalizedPrdName);
17054
+ if (fs38.existsSync(donePath)) {
17055
+ fs38.renameSync(donePath, pendingPath);
16168
17056
  success(`Moved "${normalizedPrdName}" back to pending.`);
16169
17057
  dim(`From: ${donePath}`);
16170
17058
  dim(`To: ${pendingPath}`);
@@ -16416,7 +17304,7 @@ function prdsCommand(program2) {
16416
17304
 
16417
17305
  // src/commands/cancel.ts
16418
17306
  init_dist();
16419
- import * as fs38 from "fs";
17307
+ import * as fs39 from "fs";
16420
17308
  import * as readline2 from "readline";
16421
17309
  function getLockFilePaths2(projectDir) {
16422
17310
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -16463,7 +17351,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
16463
17351
  const pid = lockStatus.pid;
16464
17352
  if (!lockStatus.running) {
16465
17353
  try {
16466
- fs38.unlinkSync(lockPath);
17354
+ fs39.unlinkSync(lockPath);
16467
17355
  return {
16468
17356
  success: true,
16469
17357
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -16501,7 +17389,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
16501
17389
  await sleep2(3e3);
16502
17390
  if (!isProcessRunning3(pid)) {
16503
17391
  try {
16504
- fs38.unlinkSync(lockPath);
17392
+ fs39.unlinkSync(lockPath);
16505
17393
  } catch {
16506
17394
  }
16507
17395
  return {
@@ -16536,7 +17424,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
16536
17424
  await sleep2(500);
16537
17425
  if (!isProcessRunning3(pid)) {
16538
17426
  try {
16539
- fs38.unlinkSync(lockPath);
17427
+ fs39.unlinkSync(lockPath);
16540
17428
  } catch {
16541
17429
  }
16542
17430
  return {
@@ -16597,31 +17485,31 @@ function cancelCommand(program2) {
16597
17485
 
16598
17486
  // src/commands/slice.ts
16599
17487
  init_dist();
16600
- import * as fs39 from "fs";
16601
- import * as path39 from "path";
17488
+ import * as fs40 from "fs";
17489
+ import * as path40 from "path";
16602
17490
  function plannerLockPath2(projectDir) {
16603
17491
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
16604
17492
  }
16605
17493
  function acquirePlannerLock(projectDir) {
16606
17494
  const lockFile = plannerLockPath2(projectDir);
16607
- if (fs39.existsSync(lockFile)) {
16608
- const pidRaw = fs39.readFileSync(lockFile, "utf-8").trim();
17495
+ if (fs40.existsSync(lockFile)) {
17496
+ const pidRaw = fs40.readFileSync(lockFile, "utf-8").trim();
16609
17497
  const pid = parseInt(pidRaw, 10);
16610
17498
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
16611
17499
  return { acquired: false, lockFile, pid };
16612
17500
  }
16613
17501
  try {
16614
- fs39.unlinkSync(lockFile);
17502
+ fs40.unlinkSync(lockFile);
16615
17503
  } catch {
16616
17504
  }
16617
17505
  }
16618
- fs39.writeFileSync(lockFile, String(process.pid));
17506
+ fs40.writeFileSync(lockFile, String(process.pid));
16619
17507
  return { acquired: true, lockFile };
16620
17508
  }
16621
17509
  function releasePlannerLock(lockFile) {
16622
17510
  try {
16623
- if (fs39.existsSync(lockFile)) {
16624
- fs39.unlinkSync(lockFile);
17511
+ if (fs40.existsSync(lockFile)) {
17512
+ fs40.unlinkSync(lockFile);
16625
17513
  }
16626
17514
  } catch {
16627
17515
  }
@@ -16630,12 +17518,12 @@ function resolvePlannerIssueColumn(config) {
16630
17518
  return config.roadmapScanner.issueColumn === "Draft" ? "Draft" : "Ready";
16631
17519
  }
16632
17520
  function buildPlannerIssueBody(projectDir, config, result) {
16633
- const relativePrdPath = path39.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
16634
- const absolutePrdPath = path39.join(projectDir, config.prdDir, result.file ?? "");
17521
+ const relativePrdPath = path40.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
17522
+ const absolutePrdPath = path40.join(projectDir, config.prdDir, result.file ?? "");
16635
17523
  const sourceItem = result.item;
16636
17524
  let prdContent;
16637
17525
  try {
16638
- prdContent = fs39.readFileSync(absolutePrdPath, "utf-8");
17526
+ prdContent = fs40.readFileSync(absolutePrdPath, "utf-8");
16639
17527
  } catch {
16640
17528
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
16641
17529
  }
@@ -16842,7 +17730,7 @@ function sliceCommand(program2) {
16842
17730
  if (!options.dryRun && result.sliced) {
16843
17731
  await sendNotifications(config, {
16844
17732
  event: "run_succeeded",
16845
- projectName: path39.basename(projectDir),
17733
+ projectName: path40.basename(projectDir),
16846
17734
  exitCode,
16847
17735
  provider: config.provider,
16848
17736
  prTitle: result.item?.title
@@ -16850,7 +17738,7 @@ function sliceCommand(program2) {
16850
17738
  } else if (!options.dryRun && !nothingPending) {
16851
17739
  await sendNotifications(config, {
16852
17740
  event: "run_failed",
16853
- projectName: path39.basename(projectDir),
17741
+ projectName: path40.basename(projectDir),
16854
17742
  exitCode,
16855
17743
  provider: config.provider
16856
17744
  });
@@ -16867,20 +17755,20 @@ function sliceCommand(program2) {
16867
17755
  // src/commands/state.ts
16868
17756
  init_dist();
16869
17757
  import * as os9 from "os";
16870
- import * as path40 from "path";
17758
+ import * as path41 from "path";
16871
17759
  import chalk5 from "chalk";
16872
17760
  import { Command } from "commander";
16873
17761
  function createStateCommand() {
16874
17762
  const state = new Command("state");
16875
17763
  state.description("Manage Night Watch state");
16876
17764
  state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
16877
- const nightWatchHome = process.env.NIGHT_WATCH_HOME || path40.join(os9.homedir(), GLOBAL_CONFIG_DIR);
17765
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path41.join(os9.homedir(), GLOBAL_CONFIG_DIR);
16878
17766
  if (opts.dryRun) {
16879
17767
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
16880
17768
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
16881
- console.log(` ${path40.join(nightWatchHome, "projects.json")}`);
16882
- console.log(` ${path40.join(nightWatchHome, "history.json")}`);
16883
- console.log(` ${path40.join(nightWatchHome, "prd-states.json")}`);
17769
+ console.log(` ${path41.join(nightWatchHome, "projects.json")}`);
17770
+ console.log(` ${path41.join(nightWatchHome, "history.json")}`);
17771
+ console.log(` ${path41.join(nightWatchHome, "prd-states.json")}`);
16884
17772
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
16885
17773
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
16886
17774
  return;
@@ -16918,8 +17806,8 @@ function createStateCommand() {
16918
17806
  init_dist();
16919
17807
  init_dist();
16920
17808
  import { execFileSync as execFileSync6 } from "child_process";
16921
- import * as fs40 from "fs";
16922
- import * as path41 from "path";
17809
+ import * as fs41 from "fs";
17810
+ import * as path42 from "path";
16923
17811
  import * as readline3 from "readline";
16924
17812
  import chalk6 from "chalk";
16925
17813
  async function run(fn) {
@@ -16941,7 +17829,7 @@ function getProvider(config, cwd) {
16941
17829
  return createBoardProvider(bp, cwd);
16942
17830
  }
16943
17831
  function defaultBoardTitle(cwd) {
16944
- return `${path41.basename(cwd)} Night Watch`;
17832
+ return `${path42.basename(cwd)} Night Watch`;
16945
17833
  }
16946
17834
  async function ensureBoardConfigured(config, cwd, provider, options) {
16947
17835
  if (config.boardProvider?.projectNumber) {
@@ -17140,11 +18028,11 @@ function boardCommand(program2) {
17140
18028
  let body = options.body ?? "";
17141
18029
  if (options.bodyFile) {
17142
18030
  const filePath = options.bodyFile;
17143
- if (!fs40.existsSync(filePath)) {
18031
+ if (!fs41.existsSync(filePath)) {
17144
18032
  console.error(`File not found: ${filePath}`);
17145
18033
  process.exit(1);
17146
18034
  }
17147
- body = fs40.readFileSync(filePath, "utf-8");
18035
+ body = fs41.readFileSync(filePath, "utf-8");
17148
18036
  }
17149
18037
  const labels = [];
17150
18038
  if (options.label) {
@@ -17385,12 +18273,12 @@ function boardCommand(program2) {
17385
18273
  const config = loadConfig(cwd);
17386
18274
  const provider = getProvider(config, cwd);
17387
18275
  await ensureBoardConfigured(config, cwd, provider);
17388
- const roadmapPath = options.roadmap ?? path41.join(cwd, "ROADMAP.md");
17389
- if (!fs40.existsSync(roadmapPath)) {
18276
+ const roadmapPath = options.roadmap ?? path42.join(cwd, "ROADMAP.md");
18277
+ if (!fs41.existsSync(roadmapPath)) {
17390
18278
  console.error(`Roadmap file not found: ${roadmapPath}`);
17391
18279
  process.exit(1);
17392
18280
  }
17393
- const roadmapContent = fs40.readFileSync(roadmapPath, "utf-8");
18281
+ const roadmapContent = fs41.readFileSync(roadmapPath, "utf-8");
17394
18282
  const items = parseRoadmap(roadmapContent);
17395
18283
  const uncheckedItems = getUncheckedItems(items);
17396
18284
  if (uncheckedItems.length === 0) {
@@ -17514,12 +18402,12 @@ function boardCommand(program2) {
17514
18402
  // src/commands/queue.ts
17515
18403
  init_dist();
17516
18404
  init_dist();
17517
- import * as path42 from "path";
17518
- import { spawn as spawn7 } from "child_process";
18405
+ import * as path43 from "path";
18406
+ import { spawn as spawn8 } from "child_process";
17519
18407
  import chalk7 from "chalk";
17520
18408
  import { Command as Command2 } from "commander";
17521
18409
  var logger6 = createLogger("queue");
17522
- var VALID_JOB_TYPES2 = [
18410
+ var VALID_JOB_TYPES3 = [
17523
18411
  "executor",
17524
18412
  "reviewer",
17525
18413
  "qa",
@@ -17552,6 +18440,13 @@ function printQueueEntry(entry, indent = "") {
17552
18440
  console.log(`${indent} Dispatched: ${formatTimestamp(entry.dispatchedAt)}`);
17553
18441
  }
17554
18442
  }
18443
+ function isJobPaused(projectDir, jobType) {
18444
+ try {
18445
+ return loadConfig(projectDir).pausedJobs?.[jobType] === true;
18446
+ } catch {
18447
+ return false;
18448
+ }
18449
+ }
17555
18450
  function createQueueCommand() {
17556
18451
  const queue = new Command2("queue");
17557
18452
  queue.description("Manage the global job queue");
@@ -17620,18 +18515,18 @@ function createQueueCommand() {
17620
18515
  }
17621
18516
  });
17622
18517
  queue.command("clear").description("Clear pending jobs from the queue").option("--type <type>", "Only clear jobs of this type").option("--all", "Clear all entries including running (dangerous)").action((opts) => {
17623
- if (opts.type && !VALID_JOB_TYPES2.includes(opts.type)) {
18518
+ if (opts.type && !VALID_JOB_TYPES3.includes(opts.type)) {
17624
18519
  console.error(chalk7.red(`Invalid job type: ${opts.type}`));
17625
- console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES2.join(", ")}`));
18520
+ console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES3.join(", ")}`));
17626
18521
  process.exit(1);
17627
18522
  }
17628
18523
  const count = clearQueue(opts.type);
17629
18524
  console.log(chalk7.green(`Cleared ${count} pending job(s) from the queue.`));
17630
18525
  });
17631
18526
  queue.command("enqueue <job-type> <project-dir>").description("Manually enqueue a job").option("--env <json>", "JSON object of environment variables to store", "{}").option("--provider-key <key>", "Provider bucket key (e.g. claude-native, codex)").action((jobType, projectDir, opts) => {
17632
- if (!VALID_JOB_TYPES2.includes(jobType)) {
18527
+ if (!VALID_JOB_TYPES3.includes(jobType)) {
17633
18528
  console.error(chalk7.red(`Invalid job type: ${jobType}`));
17634
- console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES2.join(", ")}`));
18529
+ console.error(chalk7.dim(`Valid types: ${VALID_JOB_TYPES3.join(", ")}`));
17635
18530
  process.exit(1);
17636
18531
  }
17637
18532
  let envVars = {};
@@ -17643,8 +18538,12 @@ function createQueueCommand() {
17643
18538
  process.exit(1);
17644
18539
  }
17645
18540
  }
17646
- const projectName = path42.basename(projectDir);
18541
+ const projectName = path43.basename(projectDir);
17647
18542
  const queueConfig = loadConfig(projectDir).queue;
18543
+ if (isJobPaused(projectDir, jobType)) {
18544
+ logger6.info(`Skipping enqueue for paused job: ${jobType}`);
18545
+ return;
18546
+ }
17648
18547
  const id = enqueueJob(
17649
18548
  projectDir,
17650
18549
  projectName,
@@ -17682,6 +18581,11 @@ function createQueueCommand() {
17682
18581
  logger6.info("No pending jobs to dispatch");
17683
18582
  return;
17684
18583
  }
18584
+ if (isJobPaused(entry.projectPath, entry.jobType)) {
18585
+ logger6.info(`Skipping paused queued job: ${entry.jobType} for ${entry.projectName}`);
18586
+ removeJob(entry.id);
18587
+ return;
18588
+ }
17685
18589
  logger6.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
17686
18590
  const scriptName = getScriptNameForJobType(entry.jobType);
17687
18591
  if (!scriptName) {
@@ -17706,7 +18610,7 @@ function createQueueCommand() {
17706
18610
  const scriptPath = getScriptPath(scriptName);
17707
18611
  logger6.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
17708
18612
  try {
17709
- const child = spawn7("bash", [scriptPath, entry.projectPath], {
18613
+ const child = spawn8("bash", [scriptPath, entry.projectPath], {
17710
18614
  detached: true,
17711
18615
  stdio: "ignore",
17712
18616
  env,
@@ -17726,13 +18630,16 @@ function createQueueCommand() {
17726
18630
  queue.command("claim <job-type> <project-dir>").description(
17727
18631
  "Atomically claim a concurrency slot and insert a running entry (used by cron scripts)"
17728
18632
  ).option("--provider-key <key>", "Provider bucket key (e.g. claude-native, codex)").option("--pid <pid>", "PID of the calling process (stored for stale-job detection)").action((jobType, projectDir, opts) => {
17729
- if (!VALID_JOB_TYPES2.includes(jobType)) {
18633
+ if (!VALID_JOB_TYPES3.includes(jobType)) {
17730
18634
  console.error(`Invalid job type: ${jobType}`);
17731
- console.error(`Valid types: ${VALID_JOB_TYPES2.join(", ")}`);
18635
+ console.error(`Valid types: ${VALID_JOB_TYPES3.join(", ")}`);
17732
18636
  process.exit(1);
17733
18637
  }
17734
18638
  const queueConfig = loadConfig(projectDir).queue;
17735
- const projectName = path42.basename(projectDir);
18639
+ if (isJobPaused(projectDir, jobType)) {
18640
+ process.exit(2);
18641
+ }
18642
+ const projectName = path43.basename(projectDir);
17736
18643
  const callerPid = opts.pid ? parseInt(opts.pid, 10) : void 0;
17737
18644
  const result = claimJobSlot(
17738
18645
  projectDir,
@@ -17875,7 +18782,7 @@ function notifyCommand(program2) {
17875
18782
 
17876
18783
  // src/commands/summary.ts
17877
18784
  init_dist();
17878
- import path43 from "path";
18785
+ import path44 from "path";
17879
18786
  import chalk8 from "chalk";
17880
18787
  function formatDuration2(seconds) {
17881
18788
  if (seconds === null) return "-";
@@ -17905,7 +18812,7 @@ function formatJobStatus(status) {
17905
18812
  return chalk8.dim(status);
17906
18813
  }
17907
18814
  function getProjectName2(projectPath) {
17908
- return path43.basename(projectPath) || projectPath;
18815
+ return path44.basename(projectPath) || projectPath;
17909
18816
  }
17910
18817
  function formatProvider(providerKey) {
17911
18818
  return providerKey.split(":")[0] || providerKey;
@@ -18024,7 +18931,7 @@ function summaryCommand(program2) {
18024
18931
  // src/commands/resolve.ts
18025
18932
  init_dist();
18026
18933
  import { execFileSync as execFileSync7 } from "child_process";
18027
- import * as path44 from "path";
18934
+ import * as path45 from "path";
18028
18935
  function buildEnvVars6(config, options) {
18029
18936
  const env = buildBaseEnvVars(config, "pr-resolver", options.dryRun);
18030
18937
  env.NW_PR_RESOLVER_MAX_RUNTIME = String(config.prResolver.maxRuntime);
@@ -18154,7 +19061,7 @@ ${stderr}`);
18154
19061
  const notificationEvent = exitCode === 0 ? "pr_resolver_completed" : "pr_resolver_failed";
18155
19062
  await sendNotifications(config, {
18156
19063
  event: notificationEvent,
18157
- projectName: path44.basename(projectDir),
19064
+ projectName: path45.basename(projectDir),
18158
19065
  exitCode,
18159
19066
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
18160
19067
  });
@@ -18169,7 +19076,7 @@ ${stderr}`);
18169
19076
 
18170
19077
  // src/commands/merge.ts
18171
19078
  init_dist();
18172
- import * as path45 from "path";
19079
+ import * as path46 from "path";
18173
19080
  function buildEnvVars7(config, options) {
18174
19081
  const env = buildBaseEnvVars(config, "merger", options.dryRun);
18175
19082
  env.NW_MERGER_MAX_RUNTIME = String(config.merger.maxRuntime);
@@ -18275,7 +19182,7 @@ ${stderr}`);
18275
19182
  if (notificationEvent) {
18276
19183
  await sendNotifications(config, {
18277
19184
  event: notificationEvent,
18278
- projectName: path45.basename(projectDir),
19185
+ projectName: path46.basename(projectDir),
18279
19186
  exitCode,
18280
19187
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL)
18281
19188
  });
@@ -18289,19 +19196,289 @@ ${stderr}`);
18289
19196
  });
18290
19197
  }
18291
19198
 
19199
+ // src/commands/agent.ts
19200
+ init_dist();
19201
+ var SCHEMA_VERSION2 = 1;
19202
+ var JSON_OPTION = "--json";
19203
+ var JSON_OPTION_DESCRIPTION = "Output as JSON";
19204
+ function writeJson(value) {
19205
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
19206
+ `);
19207
+ }
19208
+ function fail(message, options) {
19209
+ if (options?.json) {
19210
+ const payload = {
19211
+ schemaVersion: SCHEMA_VERSION2,
19212
+ ok: false,
19213
+ error: message
19214
+ };
19215
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}
19216
+ `);
19217
+ } else {
19218
+ process.stderr.write(`${message}
19219
+ `);
19220
+ }
19221
+ process.exit(1);
19222
+ }
19223
+ function getProcess(snapshot, name) {
19224
+ const processInfo = snapshot.processes.find((processEntry) => processEntry.name === name);
19225
+ return { running: processInfo?.running ?? false, pid: processInfo?.pid ?? null };
19226
+ }
19227
+ function buildLegacyStatus(snapshot, config) {
19228
+ const pendingPrds = snapshot.prds.filter(
19229
+ (prd) => prd.status === "ready" || prd.status === "blocked"
19230
+ ).length;
19231
+ const claimedPrds = snapshot.prds.filter((prd) => prd.status === "in-progress").length;
19232
+ const donePrds = snapshot.prds.filter((prd) => prd.status === "done").length;
19233
+ const logs = Object.fromEntries(
19234
+ snapshot.logs.map((log) => [
19235
+ log.name,
19236
+ { path: log.path, lastLines: log.lastLines, exists: log.exists, size: log.size }
19237
+ ])
19238
+ );
19239
+ return {
19240
+ projectName: snapshot.projectName,
19241
+ projectDir: snapshot.projectDir,
19242
+ provider: config.provider,
19243
+ reviewerEnabled: config.reviewerEnabled,
19244
+ autoMerge: config.autoMerge,
19245
+ autoMergeMethod: config.autoMergeMethod,
19246
+ executor: getProcess(snapshot, "executor"),
19247
+ reviewer: getProcess(snapshot, "reviewer"),
19248
+ qa: getProcess(snapshot, "qa"),
19249
+ audit: getProcess(snapshot, "audit"),
19250
+ planner: getProcess(snapshot, "planner"),
19251
+ analytics: getProcess(snapshot, "analytics"),
19252
+ merger: getProcess(snapshot, "merger"),
19253
+ prds: { pending: pendingPrds, claimed: claimedPrds, done: donePrds },
19254
+ prs: { open: snapshot.prs.length },
19255
+ crontab: snapshot.crontab,
19256
+ logs
19257
+ };
19258
+ }
19259
+ function buildPausedState(config) {
19260
+ return Object.fromEntries(
19261
+ getValidJobTypes().map((jobType) => [jobType, config.pausedJobs?.[jobType] === true])
19262
+ );
19263
+ }
19264
+ function buildLastRuns(analytics) {
19265
+ const lastRuns = Object.fromEntries(
19266
+ getValidJobTypes().map((jobType) => [
19267
+ jobType,
19268
+ { lastSuccessAt: null, lastFailureAt: null, lastExitCode: null }
19269
+ ])
19270
+ );
19271
+ for (const run2 of analytics.recentRuns) {
19272
+ const item = lastRuns[run2.jobType];
19273
+ if (!item) continue;
19274
+ const finishedAt = run2.finishedAt ? new Date(run2.finishedAt * 1e3).toISOString() : null;
19275
+ if (run2.status === "success" && item.lastSuccessAt === null) {
19276
+ item.lastSuccessAt = finishedAt;
19277
+ item.lastExitCode = 0;
19278
+ } else if (run2.status !== "success" && item.lastFailureAt === null) {
19279
+ item.lastFailureAt = finishedAt;
19280
+ item.lastExitCode = 1;
19281
+ }
19282
+ }
19283
+ return lastRuns;
19284
+ }
19285
+ async function getBoardSnapshot(projectDir, config) {
19286
+ if (config.boardProvider?.enabled === false || !config.boardProvider?.projectNumber) {
19287
+ return { configured: false, columns: [], items: [], error: null };
19288
+ }
19289
+ try {
19290
+ const provider = createBoardProvider(config.boardProvider, projectDir);
19291
+ const [columns, items] = await Promise.all([provider.getColumns(), provider.getAllIssues()]);
19292
+ return { configured: true, columns, items, error: null };
19293
+ } catch (error2) {
19294
+ return {
19295
+ configured: true,
19296
+ columns: [],
19297
+ items: [],
19298
+ error: error2 instanceof Error ? error2.message : String(error2)
19299
+ };
19300
+ }
19301
+ }
19302
+ function buildHealth(snapshot, config) {
19303
+ const checks = [
19304
+ {
19305
+ name: "config",
19306
+ ok: true,
19307
+ message: "Configuration loaded"
19308
+ },
19309
+ {
19310
+ name: "cron",
19311
+ ok: snapshot.crontab.installed,
19312
+ message: snapshot.crontab.installed ? "Cron entries installed" : "No Night Watch cron entries found"
19313
+ },
19314
+ {
19315
+ name: "queue",
19316
+ ok: true,
19317
+ message: config.queue.enabled ? "Global queue enabled" : "Global queue disabled"
19318
+ },
19319
+ {
19320
+ name: "provider",
19321
+ ok: Boolean(config.provider),
19322
+ message: config.provider ? `Provider configured: ${config.provider}` : "No provider configured"
19323
+ }
19324
+ ];
19325
+ const staleLocks = snapshot.processes.filter(
19326
+ (processInfo) => !processInfo.running && processInfo.pid !== null
19327
+ );
19328
+ checks.push({
19329
+ name: "locks",
19330
+ ok: staleLocks.length === 0,
19331
+ message: staleLocks.length === 0 ? "No stale lock files detected" : `Stale lock files detected for ${staleLocks.map((lock) => lock.name).join(", ")}`
19332
+ });
19333
+ return { schemaVersion: SCHEMA_VERSION2, ok: checks.every((check) => check.ok), checks };
19334
+ }
19335
+ async function buildAgentStatus(projectDir) {
19336
+ const config = loadConfig(projectDir);
19337
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
19338
+ const analytics = getJobRunsAnalytics(24 * 30);
19339
+ const health = buildHealth(snapshot, config);
19340
+ return {
19341
+ schemaVersion: SCHEMA_VERSION2,
19342
+ generatedAt: snapshot.timestamp.toISOString(),
19343
+ project: { name: snapshot.projectName, dir: snapshot.projectDir, provider: config.provider },
19344
+ status: buildLegacyStatus(snapshot, config),
19345
+ paused: buildPausedState(config),
19346
+ queue: getQueueStatus(),
19347
+ board: await getBoardSnapshot(projectDir, config),
19348
+ health,
19349
+ lastRuns: buildLastRuns(analytics)
19350
+ };
19351
+ }
19352
+ function normalizeJobType(job) {
19353
+ if (getValidJobTypes().includes(job)) {
19354
+ return job;
19355
+ }
19356
+ throw new Error(`Invalid job: ${job}. Valid jobs: ${getValidJobTypes().join(", ")}`);
19357
+ }
19358
+ function agentCommand(program2) {
19359
+ const agent = program2.command("agent").description("Machine-readable agent operations");
19360
+ agent.command("status").description("Print a stable machine-readable project snapshot").requiredOption(JSON_OPTION, "Output status as JSON").action(async () => {
19361
+ writeJson(await buildAgentStatus(process.cwd()));
19362
+ });
19363
+ }
19364
+ function configCommand(program2) {
19365
+ const config = program2.command("config").description("Inspect and edit Night Watch config");
19366
+ config.command("list").description("Print resolved config").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((options) => {
19367
+ const value = loadConfig(process.cwd());
19368
+ if (options.json) {
19369
+ writeJson({ schemaVersion: SCHEMA_VERSION2, config: value });
19370
+ } else {
19371
+ writeJson(value);
19372
+ }
19373
+ });
19374
+ config.command("get <path>").description("Read a resolved config value by dot path").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((dotPath, options) => {
19375
+ try {
19376
+ const result = getConfigValue(process.cwd(), dotPath);
19377
+ if (options.json) {
19378
+ writeJson({ schemaVersion: SCHEMA_VERSION2, ...result });
19379
+ } else {
19380
+ writeJson(result.value);
19381
+ }
19382
+ } catch (error2) {
19383
+ fail(error2 instanceof Error ? error2.message : String(error2), options);
19384
+ }
19385
+ });
19386
+ config.command("set <path> <value>").description("Write a config value by dot path").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((dotPath, rawValue, options) => {
19387
+ try {
19388
+ const result = setConfigValue(process.cwd(), dotPath, parseConfigValue(rawValue));
19389
+ if (options.json) {
19390
+ writeJson({ schemaVersion: SCHEMA_VERSION2, ok: true, ...result });
19391
+ } else {
19392
+ process.stdout.write(`Updated ${result.path}
19393
+ `);
19394
+ }
19395
+ } catch (error2) {
19396
+ fail(error2 instanceof Error ? error2.message : String(error2), options);
19397
+ }
19398
+ });
19399
+ }
19400
+ function healthCommand(program2) {
19401
+ program2.command("health").description("Check automation readiness").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action(async (options) => {
19402
+ const config = loadConfig(process.cwd());
19403
+ const snapshot = await fetchStatusSnapshot(process.cwd(), config);
19404
+ const health = buildHealth(snapshot, config);
19405
+ if (options.json) {
19406
+ writeJson(health);
19407
+ } else {
19408
+ for (const check of health.checks) {
19409
+ process.stdout.write(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}
19410
+ `);
19411
+ }
19412
+ }
19413
+ if (!health.ok) {
19414
+ process.exitCode = 1;
19415
+ }
19416
+ });
19417
+ }
19418
+ function jobCommand(program2) {
19419
+ const job = program2.command("job").description("Manage Night Watch jobs");
19420
+ job.command("pause <job>").description("Pause a cron/queue-dispatched job").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((jobName, options) => {
19421
+ try {
19422
+ const jobType = normalizeJobType(jobName);
19423
+ const result = setConfigValue(process.cwd(), `pausedJobs.${jobType}`, true);
19424
+ if (options.json) {
19425
+ writeJson({
19426
+ schemaVersion: SCHEMA_VERSION2,
19427
+ ok: true,
19428
+ job: jobType,
19429
+ paused: result.value
19430
+ });
19431
+ } else {
19432
+ process.stdout.write(`Paused ${jobType}
19433
+ `);
19434
+ }
19435
+ } catch (error2) {
19436
+ fail(error2 instanceof Error ? error2.message : String(error2), options);
19437
+ }
19438
+ });
19439
+ job.command("resume <job>").description("Resume a cron/queue-dispatched job").option(JSON_OPTION, JSON_OPTION_DESCRIPTION).action((jobName, options) => {
19440
+ try {
19441
+ const jobType = normalizeJobType(jobName);
19442
+ const result = setConfigValue(process.cwd(), `pausedJobs.${jobType}`, false);
19443
+ if (options.json) {
19444
+ writeJson({
19445
+ schemaVersion: SCHEMA_VERSION2,
19446
+ ok: true,
19447
+ job: jobType,
19448
+ paused: result.value
19449
+ });
19450
+ } else {
19451
+ process.stdout.write(`Resumed ${jobType}
19452
+ `);
19453
+ }
19454
+ } catch (error2) {
19455
+ fail(error2 instanceof Error ? error2.message : String(error2), options);
19456
+ }
19457
+ });
19458
+ job.command("is-paused <job>").description("Return zero when a job is paused").action((jobName) => {
19459
+ try {
19460
+ const jobType = normalizeJobType(jobName);
19461
+ const paused = loadConfig(process.cwd()).pausedJobs?.[jobType] === true;
19462
+ process.exit(paused ? 0 : 1);
19463
+ } catch {
19464
+ process.exit(1);
19465
+ }
19466
+ });
19467
+ }
19468
+
18292
19469
  // src/cli.ts
18293
19470
  var __filename5 = fileURLToPath6(import.meta.url);
18294
19471
  var __dirname5 = dirname12(__filename5);
18295
19472
  function findPackageRoot(dir) {
18296
19473
  let d = dir;
18297
19474
  for (let i = 0; i < 5; i++) {
18298
- if (existsSync33(join37(d, "package.json"))) return d;
19475
+ if (existsSync34(join38(d, "package.json"))) return d;
18299
19476
  d = dirname12(d);
18300
19477
  }
18301
19478
  return dir;
18302
19479
  }
18303
19480
  var packageRoot = findPackageRoot(__dirname5);
18304
- var packageJson = JSON.parse(readFileSync20(join37(packageRoot, "package.json"), "utf-8"));
19481
+ var packageJson = JSON.parse(readFileSync21(join38(packageRoot, "package.json"), "utf-8"));
18305
19482
  var program = new Command3();
18306
19483
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
18307
19484
  initCommand(program);
@@ -18333,4 +19510,8 @@ notifyCommand(program);
18333
19510
  summaryCommand(program);
18334
19511
  resolveCommand(program);
18335
19512
  mergeCommand(program);
19513
+ agentCommand(program);
19514
+ configCommand(program);
19515
+ healthCommand(program);
19516
+ jobCommand(program);
18336
19517
  program.parse();