@jonit-dev/night-watch-cli 1.7.65 → 1.7.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -5,10 +5,6 @@ import 'reflect-metadata';
5
5
  import "reflect-metadata";
6
6
  import "reflect-metadata";
7
7
  import "reflect-metadata";
8
- import "reflect-metadata";
9
- import "reflect-metadata";
10
- import "reflect-metadata";
11
- import "reflect-metadata";
12
8
  import * as fs from "fs";
13
9
  import * as path from "path";
14
10
  import { fileURLToPath } from "url";
@@ -73,103 +69,105 @@ import * as path13 from "path";
73
69
  import { spawn } from "child_process";
74
70
  import { createHash as createHash3 } from "crypto";
75
71
  import { spawn as spawn2 } from "child_process";
76
- import { execFileSync as execFileSync4 } from "child_process";
77
72
  import * as fs15 from "fs";
78
73
  import * as path14 from "path";
79
- import * as os5 from "os";
74
+ import { execFileSync as execFileSync4 } from "child_process";
75
+ import * as fs16 from "fs";
80
76
  import * as path15 from "path";
77
+ import * as os5 from "os";
78
+ import * as path16 from "path";
81
79
  import Database7 from "better-sqlite3";
82
80
  import "reflect-metadata";
83
81
  import { Command as Command3 } from "commander";
84
- import { existsSync as existsSync28, readFileSync as readFileSync17 } from "fs";
82
+ import { existsSync as existsSync29, readFileSync as readFileSync17 } from "fs";
85
83
  import { fileURLToPath as fileURLToPath4 } from "url";
86
- import { dirname as dirname8, join as join33 } from "path";
87
- import fs16 from "fs";
88
- import path16 from "path";
84
+ import { dirname as dirname8, join as join34 } from "path";
85
+ import fs17 from "fs";
86
+ import path17 from "path";
89
87
  import { execSync as execSync3 } from "child_process";
90
88
  import { fileURLToPath as fileURLToPath2 } from "url";
91
- import { dirname as dirname4, join as join15 } from "path";
89
+ import { dirname as dirname4, join as join16 } from "path";
92
90
  import * as readline from "readline";
93
- import * as fs17 from "fs";
94
- import * as path17 from "path";
95
- import { execFileSync as execFileSync5 } from "child_process";
91
+ import * as fs18 from "fs";
96
92
  import * as path18 from "path";
93
+ import { execFileSync as execFileSync5 } from "child_process";
97
94
  import * as path19 from "path";
98
- import * as fs18 from "fs";
99
95
  import * as path20 from "path";
100
- import { execSync as execSync4 } from "child_process";
101
- import * as path21 from "path";
102
96
  import * as fs19 from "fs";
97
+ import * as path21 from "path";
98
+ import { execSync as execSync4 } from "child_process";
103
99
  import * as path22 from "path";
104
100
  import * as fs20 from "fs";
105
- import chalk2 from "chalk";
106
- import { spawn as spawn3 } from "child_process";
107
101
  import * as path23 from "path";
108
102
  import * as fs21 from "fs";
109
- import * as fs22 from "fs";
103
+ import chalk2 from "chalk";
104
+ import { spawn as spawn3 } from "child_process";
110
105
  import * as path24 from "path";
106
+ import * as fs22 from "fs";
107
+ import * as fs23 from "fs";
108
+ import * as path25 from "path";
111
109
  import * as readline2 from "readline";
112
110
  import blessed6 from "blessed";
113
111
  import blessed from "blessed";
114
- import * as fs23 from "fs";
112
+ import * as fs24 from "fs";
115
113
  import blessed2 from "blessed";
116
114
  import blessed3 from "blessed";
117
115
  import cronstrue from "cronstrue";
118
116
  import blessed4 from "blessed";
119
117
  import { spawn as spawn4 } from "child_process";
120
118
  import blessed5 from "blessed";
121
- import * as fs24 from "fs";
122
- import * as path25 from "path";
119
+ import * as fs25 from "fs";
120
+ import * as path26 from "path";
121
+ import * as fs30 from "fs";
123
122
  import * as fs29 from "fs";
124
- import * as fs28 from "fs";
125
- import * as path31 from "path";
123
+ import * as path32 from "path";
126
124
  import { dirname as dirname7 } from "path";
127
125
  import { fileURLToPath as fileURLToPath3 } from "url";
128
126
  import cors from "cors";
129
127
  import express from "express";
130
- import * as fs25 from "fs";
131
- import * as path26 from "path";
132
128
  import * as fs26 from "fs";
133
129
  import * as path27 from "path";
130
+ import * as fs27 from "fs";
131
+ import * as path28 from "path";
134
132
  import { execSync as execSync5, spawn as spawn5 } from "child_process";
135
133
  import { Router } from "express";
136
134
  import { Router as Router2 } from "express";
137
135
  import { Router as Router3 } from "express";
138
136
  import { CronExpressionParser } from "cron-parser";
139
- import * as fs27 from "fs";
140
- import * as path28 from "path";
137
+ import * as fs28 from "fs";
138
+ import * as path29 from "path";
141
139
  import { execSync as execSync6 } from "child_process";
142
140
  import { Router as Router4 } from "express";
143
- import * as path29 from "path";
141
+ import * as path30 from "path";
144
142
  import { Router as Router5 } from "express";
145
143
  import { Router as Router6 } from "express";
146
- import * as path30 from "path";
144
+ import * as path31 from "path";
147
145
  import { Router as Router7 } from "express";
148
146
  import { Router as Router8 } from "express";
149
147
  import { CronExpressionParser as CronExpressionParser2 } from "cron-parser";
150
148
  import { Router as Router9 } from "express";
151
149
  import { spawnSync } from "child_process";
152
- import * as fs30 from "fs";
153
- import * as path32 from "path";
154
150
  import * as fs31 from "fs";
155
151
  import * as path33 from "path";
152
+ import * as fs32 from "fs";
153
+ import * as path34 from "path";
156
154
  import chalk3 from "chalk";
157
155
  import chalk4 from "chalk";
158
156
  import { execSync as execSync7 } from "child_process";
159
- import * as fs32 from "fs";
160
- import * as readline3 from "readline";
161
157
  import * as fs33 from "fs";
162
- import * as path34 from "path";
163
- import * as os6 from "os";
158
+ import * as readline3 from "readline";
159
+ import * as fs34 from "fs";
164
160
  import * as path35 from "path";
161
+ import * as os6 from "os";
162
+ import * as path36 from "path";
165
163
  import chalk5 from "chalk";
166
164
  import { Command } from "commander";
167
165
  import { execFileSync as execFileSync6 } from "child_process";
168
- import * as fs34 from "fs";
169
- import * as path36 from "path";
166
+ import * as fs35 from "fs";
167
+ import * as path37 from "path";
170
168
  import * as readline4 from "readline";
171
169
  import chalk6 from "chalk";
172
- import * as path37 from "path";
170
+ import * as path38 from "path";
173
171
  import { spawn as spawn6 } from "child_process";
174
172
  import chalk7 from "chalk";
175
173
  import { Command as Command2 } from "commander";
@@ -258,6 +256,7 @@ var DEFAULT_QUEUE_MAX_CONCURRENCY;
258
256
  var DEFAULT_QUEUE_MAX_WAIT_TIME;
259
257
  var DEFAULT_QUEUE_PRIORITY;
260
258
  var DEFAULT_QUEUE;
259
+ var DEFAULT_SCHEDULING_PRIORITY;
261
260
  var QUEUE_LOCK_FILE_NAME;
262
261
  var init_constants = __esm({
263
262
  "../core/dist/constants.js"() {
@@ -363,7 +362,7 @@ var init_constants = __esm({
363
362
  PRD_STATES_FILE_NAME = "prd-states.json";
364
363
  STATE_DB_FILE_NAME = "state.db";
365
364
  MAX_HISTORY_RECORDS_PER_PRD = 10;
366
- DEFAULT_QUEUE_ENABLED = false;
365
+ DEFAULT_QUEUE_ENABLED = true;
367
366
  DEFAULT_QUEUE_MAX_CONCURRENCY = 1;
368
367
  DEFAULT_QUEUE_MAX_WAIT_TIME = 7200;
369
368
  DEFAULT_QUEUE_PRIORITY = {
@@ -379,6 +378,7 @@ var init_constants = __esm({
379
378
  maxWaitTime: DEFAULT_QUEUE_MAX_WAIT_TIME,
380
379
  priority: { ...DEFAULT_QUEUE_PRIORITY }
381
380
  };
381
+ DEFAULT_SCHEDULING_PRIORITY = 3;
382
382
  QUEUE_LOCK_FILE_NAME = "queue.lock";
383
383
  }
384
384
  });
@@ -398,6 +398,7 @@ function getDefaultConfig() {
398
398
  reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
399
399
  scheduleBundleId: null,
400
400
  cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
401
+ schedulingPriority: DEFAULT_SCHEDULING_PRIORITY,
401
402
  maxRetries: DEFAULT_MAX_RETRIES,
402
403
  // Reviewer retry configuration
403
404
  reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
@@ -474,6 +475,7 @@ function normalizeConfig(rawConfig) {
474
475
  normalized.scheduleBundleId = null;
475
476
  }
476
477
  normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
478
+ normalized.schedulingPriority = readNumber(rawConfig.schedulingPriority);
477
479
  normalized.maxRetries = readNumber(rawConfig.maxRetries);
478
480
  normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
479
481
  normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
@@ -601,7 +603,7 @@ function normalizeConfig(rawConfig) {
601
603
  if (rawQueue) {
602
604
  const queue = {
603
605
  enabled: readBoolean(rawQueue.enabled) ?? DEFAULT_QUEUE.enabled,
604
- maxConcurrency: readNumber(rawQueue.maxConcurrency) ?? DEFAULT_QUEUE.maxConcurrency,
606
+ maxConcurrency: DEFAULT_QUEUE.maxConcurrency,
605
607
  maxWaitTime: readNumber(rawQueue.maxWaitTime) ?? DEFAULT_QUEUE.maxWaitTime,
606
608
  priority: { ...DEFAULT_QUEUE.priority }
607
609
  };
@@ -614,10 +616,13 @@ function normalizeConfig(rawConfig) {
614
616
  }
615
617
  }
616
618
  }
617
- queue.maxConcurrency = Math.max(1, Math.min(10, queue.maxConcurrency));
619
+ queue.maxConcurrency = DEFAULT_QUEUE.maxConcurrency;
618
620
  queue.maxWaitTime = Math.max(300, Math.min(14400, queue.maxWaitTime));
619
621
  normalized.queue = queue;
620
622
  }
623
+ if (normalized.schedulingPriority !== void 0) {
624
+ normalized.schedulingPriority = Math.max(1, Math.min(5, normalized.schedulingPriority));
625
+ }
621
626
  return normalized;
622
627
  }
623
628
  function parseBoolean(value) {
@@ -757,6 +762,12 @@ function loadConfig(projectDir) {
757
762
  envConfig.cronScheduleOffset = offset;
758
763
  }
759
764
  }
765
+ if (process.env.NW_SCHEDULING_PRIORITY) {
766
+ const priority = parseInt(process.env.NW_SCHEDULING_PRIORITY, 10);
767
+ if (!isNaN(priority)) {
768
+ envConfig.schedulingPriority = Math.max(1, Math.min(5, priority));
769
+ }
770
+ }
760
771
  if (process.env.NW_MAX_RETRIES) {
761
772
  const retries = parseInt(process.env.NW_MAX_RETRIES, 10);
762
773
  if (!isNaN(retries) && retries >= 1) {
@@ -967,10 +978,7 @@ function loadConfig(projectDir) {
967
978
  }
968
979
  }
969
980
  if (process.env.NW_QUEUE_MAX_CONCURRENCY) {
970
- const maxConcurrency = parseInt(process.env.NW_QUEUE_MAX_CONCURRENCY, 10);
971
- if (!isNaN(maxConcurrency) && maxConcurrency >= 1) {
972
- envConfig.queue = { ...queueBaseConfig(), maxConcurrency: Math.min(10, maxConcurrency) };
973
- }
981
+ envConfig.queue = { ...queueBaseConfig(), maxConcurrency: DEFAULT_QUEUE.maxConcurrency };
974
982
  }
975
983
  if (process.env.NW_QUEUE_MAX_WAIT_TIME) {
976
984
  const maxWaitTime = parseInt(process.env.NW_QUEUE_MAX_WAIT_TIME, 10);
@@ -2976,7 +2984,7 @@ function buildAvatarPrompt(personaName, role) {
2976
2984
  return `Professional headshot portrait photo of a ${descriptor}, photorealistic, clean soft neutral background, natural diffused window lighting, shot at f/2.8, shallow depth of field, looking directly at camera, candid professional headshot style, no retouching artifacts, natural skin texture`;
2977
2985
  }
2978
2986
  function sleep(ms) {
2979
- return new Promise((resolve9) => setTimeout(resolve9, ms));
2987
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
2980
2988
  }
2981
2989
  async function generatePersonaAvatar(personaName, personaRole, apiToken) {
2982
2990
  const prompt2 = buildAvatarPrompt(personaName, personaRole);
@@ -3774,7 +3782,7 @@ function getLockFilePaths(projectDir) {
3774
3782
  };
3775
3783
  }
3776
3784
  function sleep2(ms) {
3777
- return new Promise((resolve9) => setTimeout(resolve9, ms));
3785
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
3778
3786
  }
3779
3787
  async function cancelProcess(processType, lockPath, force = false) {
3780
3788
  const lockStatus = checkLockFile(lockPath);
@@ -4097,7 +4105,7 @@ var init_config_writer = __esm({
4097
4105
  "../core/dist/utils/config-writer.js"() {
4098
4106
  "use strict";
4099
4107
  init_constants();
4100
- PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set(["notifications", "qa", "audit", "roadmapScanner"]);
4108
+ PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set(["notifications", "qa", "audit", "roadmapScanner", "queue"]);
4101
4109
  }
4102
4110
  });
4103
4111
  function getHistoryPath() {
@@ -5527,7 +5535,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5527
5535
  const logStream = fs14.createWriteStream(logFile, { flags: "w" });
5528
5536
  logStream.on("error", () => {
5529
5537
  });
5530
- return new Promise((resolve9) => {
5538
+ return new Promise((resolve10) => {
5531
5539
  const childEnv = {
5532
5540
  ...process.env,
5533
5541
  ...config.providerEnv
@@ -5545,7 +5553,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5545
5553
  });
5546
5554
  child.on("error", (error2) => {
5547
5555
  logStream.end();
5548
- resolve9({
5556
+ resolve10({
5549
5557
  sliced: false,
5550
5558
  error: `Failed to spawn provider: ${error2.message}`,
5551
5559
  item
@@ -5554,7 +5562,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5554
5562
  child.on("close", (code) => {
5555
5563
  logStream.end();
5556
5564
  if (code !== 0) {
5557
- resolve9({
5565
+ resolve10({
5558
5566
  sliced: false,
5559
5567
  error: `Provider exited with code ${code ?? 1}`,
5560
5568
  item
@@ -5562,14 +5570,14 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5562
5570
  return;
5563
5571
  }
5564
5572
  if (!fs14.existsSync(filePath)) {
5565
- resolve9({
5573
+ resolve10({
5566
5574
  sliced: false,
5567
5575
  error: `Provider did not create expected file: ${filePath}`,
5568
5576
  item
5569
5577
  });
5570
5578
  return;
5571
5579
  }
5572
- resolve9({
5580
+ resolve10({
5573
5581
  sliced: true,
5574
5582
  file: filename,
5575
5583
  item
@@ -5590,7 +5598,7 @@ async function sliceNextItem(projectDir, config) {
5590
5598
  if (!roadmapExists && auditItems.length === 0) {
5591
5599
  return {
5592
5600
  sliced: false,
5593
- error: "ROADMAP.md not found"
5601
+ error: "No pending items to process"
5594
5602
  };
5595
5603
  }
5596
5604
  const roadmapItems = roadmapExists ? parseRoadmap(fs14.readFileSync(roadmapPath, "utf-8")) : [];
@@ -5725,7 +5733,7 @@ async function executeScript(scriptPath, args = [], env = {}, options = {}) {
5725
5733
  return result.exitCode;
5726
5734
  }
5727
5735
  async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
5728
- return new Promise((resolve9, reject) => {
5736
+ return new Promise((resolve10, reject) => {
5729
5737
  const childEnv = {
5730
5738
  ...process.env,
5731
5739
  ...env
@@ -5751,7 +5759,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}, options
5751
5759
  reject(error2);
5752
5760
  });
5753
5761
  child.on("close", (code) => {
5754
- resolve9({
5762
+ resolve10({
5755
5763
  exitCode: code ?? 1,
5756
5764
  stdout: stdoutChunks.join(""),
5757
5765
  stderr: stderrChunks.join("")
@@ -5764,6 +5772,106 @@ var init_shell = __esm({
5764
5772
  "use strict";
5765
5773
  }
5766
5774
  });
5775
+ function normalizeSchedulingPriority(priority) {
5776
+ if (!Number.isFinite(priority)) {
5777
+ return DEFAULT_SCHEDULING_PRIORITY;
5778
+ }
5779
+ return Math.max(1, Math.min(5, Math.floor(priority)));
5780
+ }
5781
+ function isJobTypeEnabled(config, jobType) {
5782
+ switch (jobType) {
5783
+ case "executor":
5784
+ return config.executorEnabled !== false;
5785
+ case "reviewer":
5786
+ return config.reviewerEnabled;
5787
+ case "qa":
5788
+ return config.qa.enabled;
5789
+ case "audit":
5790
+ return config.audit.enabled;
5791
+ case "slicer":
5792
+ return config.roadmapScanner.enabled;
5793
+ default:
5794
+ return true;
5795
+ }
5796
+ }
5797
+ function loadPeerConfig(projectPath) {
5798
+ if (!fs15.existsSync(projectPath) || !fs15.existsSync(path14.join(projectPath, CONFIG_FILE_NAME))) {
5799
+ return null;
5800
+ }
5801
+ try {
5802
+ return loadConfig(projectPath);
5803
+ } catch {
5804
+ return null;
5805
+ }
5806
+ }
5807
+ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5808
+ const peers = /* @__PURE__ */ new Map();
5809
+ const currentPath = path14.resolve(currentProjectDir);
5810
+ const addPeer = (projectPath, config) => {
5811
+ const resolvedPath = path14.resolve(projectPath);
5812
+ if (!isJobTypeEnabled(config, jobType)) {
5813
+ return;
5814
+ }
5815
+ peers.set(resolvedPath, {
5816
+ path: resolvedPath,
5817
+ config,
5818
+ schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
5819
+ sortKey: `${path14.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
5820
+ });
5821
+ };
5822
+ addPeer(currentPath, currentConfig);
5823
+ for (const entry of loadRegistry()) {
5824
+ const resolvedPath = path14.resolve(entry.path);
5825
+ if (resolvedPath === currentPath || peers.has(resolvedPath)) {
5826
+ continue;
5827
+ }
5828
+ const peerConfig = loadPeerConfig(resolvedPath);
5829
+ if (peerConfig) {
5830
+ addPeer(resolvedPath, peerConfig);
5831
+ }
5832
+ }
5833
+ return Array.from(peers.values()).sort((left, right) => {
5834
+ if (left.schedulingPriority !== right.schedulingPriority) {
5835
+ return right.schedulingPriority - left.schedulingPriority;
5836
+ }
5837
+ return left.sortKey.localeCompare(right.sortKey);
5838
+ });
5839
+ }
5840
+ function getSchedulingPlan(projectDir, config, jobType) {
5841
+ const peers = collectSchedulingPeers(projectDir, config, jobType);
5842
+ const currentPath = path14.resolve(projectDir);
5843
+ const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
5844
+ const peerCount = Math.max(1, peers.length);
5845
+ const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
5846
+ const manualDelayMinutes = Math.max(0, Math.floor(config.cronScheduleOffset ?? DEFAULT_CRON_SCHEDULE_OFFSET));
5847
+ return {
5848
+ manualDelayMinutes,
5849
+ balancedDelayMinutes,
5850
+ totalDelayMinutes: manualDelayMinutes + balancedDelayMinutes,
5851
+ peerCount,
5852
+ slotIndex,
5853
+ schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority)
5854
+ };
5855
+ }
5856
+ function addDelayToIsoString(isoString, delayMinutes) {
5857
+ if (!isoString) {
5858
+ return null;
5859
+ }
5860
+ const date = new Date(isoString);
5861
+ if (Number.isNaN(date.getTime())) {
5862
+ return null;
5863
+ }
5864
+ date.setTime(date.getTime() + delayMinutes * 6e4);
5865
+ return date.toISOString();
5866
+ }
5867
+ var init_scheduling = __esm({
5868
+ "../core/dist/utils/scheduling.js"() {
5869
+ "use strict";
5870
+ init_config();
5871
+ init_constants();
5872
+ init_registry();
5873
+ }
5874
+ });
5767
5875
  function validateWebhook(webhook) {
5768
5876
  const issues = [];
5769
5877
  if (!webhook.events || webhook.events.length === 0) {
@@ -5827,7 +5935,7 @@ function gitExec(args, cwd, logFile) {
5827
5935
  });
5828
5936
  if (logFile && result) {
5829
5937
  try {
5830
- fs15.appendFileSync(logFile, result);
5938
+ fs16.appendFileSync(logFile, result);
5831
5939
  } catch {
5832
5940
  }
5833
5941
  }
@@ -5836,7 +5944,7 @@ function gitExec(args, cwd, logFile) {
5836
5944
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
5837
5945
  if (logFile) {
5838
5946
  try {
5839
- fs15.appendFileSync(logFile, errorMessage + "\n");
5947
+ fs16.appendFileSync(logFile, errorMessage + "\n");
5840
5948
  } catch {
5841
5949
  }
5842
5950
  }
@@ -5895,11 +6003,11 @@ function prepareBranchWorktree(options) {
5895
6003
  }
5896
6004
  function prepareDetachedWorktree(options) {
5897
6005
  const { projectDir, worktreeDir, defaultBranch, logFile } = options;
5898
- if (fs15.existsSync(worktreeDir)) {
6006
+ if (fs16.existsSync(worktreeDir)) {
5899
6007
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
5900
6008
  if (!isRegistered) {
5901
6009
  try {
5902
- fs15.rmSync(worktreeDir, { recursive: true, force: true });
6010
+ fs16.rmSync(worktreeDir, { recursive: true, force: true });
5903
6011
  } catch {
5904
6012
  }
5905
6013
  }
@@ -5941,7 +6049,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
5941
6049
  }
5942
6050
  }
5943
6051
  function cleanupWorktrees(projectDir, scope) {
5944
- const projectName = path14.basename(projectDir);
6052
+ const projectName = path15.basename(projectDir);
5945
6053
  const matchToken = scope ? scope : `${projectName}-nw`;
5946
6054
  const removed = [];
5947
6055
  try {
@@ -5975,12 +6083,12 @@ var init_worktree_manager = __esm({
5975
6083
  }
5976
6084
  });
5977
6085
  function getStateDbPath() {
5978
- const base = process.env.NIGHT_WATCH_HOME || path15.join(os5.homedir(), GLOBAL_CONFIG_DIR);
5979
- return path15.join(base, STATE_DB_FILE_NAME);
6086
+ const base = process.env.NIGHT_WATCH_HOME || path16.join(os5.homedir(), GLOBAL_CONFIG_DIR);
6087
+ return path16.join(base, STATE_DB_FILE_NAME);
5980
6088
  }
5981
6089
  function getQueueLockPath() {
5982
- const base = process.env.NIGHT_WATCH_HOME || path15.join(os5.homedir(), GLOBAL_CONFIG_DIR);
5983
- return path15.join(base, QUEUE_LOCK_FILE_NAME);
6090
+ const base = process.env.NIGHT_WATCH_HOME || path16.join(os5.homedir(), GLOBAL_CONFIG_DIR);
6091
+ return path16.join(base, QUEUE_LOCK_FILE_NAME);
5984
6092
  }
5985
6093
  function openDb() {
5986
6094
  const dbPath = getStateDbPath();
@@ -6002,6 +6110,54 @@ function rowToEntry(row) {
6002
6110
  expiredAt: row.expired_at
6003
6111
  };
6004
6112
  }
6113
+ function getProjectSchedulingPriority(projectPath, cache) {
6114
+ if (cache.has(projectPath)) {
6115
+ return cache.get(projectPath);
6116
+ }
6117
+ let priority = 3;
6118
+ try {
6119
+ priority = normalizeSchedulingPriority(loadConfig(projectPath).schedulingPriority);
6120
+ } catch {
6121
+ priority = 3;
6122
+ }
6123
+ cache.set(projectPath, priority);
6124
+ return priority;
6125
+ }
6126
+ function selectNextPendingEntry(db) {
6127
+ const rows = db.prepare(`SELECT * FROM job_queue
6128
+ WHERE status = 'pending'
6129
+ ORDER BY priority DESC, enqueued_at ASC, id ASC`).all();
6130
+ if (rows.length === 0) {
6131
+ return null;
6132
+ }
6133
+ const headByProject = /* @__PURE__ */ new Map();
6134
+ for (const row of rows) {
6135
+ const entry = rowToEntry(row);
6136
+ if (!headByProject.has(entry.projectPath)) {
6137
+ headByProject.set(entry.projectPath, entry);
6138
+ }
6139
+ }
6140
+ const priorityCache = /* @__PURE__ */ new Map();
6141
+ const candidates = Array.from(headByProject.values());
6142
+ candidates.sort((left, right) => {
6143
+ if (left.priority !== right.priority) {
6144
+ return right.priority - left.priority;
6145
+ }
6146
+ const leftProjectPriority = getProjectSchedulingPriority(left.projectPath, priorityCache);
6147
+ const rightProjectPriority = getProjectSchedulingPriority(right.projectPath, priorityCache);
6148
+ if (leftProjectPriority !== rightProjectPriority) {
6149
+ return rightProjectPriority - leftProjectPriority;
6150
+ }
6151
+ if (left.enqueuedAt !== right.enqueuedAt) {
6152
+ return left.enqueuedAt - right.enqueuedAt;
6153
+ }
6154
+ if (left.projectName !== right.projectName) {
6155
+ return left.projectName.localeCompare(right.projectName);
6156
+ }
6157
+ return left.id - right.id;
6158
+ });
6159
+ return candidates[0] ?? null;
6160
+ }
6005
6161
  function getJobPriority(jobType, config) {
6006
6162
  const priorityMap = config?.priority ?? DEFAULT_QUEUE_PRIORITY;
6007
6163
  return priorityMap[jobType] ?? 0;
@@ -6047,33 +6203,38 @@ function removeJob(queueId) {
6047
6203
  function getNextPendingJob() {
6048
6204
  const db = openDb();
6049
6205
  try {
6050
- const row = db.prepare(`SELECT * FROM job_queue
6051
- WHERE status = 'pending'
6052
- ORDER BY priority DESC, enqueued_at ASC
6053
- LIMIT 1`).get();
6054
- return row ? rowToEntry(row) : null;
6206
+ return selectNextPendingEntry(db);
6055
6207
  } finally {
6056
6208
  db.close();
6057
6209
  }
6058
6210
  }
6211
+ function getInFlightCount() {
6212
+ const db = openDb();
6213
+ try {
6214
+ const running = db.prepare(`SELECT COUNT(*) as count FROM job_queue WHERE status IN ('running', 'dispatched')`).get();
6215
+ return running?.count ?? 0;
6216
+ } finally {
6217
+ db.close();
6218
+ }
6219
+ }
6220
+ function canStartJob(config) {
6221
+ const maxConcurrency = config?.maxConcurrency ?? 1;
6222
+ return getInFlightCount() < maxConcurrency;
6223
+ }
6059
6224
  function dispatchNextJob(config) {
6060
6225
  expireStaleJobs(config?.maxWaitTime ?? DEFAULT_QUEUE_MAX_WAIT_TIME);
6061
6226
  const db = openDb();
6062
6227
  try {
6228
+ const maxConcurrency = config?.maxConcurrency ?? 1;
6063
6229
  const running = db.prepare(`SELECT COUNT(*) as count FROM job_queue WHERE status IN ('running', 'dispatched')`).get();
6064
6230
  const runningCount = running?.count ?? 0;
6065
- const maxConcurrency = config?.maxConcurrency ?? 1;
6066
6231
  if (runningCount >= maxConcurrency) {
6067
6232
  return null;
6068
6233
  }
6069
- const row = db.prepare(`SELECT * FROM job_queue
6070
- WHERE status = 'pending'
6071
- ORDER BY priority DESC, enqueued_at ASC
6072
- LIMIT 1`).get();
6073
- if (!row) {
6234
+ const entry = selectNextPendingEntry(db);
6235
+ if (!entry) {
6074
6236
  return null;
6075
6237
  }
6076
- const entry = rowToEntry(row);
6077
6238
  const now = Math.floor(Date.now() / 1e3);
6078
6239
  db.prepare(`UPDATE job_queue SET status = 'dispatched', dispatched_at = ? WHERE id = ?`).run(now, entry.id);
6079
6240
  return { ...entry, status: "dispatched", dispatchedAt: now };
@@ -6165,7 +6326,9 @@ function updateJobStatus(id, status) {
6165
6326
  var init_job_queue = __esm({
6166
6327
  "../core/dist/utils/job-queue.js"() {
6167
6328
  "use strict";
6329
+ init_config();
6168
6330
  init_constants();
6331
+ init_scheduling();
6169
6332
  }
6170
6333
  });
6171
6334
  function renderDependsOn(deps) {
@@ -6394,6 +6557,7 @@ __export(dist_exports, {
6394
6557
  DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
6395
6558
  DEFAULT_REVIEWER_SCHEDULE: () => DEFAULT_REVIEWER_SCHEDULE,
6396
6559
  DEFAULT_ROADMAP_SCANNER: () => DEFAULT_ROADMAP_SCANNER,
6560
+ DEFAULT_SCHEDULING_PRIORITY: () => DEFAULT_SCHEDULING_PRIORITY,
6397
6561
  DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
6398
6562
  DEFAULT_SLICER_SCHEDULE: () => DEFAULT_SLICER_SCHEDULE,
6399
6563
  DEFAULT_TEMPLATES_DIR: () => DEFAULT_TEMPLATES_DIR,
@@ -6429,10 +6593,12 @@ __export(dist_exports, {
6429
6593
  VALID_MERGE_METHODS: () => VALID_MERGE_METHODS,
6430
6594
  VALID_PROVIDERS: () => VALID_PROVIDERS,
6431
6595
  acquireLock: () => acquireLock,
6596
+ addDelayToIsoString: () => addDelayToIsoString,
6432
6597
  addEntry: () => addEntry,
6433
6598
  auditLockPath: () => auditLockPath,
6434
6599
  buildDescription: () => buildDescription,
6435
6600
  calculateStringSimilarity: () => calculateStringSimilarity,
6601
+ canStartJob: () => canStartJob,
6436
6602
  cancelProcess: () => cancelProcess,
6437
6603
  checkConfigFile: () => checkConfigFile,
6438
6604
  checkCrontabAccess: () => checkCrontabAccess,
@@ -6506,6 +6672,7 @@ __export(dist_exports, {
6506
6672
  getEventEmoji: () => getEventEmoji,
6507
6673
  getEventTitle: () => getEventTitle,
6508
6674
  getHistoryPath: () => getHistoryPath,
6675
+ getInFlightCount: () => getInFlightCount,
6509
6676
  getJobPriority: () => getJobPriority,
6510
6677
  getLabelsForSection: () => getLabelsForSection,
6511
6678
  getLastExecution: () => getLastExecution,
@@ -6526,6 +6693,7 @@ __export(dist_exports, {
6526
6693
  getRepositories: () => getRepositories,
6527
6694
  getRoadmapStatus: () => getRoadmapStatus,
6528
6695
  getRunningJob: () => getRunningJob,
6696
+ getSchedulingPlan: () => getSchedulingPlan,
6529
6697
  getScriptPath: () => getScriptPath,
6530
6698
  getStateFilePath: () => getStateFilePath,
6531
6699
  getStateItem: () => getStateItem,
@@ -6539,6 +6707,7 @@ __export(dist_exports, {
6539
6707
  isContainerInitialized: () => isContainerInitialized,
6540
6708
  isInCooldown: () => isInCooldown,
6541
6709
  isItemProcessed: () => isItemProcessed,
6710
+ isJobTypeEnabled: () => isJobTypeEnabled,
6542
6711
  isProcessRunning: () => isProcessRunning,
6543
6712
  isValidCategory: () => isValidCategory,
6544
6713
  isValidHorizon: () => isValidHorizon,
@@ -6554,6 +6723,7 @@ __export(dist_exports, {
6554
6723
  markJobRunning: () => markJobRunning,
6555
6724
  markPrdDone: () => markPrdDone,
6556
6725
  migrateJsonToSqlite: () => migrateJsonToSqlite,
6726
+ normalizeSchedulingPriority: () => normalizeSchedulingPriority,
6557
6727
  parsePrdDependencies: () => parsePrdDependencies,
6558
6728
  parseRoadmap: () => parseRoadmap,
6559
6729
  parseScriptResult: () => parseScriptResult,
@@ -6641,6 +6811,7 @@ var init_dist = __esm({
6641
6811
  init_roadmap_state();
6642
6812
  init_script_result();
6643
6813
  init_shell();
6814
+ init_scheduling();
6644
6815
  init_status_data();
6645
6816
  init_ui();
6646
6817
  init_webhook_validator();
@@ -6656,22 +6827,22 @@ var __dirname2 = dirname4(__filename);
6656
6827
  function findTemplatesDir(startDir) {
6657
6828
  let d = startDir;
6658
6829
  for (let i = 0; i < 8; i++) {
6659
- const candidate = join15(d, "templates");
6660
- if (fs16.existsSync(candidate) && fs16.statSync(candidate).isDirectory()) {
6830
+ const candidate = join16(d, "templates");
6831
+ if (fs17.existsSync(candidate) && fs17.statSync(candidate).isDirectory()) {
6661
6832
  return candidate;
6662
6833
  }
6663
6834
  d = dirname4(d);
6664
6835
  }
6665
- return join15(startDir, "templates");
6836
+ return join16(startDir, "templates");
6666
6837
  }
6667
6838
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
6668
6839
  function hasPlaywrightDependency(cwd) {
6669
- const packageJsonPath = path16.join(cwd, "package.json");
6670
- if (!fs16.existsSync(packageJsonPath)) {
6840
+ const packageJsonPath = path17.join(cwd, "package.json");
6841
+ if (!fs17.existsSync(packageJsonPath)) {
6671
6842
  return false;
6672
6843
  }
6673
6844
  try {
6674
- const packageJson2 = JSON.parse(fs16.readFileSync(packageJsonPath, "utf-8"));
6845
+ const packageJson2 = JSON.parse(fs17.readFileSync(packageJsonPath, "utf-8"));
6675
6846
  return Boolean(packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright);
6676
6847
  } catch {
6677
6848
  return false;
@@ -6681,7 +6852,7 @@ function detectPlaywright(cwd) {
6681
6852
  if (hasPlaywrightDependency(cwd)) {
6682
6853
  return true;
6683
6854
  }
6684
- if (fs16.existsSync(path16.join(cwd, "node_modules", ".bin", "playwright"))) {
6855
+ if (fs17.existsSync(path17.join(cwd, "node_modules", ".bin", "playwright"))) {
6685
6856
  return true;
6686
6857
  }
6687
6858
  try {
@@ -6697,10 +6868,10 @@ function detectPlaywright(cwd) {
6697
6868
  }
6698
6869
  }
6699
6870
  function resolvePlaywrightInstallCommand(cwd) {
6700
- if (fs16.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) {
6871
+ if (fs17.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) {
6701
6872
  return "pnpm add -D @playwright/test";
6702
6873
  }
6703
- if (fs16.existsSync(path16.join(cwd, "yarn.lock"))) {
6874
+ if (fs17.existsSync(path17.join(cwd, "yarn.lock"))) {
6704
6875
  return "yarn add -D @playwright/test";
6705
6876
  }
6706
6877
  return "npm install -D @playwright/test";
@@ -6709,7 +6880,7 @@ function promptYesNo(question, defaultNo = true) {
6709
6880
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
6710
6881
  return Promise.resolve(false);
6711
6882
  }
6712
- return new Promise((resolve9) => {
6883
+ return new Promise((resolve10) => {
6713
6884
  const rl = readline.createInterface({
6714
6885
  input: process.stdin,
6715
6886
  output: process.stdout
@@ -6719,10 +6890,10 @@ function promptYesNo(question, defaultNo = true) {
6719
6890
  rl.close();
6720
6891
  const normalized = answer.trim().toLowerCase();
6721
6892
  if (normalized === "") {
6722
- resolve9(!defaultNo);
6893
+ resolve10(!defaultNo);
6723
6894
  return;
6724
6895
  }
6725
- resolve9(normalized === "y" || normalized === "yes");
6896
+ resolve10(normalized === "y" || normalized === "yes");
6726
6897
  });
6727
6898
  });
6728
6899
  }
@@ -6798,7 +6969,7 @@ function getDefaultBranch(cwd) {
6798
6969
  }
6799
6970
  }
6800
6971
  function promptProviderSelection(providers) {
6801
- return new Promise((resolve9, reject) => {
6972
+ return new Promise((resolve10, reject) => {
6802
6973
  const rl = readline.createInterface({
6803
6974
  input: process.stdin,
6804
6975
  output: process.stdout
@@ -6814,41 +6985,91 @@ function promptProviderSelection(providers) {
6814
6985
  reject(new Error("Invalid selection. Please run init again and select a valid number."));
6815
6986
  return;
6816
6987
  }
6817
- resolve9(providers[selection - 1]);
6988
+ resolve10(providers[selection - 1]);
6818
6989
  });
6819
6990
  });
6820
6991
  }
6821
6992
  function ensureDir(dirPath) {
6822
- if (!fs16.existsSync(dirPath)) {
6823
- fs16.mkdirSync(dirPath, { recursive: true });
6993
+ if (!fs17.existsSync(dirPath)) {
6994
+ fs17.mkdirSync(dirPath, { recursive: true });
6824
6995
  }
6825
6996
  }
6997
+ function buildInitConfig(params) {
6998
+ const defaults = getDefaultConfig();
6999
+ return {
7000
+ $schema: "https://json-schema.org/schema",
7001
+ projectName: params.projectName,
7002
+ defaultBranch: params.defaultBranch,
7003
+ prdDir: params.prdDir,
7004
+ maxRuntime: defaults.maxRuntime,
7005
+ reviewerMaxRuntime: defaults.reviewerMaxRuntime,
7006
+ branchPrefix: defaults.branchPrefix,
7007
+ branchPatterns: [...defaults.branchPatterns],
7008
+ minReviewScore: defaults.minReviewScore,
7009
+ maxLogSize: defaults.maxLogSize,
7010
+ cronSchedule: defaults.cronSchedule,
7011
+ reviewerSchedule: defaults.reviewerSchedule,
7012
+ scheduleBundleId: defaults.scheduleBundleId ?? null,
7013
+ cronScheduleOffset: defaults.cronScheduleOffset,
7014
+ schedulingPriority: defaults.schedulingPriority,
7015
+ maxRetries: defaults.maxRetries,
7016
+ reviewerMaxRetries: defaults.reviewerMaxRetries,
7017
+ reviewerRetryDelay: defaults.reviewerRetryDelay,
7018
+ provider: params.provider,
7019
+ providerLabel: "",
7020
+ executorEnabled: defaults.executorEnabled ?? true,
7021
+ reviewerEnabled: params.reviewerEnabled,
7022
+ providerEnv: { ...defaults.providerEnv },
7023
+ notifications: {
7024
+ ...defaults.notifications,
7025
+ webhooks: [...defaults.notifications?.webhooks ?? []]
7026
+ },
7027
+ prdPriority: [...defaults.prdPriority],
7028
+ roadmapScanner: { ...defaults.roadmapScanner },
7029
+ templatesDir: defaults.templatesDir,
7030
+ boardProvider: { ...defaults.boardProvider },
7031
+ autoMerge: defaults.autoMerge,
7032
+ autoMergeMethod: defaults.autoMergeMethod,
7033
+ fallbackOnRateLimit: defaults.fallbackOnRateLimit,
7034
+ claudeModel: defaults.claudeModel,
7035
+ qa: {
7036
+ ...defaults.qa,
7037
+ branchPatterns: [...defaults.qa.branchPatterns]
7038
+ },
7039
+ audit: { ...defaults.audit },
7040
+ jobProviders: { ...defaults.jobProviders },
7041
+ queue: {
7042
+ ...defaults.queue,
7043
+ priority: { ...defaults.queue.priority }
7044
+ }
7045
+ };
7046
+ }
6826
7047
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
6827
7048
  if (customTemplatesDir !== null) {
6828
- const customPath = join15(customTemplatesDir, templateName);
6829
- if (fs16.existsSync(customPath)) {
7049
+ const customPath = join16(customTemplatesDir, templateName);
7050
+ if (fs17.existsSync(customPath)) {
6830
7051
  return { path: customPath, source: "custom" };
6831
7052
  }
6832
7053
  }
6833
- return { path: join15(bundledTemplatesDir, templateName), source: "bundled" };
7054
+ return { path: join16(bundledTemplatesDir, templateName), source: "bundled" };
6834
7055
  }
6835
7056
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
6836
- if (fs16.existsSync(targetPath) && !force) {
7057
+ if (fs17.existsSync(targetPath) && !force) {
6837
7058
  console.log(` Skipped (exists): ${targetPath}`);
6838
7059
  return { created: false, source: source ?? "bundled" };
6839
7060
  }
6840
- const templatePath = sourcePath ?? join15(TEMPLATES_DIR, templateName);
7061
+ const templatePath = sourcePath ?? join16(TEMPLATES_DIR, templateName);
6841
7062
  const resolvedSource = source ?? "bundled";
6842
- let content = fs16.readFileSync(templatePath, "utf-8");
7063
+ let content = fs17.readFileSync(templatePath, "utf-8");
6843
7064
  for (const [key, value] of Object.entries(replacements)) {
6844
7065
  content = content.replaceAll(key, value);
6845
7066
  }
6846
- fs16.writeFileSync(targetPath, content);
7067
+ fs17.writeFileSync(targetPath, content);
6847
7068
  console.log(` Created: ${targetPath} (${resolvedSource})`);
6848
7069
  return { created: true, source: resolvedSource };
6849
7070
  }
6850
7071
  function addToGitignore(cwd) {
6851
- const gitignorePath = path16.join(cwd, ".gitignore");
7072
+ const gitignorePath = path17.join(cwd, ".gitignore");
6852
7073
  const entries = [
6853
7074
  {
6854
7075
  pattern: "/logs/",
@@ -6862,13 +7083,13 @@ function addToGitignore(cwd) {
6862
7083
  },
6863
7084
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
6864
7085
  ];
6865
- if (!fs16.existsSync(gitignorePath)) {
7086
+ if (!fs17.existsSync(gitignorePath)) {
6866
7087
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
6867
- fs16.writeFileSync(gitignorePath, lines.join("\n"));
7088
+ fs17.writeFileSync(gitignorePath, lines.join("\n"));
6868
7089
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
6869
7090
  return;
6870
7091
  }
6871
- const content = fs16.readFileSync(gitignorePath, "utf-8");
7092
+ const content = fs17.readFileSync(gitignorePath, "utf-8");
6872
7093
  const missing = entries.filter((e) => !e.check(content));
6873
7094
  if (missing.length === 0) {
6874
7095
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -6876,7 +7097,7 @@ function addToGitignore(cwd) {
6876
7097
  }
6877
7098
  const additions = missing.map((e) => e.pattern).join("\n");
6878
7099
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
6879
- fs16.writeFileSync(gitignorePath, newContent);
7100
+ fs17.writeFileSync(gitignorePath, newContent);
6880
7101
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
6881
7102
  }
6882
7103
  function initCommand(program2) {
@@ -6965,54 +7186,56 @@ function initCommand(program2) {
6965
7186
  "${DEFAULT_BRANCH}": defaultBranch
6966
7187
  };
6967
7188
  step(5, totalSteps, "Creating PRD directory structure...");
6968
- const prdDirPath = path16.join(cwd, prdDir);
6969
- const doneDirPath = path16.join(prdDirPath, "done");
7189
+ const prdDirPath = path17.join(cwd, prdDir);
7190
+ const doneDirPath = path17.join(prdDirPath, "done");
6970
7191
  ensureDir(doneDirPath);
6971
7192
  success(`Created ${prdDirPath}/`);
6972
7193
  success(`Created ${doneDirPath}/`);
6973
7194
  step(6, totalSteps, "Creating logs directory...");
6974
- const logsPath = path16.join(cwd, LOG_DIR);
7195
+ const logsPath = path17.join(cwd, LOG_DIR);
6975
7196
  ensureDir(logsPath);
6976
7197
  success(`Created ${logsPath}/`);
6977
7198
  addToGitignore(cwd);
6978
7199
  step(7, totalSteps, "Creating instructions directory...");
6979
- const instructionsDir = path16.join(cwd, "instructions");
7200
+ const instructionsDir = path17.join(cwd, "instructions");
6980
7201
  ensureDir(instructionsDir);
6981
7202
  success(`Created ${instructionsDir}/`);
6982
7203
  const existingConfig = loadConfig(cwd);
6983
- const customTemplatesDirPath = path16.join(cwd, existingConfig.templatesDir);
6984
- const customTemplatesDir = fs16.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7204
+ const customTemplatesDirPath = path17.join(cwd, existingConfig.templatesDir);
7205
+ const customTemplatesDir = fs17.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
6985
7206
  const templateSources = [];
6986
7207
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
6987
- const nwResult = processTemplate("executor.md", path16.join(instructionsDir, "executor.md"), replacements, force, nwResolution.path, nwResolution.source);
7208
+ const nwResult = processTemplate("executor.md", path17.join(instructionsDir, "executor.md"), replacements, force, nwResolution.path, nwResolution.source);
6988
7209
  templateSources.push({ name: "executor.md", source: nwResult.source });
6989
7210
  const peResolution = resolveTemplatePath("prd-executor.md", customTemplatesDir, TEMPLATES_DIR);
6990
- const peResult = processTemplate("prd-executor.md", path16.join(instructionsDir, "prd-executor.md"), replacements, force, peResolution.path, peResolution.source);
7211
+ const peResult = processTemplate("prd-executor.md", path17.join(instructionsDir, "prd-executor.md"), replacements, force, peResolution.path, peResolution.source);
6991
7212
  templateSources.push({ name: "prd-executor.md", source: peResult.source });
6992
7213
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
6993
- const prResult = processTemplate("pr-reviewer.md", path16.join(instructionsDir, "pr-reviewer.md"), replacements, force, prResolution.path, prResolution.source);
7214
+ const prResult = processTemplate("pr-reviewer.md", path17.join(instructionsDir, "pr-reviewer.md"), replacements, force, prResolution.path, prResolution.source);
6994
7215
  templateSources.push({ name: "pr-reviewer.md", source: prResult.source });
6995
7216
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
6996
- const qaResult = processTemplate("qa.md", path16.join(instructionsDir, "qa.md"), replacements, force, qaResolution.path, qaResolution.source);
7217
+ const qaResult = processTemplate("qa.md", path17.join(instructionsDir, "qa.md"), replacements, force, qaResolution.path, qaResolution.source);
6997
7218
  templateSources.push({ name: "qa.md", source: qaResult.source });
6998
7219
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
6999
- const auditResult = processTemplate("audit.md", path16.join(instructionsDir, "audit.md"), replacements, force, auditResolution.path, auditResolution.source);
7220
+ const auditResult = processTemplate("audit.md", path17.join(instructionsDir, "audit.md"), replacements, force, auditResolution.path, auditResolution.source);
7000
7221
  templateSources.push({ name: "audit.md", source: auditResult.source });
7001
7222
  step(8, totalSteps, "Creating configuration file...");
7002
- const configPath = path16.join(cwd, CONFIG_FILE_NAME);
7003
- if (fs16.existsSync(configPath) && !force) {
7223
+ const configPath = path17.join(cwd, CONFIG_FILE_NAME);
7224
+ if (fs17.existsSync(configPath) && !force) {
7004
7225
  console.log(` Skipped (exists): ${configPath}`);
7005
7226
  } else {
7006
- let configContent = fs16.readFileSync(join15(TEMPLATES_DIR, "night-watch.config.json"), "utf-8");
7007
- configContent = configContent.replace('"projectName": ""', `"projectName": "${projectName}"`);
7008
- configContent = configContent.replace('"defaultBranch": ""', `"defaultBranch": "${defaultBranch}"`);
7009
- configContent = configContent.replace(/"provider":\s*"[^"]*"/, `"provider": "${selectedProvider}"`);
7010
- configContent = configContent.replace(/"reviewerEnabled":\s*(true|false)/, `"reviewerEnabled": ${reviewerEnabled}`);
7011
- fs16.writeFileSync(configPath, configContent);
7227
+ const config = buildInitConfig({
7228
+ projectName,
7229
+ defaultBranch,
7230
+ provider: selectedProvider,
7231
+ reviewerEnabled,
7232
+ prdDir
7233
+ });
7234
+ fs17.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7012
7235
  success(`Created ${configPath}`);
7013
7236
  }
7014
7237
  step(9, totalSteps, "Setting up GitHub Project board...");
7015
- const existingRaw = JSON.parse(fs16.readFileSync(configPath, "utf-8"));
7238
+ const existingRaw = JSON.parse(fs17.readFileSync(configPath, "utf-8"));
7016
7239
  const existingBoard = existingRaw.boardProvider;
7017
7240
  if (existingBoard?.projectNumber && !force) {
7018
7241
  info(`Board already configured (#${existingBoard.projectNumber}), skipping.`);
@@ -7034,13 +7257,13 @@ function initCommand(program2) {
7034
7257
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
7035
7258
  const boardTitle = `${projectName} Night Watch`;
7036
7259
  const board = await provider.setupBoard(boardTitle);
7037
- const rawConfig = JSON.parse(fs16.readFileSync(configPath, "utf-8"));
7260
+ const rawConfig = JSON.parse(fs17.readFileSync(configPath, "utf-8"));
7038
7261
  rawConfig.boardProvider = {
7039
7262
  enabled: true,
7040
7263
  provider: "github",
7041
7264
  projectNumber: board.number
7042
7265
  };
7043
- fs16.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7266
+ fs17.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7044
7267
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
7045
7268
  } catch (boardErr) {
7046
7269
  console.warn(` Warning: Could not set up GitHub Project board: ${boardErr instanceof Error ? boardErr.message : String(boardErr)}`);
@@ -7107,12 +7330,23 @@ function buildBaseEnvVars(config, jobType, isDryRun) {
7107
7330
  env.NW_QUEUE_MAX_CONCURRENCY = String(queueConfig.maxConcurrency);
7108
7331
  env.NW_QUEUE_MAX_WAIT_TIME = String(queueConfig.maxWaitTime);
7109
7332
  env.NW_QUEUE_PRIORITY_JSON = JSON.stringify(queueConfig.priority);
7333
+ env.NW_SCHEDULING_PRIORITY = String(config.schedulingPriority ?? 3);
7110
7334
  if (isDryRun) {
7111
7335
  env.NW_DRY_RUN = "1";
7112
7336
  }
7113
7337
  env.NW_EXECUTION_CONTEXT = "agent";
7114
7338
  return env;
7115
7339
  }
7340
+ async function maybeApplyCronSchedulingDelay(config, jobType, projectDir) {
7341
+ const plan = getSchedulingPlan(projectDir, config, jobType);
7342
+ if (process.env.NW_CRON_TRIGGER !== "1" || process.env.NW_QUEUE_DISPATCHED === "1") {
7343
+ return plan;
7344
+ }
7345
+ if (plan.totalDelayMinutes > 0) {
7346
+ await new Promise((resolve10) => setTimeout(resolve10, plan.totalDelayMinutes * 6e4));
7347
+ }
7348
+ return plan;
7349
+ }
7116
7350
  function formatProviderDisplay(providerCmd, providerLabel) {
7117
7351
  const cmd = providerCmd?.trim();
7118
7352
  if (!cmd)
@@ -7155,12 +7389,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7155
7389
  return scriptStatus === "skip_no_eligible_prd";
7156
7390
  }
7157
7391
  function getCrossProjectFallbackCandidates(currentProjectDir) {
7158
- const current = path17.resolve(currentProjectDir);
7392
+ const current = path18.resolve(currentProjectDir);
7159
7393
  const { valid, invalid } = validateRegistry();
7160
7394
  for (const entry of invalid) {
7161
7395
  warn(`Skipping invalid registry entry: ${entry.path}`);
7162
7396
  }
7163
- return valid.filter((entry) => path17.resolve(entry.path) !== current);
7397
+ return valid.filter((entry) => path18.resolve(entry.path) !== current);
7164
7398
  }
7165
7399
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
7166
7400
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -7168,7 +7402,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7168
7402
  if (nonTelegramWebhooks.length > 0) {
7169
7403
  const _rateLimitCtx = {
7170
7404
  event: "rate_limit_fallback",
7171
- projectName: path17.basename(projectDir),
7405
+ projectName: path18.basename(projectDir),
7172
7406
  exitCode,
7173
7407
  provider: config.provider
7174
7408
  };
@@ -7193,7 +7427,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7193
7427
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
7194
7428
  const _ctx = {
7195
7429
  event,
7196
- projectName: path17.basename(projectDir),
7430
+ projectName: path18.basename(projectDir),
7197
7431
  exitCode,
7198
7432
  provider: config.provider,
7199
7433
  prdName: scriptResult?.data.prd,
@@ -7294,20 +7528,20 @@ function applyCliOverrides(config, options) {
7294
7528
  return overridden;
7295
7529
  }
7296
7530
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7297
- const absolutePrdDir = path17.join(projectDir, prdDir);
7298
- const doneDir = path17.join(absolutePrdDir, "done");
7531
+ const absolutePrdDir = path18.join(projectDir, prdDir);
7532
+ const doneDir = path18.join(absolutePrdDir, "done");
7299
7533
  const pending = [];
7300
7534
  const completed = [];
7301
- if (fs17.existsSync(absolutePrdDir)) {
7302
- const entries = fs17.readdirSync(absolutePrdDir, { withFileTypes: true });
7535
+ if (fs18.existsSync(absolutePrdDir)) {
7536
+ const entries = fs18.readdirSync(absolutePrdDir, { withFileTypes: true });
7303
7537
  for (const entry of entries) {
7304
7538
  if (entry.isFile() && entry.name.endsWith(".md")) {
7305
- const claimPath = path17.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
7539
+ const claimPath = path18.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
7306
7540
  let claimed = false;
7307
7541
  let claimInfo = null;
7308
- if (fs17.existsSync(claimPath)) {
7542
+ if (fs18.existsSync(claimPath)) {
7309
7543
  try {
7310
- const content = fs17.readFileSync(claimPath, "utf-8");
7544
+ const content = fs18.readFileSync(claimPath, "utf-8");
7311
7545
  const data = JSON.parse(content);
7312
7546
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
7313
7547
  if (age < maxRuntime) {
@@ -7321,8 +7555,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7321
7555
  }
7322
7556
  }
7323
7557
  }
7324
- if (fs17.existsSync(doneDir)) {
7325
- const entries = fs17.readdirSync(doneDir, { withFileTypes: true });
7558
+ if (fs18.existsSync(doneDir)) {
7559
+ const entries = fs18.readdirSync(doneDir, { withFileTypes: true });
7326
7560
  for (const entry of entries) {
7327
7561
  if (entry.isFile() && entry.name.endsWith(".md")) {
7328
7562
  completed.push(entry.name);
@@ -7431,6 +7665,7 @@ function runCommand(program2) {
7431
7665
  const spinner = createSpinner("Running PRD executor...");
7432
7666
  spinner.start();
7433
7667
  try {
7668
+ await maybeApplyCronSchedulingDelay(config, "executor", projectDir);
7434
7669
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars, { cwd: projectDir });
7435
7670
  const scriptResult = parseScriptResult(`${stdout}
7436
7671
  ${stderr}`);
@@ -7648,6 +7883,7 @@ function reviewCommand(program2) {
7648
7883
  const spinner = createSpinner("Running PR reviewer...");
7649
7884
  spinner.start();
7650
7885
  try {
7886
+ await maybeApplyCronSchedulingDelay(config, "reviewer", projectDir);
7651
7887
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
7652
7888
  const scriptResult = parseScriptResult(`${stdout}
7653
7889
  ${stderr}`);
@@ -7686,7 +7922,7 @@ ${stderr}`);
7686
7922
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
7687
7923
  const _reviewCtx = {
7688
7924
  event: "review_completed",
7689
- projectName: path18.basename(projectDir),
7925
+ projectName: path19.basename(projectDir),
7690
7926
  exitCode,
7691
7927
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
7692
7928
  prUrl: prDetails?.url,
@@ -7707,7 +7943,7 @@ ${stderr}`);
7707
7943
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
7708
7944
  const _mergeCtx = {
7709
7945
  event: "pr_auto_merged",
7710
- projectName: path18.basename(projectDir),
7946
+ projectName: path19.basename(projectDir),
7711
7947
  exitCode,
7712
7948
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
7713
7949
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -7831,6 +8067,7 @@ function qaCommand(program2) {
7831
8067
  const spinner = createSpinner("Running QA process...");
7832
8068
  spinner.start();
7833
8069
  try {
8070
+ await maybeApplyCronSchedulingDelay(config, "qa", projectDir);
7834
8071
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
7835
8072
  const scriptResult = parseScriptResult(`${stdout}
7836
8073
  ${stderr}`);
@@ -7859,7 +8096,7 @@ ${stderr}`);
7859
8096
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
7860
8097
  const _qaCtx = {
7861
8098
  event: "qa_completed",
7862
- projectName: path19.basename(projectDir),
8099
+ projectName: path20.basename(projectDir),
7863
8100
  exitCode,
7864
8101
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
7865
8102
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -7925,7 +8162,7 @@ function auditCommand(program2) {
7925
8162
  configTable.push(["Provider", auditProvider]);
7926
8163
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
7927
8164
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
7928
- configTable.push(["Report File", path20.join(projectDir, "logs", "audit-report.md")]);
8165
+ configTable.push(["Report File", path21.join(projectDir, "logs", "audit-report.md")]);
7929
8166
  console.log(configTable.toString());
7930
8167
  header("Provider Invocation");
7931
8168
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -7942,6 +8179,7 @@ function auditCommand(program2) {
7942
8179
  const spinner = createSpinner("Running code audit...");
7943
8180
  spinner.start();
7944
8181
  try {
8182
+ await maybeApplyCronSchedulingDelay(config, "audit", projectDir);
7945
8183
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
7946
8184
  const scriptResult = parseScriptResult(`${stdout}
7947
8185
  ${stderr}`);
@@ -7953,8 +8191,8 @@ ${stderr}`);
7953
8191
  } else if (scriptResult?.status?.startsWith("skip_")) {
7954
8192
  spinner.succeed("Code audit skipped");
7955
8193
  } else {
7956
- const reportPath = path20.join(projectDir, "logs", "audit-report.md");
7957
- if (!fs18.existsSync(reportPath)) {
8194
+ const reportPath = path21.join(projectDir, "logs", "audit-report.md");
8195
+ if (!fs19.existsSync(reportPath)) {
7958
8196
  spinner.fail("Code audit finished without a report file");
7959
8197
  process.exit(1);
7960
8198
  }
@@ -7965,9 +8203,9 @@ ${stderr}`);
7965
8203
  const providerExit = scriptResult?.data?.provider_exit;
7966
8204
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
7967
8205
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
7968
- const logPath = path20.join(projectDir, "logs", "audit.log");
7969
- if (fs18.existsSync(logPath)) {
7970
- const logLines = fs18.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
8206
+ const logPath = path21.join(projectDir, "logs", "audit.log");
8207
+ if (fs19.existsSync(logPath)) {
8208
+ const logLines = fs19.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
7971
8209
  if (logLines.length > 0) {
7972
8210
  process.stderr.write(logLines.join("\n") + "\n");
7973
8211
  }
@@ -7987,8 +8225,8 @@ function shellQuote(value) {
7987
8225
  function getNightWatchBinPath() {
7988
8226
  try {
7989
8227
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
7990
- const binPath = path21.join(npmBin, "night-watch");
7991
- if (fs19.existsSync(binPath)) {
8228
+ const binPath = path22.join(npmBin, "night-watch");
8229
+ if (fs20.existsSync(binPath)) {
7992
8230
  return binPath;
7993
8231
  }
7994
8232
  } catch {
@@ -8002,45 +8240,32 @@ function getNightWatchBinPath() {
8002
8240
  function getNodeBinDir() {
8003
8241
  try {
8004
8242
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
8005
- return path21.dirname(nodePath);
8243
+ return path22.dirname(nodePath);
8006
8244
  } catch {
8007
8245
  return "";
8008
8246
  }
8009
8247
  }
8010
8248
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
8011
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path21.dirname(nightWatchBin) : "";
8249
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path22.dirname(nightWatchBin) : "";
8012
8250
  const pathParts = Array.from(new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0)));
8013
8251
  if (pathParts.length === 0) {
8014
8252
  return "";
8015
8253
  }
8016
8254
  return `export PATH="${pathParts.join(":")}:$PATH" && `;
8017
8255
  }
8018
- function applyScheduleOffset(schedule, offset) {
8019
- if (offset === 0)
8020
- return schedule;
8021
- const parts = schedule.split(/\s+/);
8022
- if (parts.length < 5)
8023
- return schedule;
8024
- if (/^\d+$/.test(parts[0])) {
8025
- parts[0] = String(offset);
8026
- return parts.join(" ");
8027
- }
8028
- return schedule;
8029
- }
8030
8256
  function performInstall(projectDir, config, options) {
8031
8257
  try {
8032
- const offset = config.cronScheduleOffset ?? 0;
8033
- const executorSchedule = applyScheduleOffset(options?.schedule || config.cronSchedule, offset);
8034
- const reviewerSchedule = applyScheduleOffset(options?.reviewerSchedule || config.reviewerSchedule, offset);
8258
+ const executorSchedule = options?.schedule || config.cronSchedule;
8259
+ const reviewerSchedule = options?.reviewerSchedule || config.reviewerSchedule;
8035
8260
  const nightWatchBin = getNightWatchBinPath();
8036
8261
  const projectName = getProjectName(projectDir);
8037
8262
  const marker = generateMarker(projectName);
8038
- const logDir = path21.join(projectDir, LOG_DIR);
8039
- if (!fs19.existsSync(logDir)) {
8040
- fs19.mkdirSync(logDir, { recursive: true });
8263
+ const logDir = path22.join(projectDir, LOG_DIR);
8264
+ if (!fs20.existsSync(logDir)) {
8265
+ fs20.mkdirSync(logDir, { recursive: true });
8041
8266
  }
8042
- const executorLog = path21.join(logDir, "executor.log");
8043
- const reviewerLog = path21.join(logDir, "reviewer.log");
8267
+ const executorLog = path22.join(logDir, "executor.log");
8268
+ const reviewerLog = path22.join(logDir, "reviewer.log");
8044
8269
  if (!options?.force) {
8045
8270
  const existingEntries2 = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
8046
8271
  if (existingEntries2.length > 0) {
@@ -8056,6 +8281,7 @@ function performInstall(projectDir, config, options) {
8056
8281
  const nodeBinDir = getNodeBinDir();
8057
8282
  const pathPrefix = buildCronPathPrefix(nodeBinDir, nightWatchBin);
8058
8283
  const cliBinPrefix = `export NW_CLI_BIN=${shellQuote(nightWatchBin)} && `;
8284
+ const cronTriggerPrefix = "export NW_CRON_TRIGGER=1 && ";
8059
8285
  let providerEnvPrefix = "";
8060
8286
  if (config.providerEnv && Object.keys(config.providerEnv).length > 0) {
8061
8287
  const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
@@ -8063,35 +8289,35 @@ function performInstall(projectDir, config, options) {
8063
8289
  }
8064
8290
  const installExecutor = config.executorEnabled !== false;
8065
8291
  if (installExecutor) {
8066
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
8292
+ const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
8067
8293
  entries.push(executorEntry);
8068
8294
  }
8069
8295
  const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
8070
8296
  if (installReviewer) {
8071
- const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
8297
+ const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
8072
8298
  entries.push(reviewerEntry);
8073
8299
  }
8074
8300
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
8075
8301
  if (installSlicer) {
8076
- const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
8077
- const slicerLog = path21.join(logDir, "slicer.log");
8078
- const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8302
+ const slicerSchedule = config.roadmapScanner.slicerSchedule;
8303
+ const slicerLog = path22.join(logDir, "slicer.log");
8304
+ const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8079
8305
  entries.push(slicerEntry);
8080
8306
  }
8081
8307
  const disableQa = options?.noQa === true || options?.qa === false;
8082
8308
  const installQa = disableQa ? false : config.qa.enabled;
8083
8309
  if (installQa) {
8084
- const qaSchedule = applyScheduleOffset(config.qa.schedule, offset);
8085
- const qaLog = path21.join(logDir, "qa.log");
8086
- const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8310
+ const qaSchedule = config.qa.schedule;
8311
+ const qaLog = path22.join(logDir, "qa.log");
8312
+ const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8087
8313
  entries.push(qaEntry);
8088
8314
  }
8089
8315
  const disableAudit = options?.noAudit === true || options?.audit === false;
8090
8316
  const installAudit = disableAudit ? false : config.audit.enabled;
8091
8317
  if (installAudit) {
8092
- const auditSchedule = applyScheduleOffset(config.audit.schedule, offset);
8093
- const auditLog = path21.join(logDir, "audit.log");
8094
- const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8318
+ const auditSchedule = config.audit.schedule;
8319
+ const auditLog = path22.join(logDir, "audit.log");
8320
+ const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8095
8321
  entries.push(auditEntry);
8096
8322
  }
8097
8323
  const existingEntries = new Set(Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])));
@@ -8114,18 +8340,17 @@ function installCommand(program2) {
8114
8340
  try {
8115
8341
  const projectDir = process.cwd();
8116
8342
  const config = loadConfig(projectDir);
8117
- const offset = config.cronScheduleOffset ?? 0;
8118
- const executorSchedule = applyScheduleOffset(options.schedule || config.cronSchedule, offset);
8119
- const reviewerSchedule = applyScheduleOffset(options.reviewerSchedule || config.reviewerSchedule, offset);
8343
+ const executorSchedule = options.schedule || config.cronSchedule;
8344
+ const reviewerSchedule = options.reviewerSchedule || config.reviewerSchedule;
8120
8345
  const nightWatchBin = getNightWatchBinPath();
8121
8346
  const projectName = getProjectName(projectDir);
8122
8347
  const marker = generateMarker(projectName);
8123
- const logDir = path21.join(projectDir, LOG_DIR);
8124
- if (!fs19.existsSync(logDir)) {
8125
- fs19.mkdirSync(logDir, { recursive: true });
8348
+ const logDir = path22.join(projectDir, LOG_DIR);
8349
+ if (!fs20.existsSync(logDir)) {
8350
+ fs20.mkdirSync(logDir, { recursive: true });
8126
8351
  }
8127
- const executorLog = path21.join(logDir, "executor.log");
8128
- const reviewerLog = path21.join(logDir, "reviewer.log");
8352
+ const executorLog = path22.join(logDir, "executor.log");
8353
+ const reviewerLog = path22.join(logDir, "reviewer.log");
8129
8354
  const existingEntries = Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]));
8130
8355
  if (existingEntries.length > 0 && !options.force) {
8131
8356
  warn(`Night Watch is already installed for ${projectName}.`);
@@ -8140,6 +8365,7 @@ function installCommand(program2) {
8140
8365
  const nodeBinDir = getNodeBinDir();
8141
8366
  const pathPrefix = buildCronPathPrefix(nodeBinDir, nightWatchBin);
8142
8367
  const cliBinPrefix = `export NW_CLI_BIN=${shellQuote(nightWatchBin)} && `;
8368
+ const cronTriggerPrefix = "export NW_CRON_TRIGGER=1 && ";
8143
8369
  let providerEnvPrefix = "";
8144
8370
  if (config.providerEnv && Object.keys(config.providerEnv).length > 0) {
8145
8371
  const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
@@ -8147,38 +8373,38 @@ function installCommand(program2) {
8147
8373
  }
8148
8374
  const installExecutor = config.executorEnabled !== false;
8149
8375
  if (installExecutor) {
8150
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
8376
+ const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
8151
8377
  entries.push(executorEntry);
8152
8378
  }
8153
8379
  const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
8154
8380
  if (installReviewer) {
8155
- const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
8381
+ const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
8156
8382
  entries.push(reviewerEntry);
8157
8383
  }
8158
8384
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
8159
8385
  let slicerLog;
8160
8386
  if (installSlicer) {
8161
- slicerLog = path21.join(logDir, "slicer.log");
8162
- const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
8163
- const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8387
+ slicerLog = path22.join(logDir, "slicer.log");
8388
+ const slicerSchedule = config.roadmapScanner.slicerSchedule;
8389
+ const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8164
8390
  entries.push(slicerEntry);
8165
8391
  }
8166
8392
  const disableQa = options.noQa === true || options.qa === false;
8167
8393
  const installQa = disableQa ? false : config.qa.enabled;
8168
8394
  let qaLog;
8169
8395
  if (installQa) {
8170
- qaLog = path21.join(logDir, "qa.log");
8171
- const qaSchedule = applyScheduleOffset(config.qa.schedule, offset);
8172
- const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8396
+ qaLog = path22.join(logDir, "qa.log");
8397
+ const qaSchedule = config.qa.schedule;
8398
+ const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8173
8399
  entries.push(qaEntry);
8174
8400
  }
8175
8401
  const disableAudit = options.noAudit === true || options.audit === false;
8176
8402
  const installAudit = disableAudit ? false : config.audit.enabled;
8177
8403
  let auditLog;
8178
8404
  if (installAudit) {
8179
- auditLog = path21.join(logDir, "audit.log");
8180
- const auditSchedule = applyScheduleOffset(config.audit.schedule, offset);
8181
- const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8405
+ auditLog = path22.join(logDir, "audit.log");
8406
+ const auditSchedule = config.audit.schedule;
8407
+ const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8182
8408
  entries.push(auditEntry);
8183
8409
  }
8184
8410
  const existingEntrySet = new Set(existingEntries);
@@ -8229,19 +8455,19 @@ function performUninstall(projectDir, options) {
8229
8455
  const removedCount = removeEntriesForProject(projectDir, marker);
8230
8456
  unregisterProject(projectDir);
8231
8457
  if (!options?.keepLogs) {
8232
- const logDir = path22.join(projectDir, "logs");
8233
- if (fs20.existsSync(logDir)) {
8458
+ const logDir = path23.join(projectDir, "logs");
8459
+ if (fs21.existsSync(logDir)) {
8234
8460
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8235
8461
  logFiles.forEach((logFile) => {
8236
- const logPath = path22.join(logDir, logFile);
8237
- if (fs20.existsSync(logPath)) {
8238
- fs20.unlinkSync(logPath);
8462
+ const logPath = path23.join(logDir, logFile);
8463
+ if (fs21.existsSync(logPath)) {
8464
+ fs21.unlinkSync(logPath);
8239
8465
  }
8240
8466
  });
8241
8467
  try {
8242
- const remainingFiles = fs20.readdirSync(logDir);
8468
+ const remainingFiles = fs21.readdirSync(logDir);
8243
8469
  if (remainingFiles.length === 0) {
8244
- fs20.rmdirSync(logDir);
8470
+ fs21.rmdirSync(logDir);
8245
8471
  }
8246
8472
  } catch {
8247
8473
  }
@@ -8272,21 +8498,21 @@ function uninstallCommand(program2) {
8272
8498
  existingEntries.forEach((entry) => dim(` ${entry}`));
8273
8499
  const removedCount = removeEntriesForProject(projectDir, marker);
8274
8500
  if (!options.keepLogs) {
8275
- const logDir = path22.join(projectDir, "logs");
8276
- if (fs20.existsSync(logDir)) {
8501
+ const logDir = path23.join(projectDir, "logs");
8502
+ if (fs21.existsSync(logDir)) {
8277
8503
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8278
8504
  let logsRemoved = 0;
8279
8505
  logFiles.forEach((logFile) => {
8280
- const logPath = path22.join(logDir, logFile);
8281
- if (fs20.existsSync(logPath)) {
8282
- fs20.unlinkSync(logPath);
8506
+ const logPath = path23.join(logDir, logFile);
8507
+ if (fs21.existsSync(logPath)) {
8508
+ fs21.unlinkSync(logPath);
8283
8509
  logsRemoved++;
8284
8510
  }
8285
8511
  });
8286
8512
  try {
8287
- const remainingFiles = fs20.readdirSync(logDir);
8513
+ const remainingFiles = fs21.readdirSync(logDir);
8288
8514
  if (remainingFiles.length === 0) {
8289
- fs20.rmdirSync(logDir);
8515
+ fs21.rmdirSync(logDir);
8290
8516
  }
8291
8517
  } catch {
8292
8518
  }
@@ -8512,11 +8738,11 @@ function statusCommand(program2) {
8512
8738
  }
8513
8739
  init_dist();
8514
8740
  function getLastLines(filePath, lineCount) {
8515
- if (!fs21.existsSync(filePath)) {
8741
+ if (!fs22.existsSync(filePath)) {
8516
8742
  return `Log file not found: ${filePath}`;
8517
8743
  }
8518
8744
  try {
8519
- const content = fs21.readFileSync(filePath, "utf-8");
8745
+ const content = fs22.readFileSync(filePath, "utf-8");
8520
8746
  const lines = content.trim().split("\n");
8521
8747
  return lines.slice(-lineCount).join("\n");
8522
8748
  } catch (error2) {
@@ -8524,7 +8750,7 @@ function getLastLines(filePath, lineCount) {
8524
8750
  }
8525
8751
  }
8526
8752
  function followLog(filePath) {
8527
- if (!fs21.existsSync(filePath)) {
8753
+ if (!fs22.existsSync(filePath)) {
8528
8754
  console.log(`Log file not found: ${filePath}`);
8529
8755
  console.log("The log file will be created when the first execution runs.");
8530
8756
  return;
@@ -8544,13 +8770,13 @@ function logsCommand(program2) {
8544
8770
  program2.command("logs").description("View night-watch log output").option("-n, --lines <count>", "Number of lines to show", "50").option("-f, --follow", "Follow log output (tail -f)").option("-t, --type <type>", "Log type to view (executor|reviewer|qa|audit|planner|all)", "all").action(async (options) => {
8545
8771
  try {
8546
8772
  const projectDir = process.cwd();
8547
- const logDir = path23.join(projectDir, LOG_DIR);
8773
+ const logDir = path24.join(projectDir, LOG_DIR);
8548
8774
  const lineCount = parseInt(options.lines || "50", 10);
8549
- const executorLog = path23.join(logDir, EXECUTOR_LOG_FILE);
8550
- const reviewerLog = path23.join(logDir, REVIEWER_LOG_FILE);
8551
- const qaLog = path23.join(logDir, `${QA_LOG_NAME}.log`);
8552
- const auditLog = path23.join(logDir, `${AUDIT_LOG_NAME}.log`);
8553
- const plannerLog = path23.join(logDir, `${PLANNER_LOG_NAME}.log`);
8775
+ const executorLog = path24.join(logDir, EXECUTOR_LOG_FILE);
8776
+ const reviewerLog = path24.join(logDir, REVIEWER_LOG_FILE);
8777
+ const qaLog = path24.join(logDir, `${QA_LOG_NAME}.log`);
8778
+ const auditLog = path24.join(logDir, `${AUDIT_LOG_NAME}.log`);
8779
+ const plannerLog = path24.join(logDir, `${PLANNER_LOG_NAME}.log`);
8554
8780
  const logType = options.type?.toLowerCase() || "all";
8555
8781
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
8556
8782
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
@@ -8620,9 +8846,9 @@ function slugify2(name) {
8620
8846
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8621
8847
  }
8622
8848
  function getNextPrdNumber2(prdDir) {
8623
- if (!fs22.existsSync(prdDir))
8849
+ if (!fs23.existsSync(prdDir))
8624
8850
  return 1;
8625
- const files = fs22.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
8851
+ const files = fs23.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
8626
8852
  const numbers = files.map((f) => {
8627
8853
  const match = f.match(/^(\d+)-/);
8628
8854
  return match ? parseInt(match[1], 10) : 0;
@@ -8630,9 +8856,9 @@ function getNextPrdNumber2(prdDir) {
8630
8856
  return Math.max(0, ...numbers) + 1;
8631
8857
  }
8632
8858
  function prompt(rl, question) {
8633
- return new Promise((resolve9) => {
8859
+ return new Promise((resolve10) => {
8634
8860
  rl.question(question, (answer) => {
8635
- resolve9(answer.trim());
8861
+ resolve10(answer.trim());
8636
8862
  });
8637
8863
  });
8638
8864
  }
@@ -8644,10 +8870,10 @@ function parseDependencies(content) {
8644
8870
  }
8645
8871
  function isClaimActive(claimPath, maxRuntime) {
8646
8872
  try {
8647
- if (!fs22.existsSync(claimPath)) {
8873
+ if (!fs23.existsSync(claimPath)) {
8648
8874
  return { active: false };
8649
8875
  }
8650
- const content = fs22.readFileSync(claimPath, "utf-8");
8876
+ const content = fs23.readFileSync(claimPath, "utf-8");
8651
8877
  const claim = JSON.parse(content);
8652
8878
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
8653
8879
  if (age < maxRuntime) {
@@ -8663,9 +8889,9 @@ function prdCommand(program2) {
8663
8889
  prd.command("create").description("Generate a new PRD markdown file from template").argument("<name>", "PRD name (used for title and filename)").option("-i, --interactive", "Prompt for complexity, dependencies, and phase count", false).option("-t, --template <path>", "Path to a custom template file").option("--deps <files>", "Comma-separated dependency filenames").option("--phases <count>", "Number of execution phases", "3").option("--no-number", "Skip auto-numbering prefix").action(async (name, options) => {
8664
8890
  const projectDir = process.cwd();
8665
8891
  const config = loadConfig(projectDir);
8666
- const prdDir = path24.join(projectDir, config.prdDir);
8667
- if (!fs22.existsSync(prdDir)) {
8668
- fs22.mkdirSync(prdDir, { recursive: true });
8892
+ const prdDir = path25.join(projectDir, config.prdDir);
8893
+ if (!fs23.existsSync(prdDir)) {
8894
+ fs23.mkdirSync(prdDir, { recursive: true });
8669
8895
  }
8670
8896
  let complexityScore = 5;
8671
8897
  let dependsOn = [];
@@ -8721,20 +8947,20 @@ function prdCommand(program2) {
8721
8947
  } else {
8722
8948
  filename = `${slug}.md`;
8723
8949
  }
8724
- const filePath = path24.join(prdDir, filename);
8725
- if (fs22.existsSync(filePath)) {
8950
+ const filePath = path25.join(prdDir, filename);
8951
+ if (fs23.existsSync(filePath)) {
8726
8952
  error(`File already exists: ${filePath}`);
8727
8953
  dim("Use a different name or remove the existing file.");
8728
8954
  process.exit(1);
8729
8955
  }
8730
8956
  let customTemplate;
8731
8957
  if (options.template) {
8732
- const templatePath = path24.resolve(options.template);
8733
- if (!fs22.existsSync(templatePath)) {
8958
+ const templatePath = path25.resolve(options.template);
8959
+ if (!fs23.existsSync(templatePath)) {
8734
8960
  error(`Template file not found: ${templatePath}`);
8735
8961
  process.exit(1);
8736
8962
  }
8737
- customTemplate = fs22.readFileSync(templatePath, "utf-8");
8963
+ customTemplate = fs23.readFileSync(templatePath, "utf-8");
8738
8964
  }
8739
8965
  const vars = {
8740
8966
  title: name,
@@ -8745,7 +8971,7 @@ function prdCommand(program2) {
8745
8971
  phaseCount
8746
8972
  };
8747
8973
  const content = renderPrdTemplate(vars, customTemplate);
8748
- fs22.writeFileSync(filePath, content, "utf-8");
8974
+ fs23.writeFileSync(filePath, content, "utf-8");
8749
8975
  header("PRD Created");
8750
8976
  success(`Created: ${filePath}`);
8751
8977
  info(`Title: ${name}`);
@@ -8757,15 +8983,15 @@ function prdCommand(program2) {
8757
8983
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
8758
8984
  const projectDir = process.cwd();
8759
8985
  const config = loadConfig(projectDir);
8760
- const absolutePrdDir = path24.join(projectDir, config.prdDir);
8761
- const doneDir = path24.join(absolutePrdDir, "done");
8986
+ const absolutePrdDir = path25.join(projectDir, config.prdDir);
8987
+ const doneDir = path25.join(absolutePrdDir, "done");
8762
8988
  const pending = [];
8763
- if (fs22.existsSync(absolutePrdDir)) {
8764
- const files = fs22.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
8989
+ if (fs23.existsSync(absolutePrdDir)) {
8990
+ const files = fs23.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
8765
8991
  for (const file of files) {
8766
- const content = fs22.readFileSync(path24.join(absolutePrdDir, file), "utf-8");
8992
+ const content = fs23.readFileSync(path25.join(absolutePrdDir, file), "utf-8");
8767
8993
  const deps = parseDependencies(content);
8768
- const claimPath = path24.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
8994
+ const claimPath = path25.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
8769
8995
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
8770
8996
  pending.push({
8771
8997
  name: file,
@@ -8776,10 +9002,10 @@ function prdCommand(program2) {
8776
9002
  }
8777
9003
  }
8778
9004
  const done = [];
8779
- if (fs22.existsSync(doneDir)) {
8780
- const files = fs22.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
9005
+ if (fs23.existsSync(doneDir)) {
9006
+ const files = fs23.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
8781
9007
  for (const file of files) {
8782
- const content = fs22.readFileSync(path24.join(doneDir, file), "utf-8");
9008
+ const content = fs23.readFileSync(path25.join(doneDir, file), "utf-8");
8783
9009
  const deps = parseDependencies(content);
8784
9010
  done.push({ name: file, dependencies: deps });
8785
9011
  }
@@ -8909,7 +9135,7 @@ function renderLogPane(projectDir, logs) {
8909
9135
  let newestMtime = 0;
8910
9136
  for (const log of existingLogs) {
8911
9137
  try {
8912
- const stat = fs23.statSync(log.path);
9138
+ const stat = fs24.statSync(log.path);
8913
9139
  if (stat.mtimeMs > newestMtime) {
8914
9140
  newestMtime = stat.mtimeMs;
8915
9141
  newestLog = log;
@@ -10590,7 +10816,7 @@ function createLogsTab() {
10590
10816
  let activeKeyHandlers = [];
10591
10817
  let activeCtx = null;
10592
10818
  function getLogPath(projectDir, logName) {
10593
- return path25.join(projectDir, "logs", `${logName}.log`);
10819
+ return path26.join(projectDir, "logs", `${logName}.log`);
10594
10820
  }
10595
10821
  function updateSelector() {
10596
10822
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -10604,7 +10830,7 @@ function createLogsTab() {
10604
10830
  function loadLog(ctx) {
10605
10831
  const logName = LOG_NAMES[selectedLogIndex];
10606
10832
  const logPath = getLogPath(ctx.projectDir, logName);
10607
- if (!fs24.existsSync(logPath)) {
10833
+ if (!fs25.existsSync(logPath)) {
10608
10834
  logContent.setContent(`{yellow-fg}No ${logName}.log file found{/yellow-fg}
10609
10835
 
10610
10836
  Log will appear here once the ${logName} runs.`);
@@ -10612,7 +10838,7 @@ Log will appear here once the ${logName} runs.`);
10612
10838
  return;
10613
10839
  }
10614
10840
  try {
10615
- const stat = fs24.statSync(logPath);
10841
+ const stat = fs25.statSync(logPath);
10616
10842
  const sizeKB = (stat.size / 1024).toFixed(1);
10617
10843
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
10618
10844
  } catch {
@@ -11177,7 +11403,7 @@ function resolveProject(req, res, next) {
11177
11403
  res.status(404).json({ error: `Project not found: ${decodedId}` });
11178
11404
  return;
11179
11405
  }
11180
- if (!fs25.existsSync(entry.path) || !fs25.existsSync(path26.join(entry.path, CONFIG_FILE_NAME))) {
11406
+ if (!fs26.existsSync(entry.path) || !fs26.existsSync(path27.join(entry.path, CONFIG_FILE_NAME))) {
11181
11407
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
11182
11408
  return;
11183
11409
  }
@@ -11249,17 +11475,17 @@ function getBoardProvider(config, projectDir) {
11249
11475
  function cleanOrphanedClaims(dir) {
11250
11476
  let entries;
11251
11477
  try {
11252
- entries = fs26.readdirSync(dir, { withFileTypes: true });
11478
+ entries = fs27.readdirSync(dir, { withFileTypes: true });
11253
11479
  } catch {
11254
11480
  return;
11255
11481
  }
11256
11482
  for (const entry of entries) {
11257
- const fullPath = path27.join(dir, entry.name);
11483
+ const fullPath = path28.join(dir, entry.name);
11258
11484
  if (entry.isDirectory() && entry.name !== "done") {
11259
11485
  cleanOrphanedClaims(fullPath);
11260
11486
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
11261
11487
  try {
11262
- fs26.unlinkSync(fullPath);
11488
+ fs27.unlinkSync(fullPath);
11263
11489
  } catch {
11264
11490
  }
11265
11491
  }
@@ -11307,7 +11533,7 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
11307
11533
  const config = loadConfig(projectDir);
11308
11534
  sendNotifications(config, {
11309
11535
  event: "run_started",
11310
- projectName: path27.basename(projectDir),
11536
+ projectName: path28.basename(projectDir),
11311
11537
  exitCode: 0,
11312
11538
  provider: config.provider
11313
11539
  }).catch(() => {
@@ -11419,19 +11645,19 @@ function createActionRouteHandlers(ctx) {
11419
11645
  res.status(400).json({ error: "Invalid PRD name" });
11420
11646
  return;
11421
11647
  }
11422
- const prdDir = path27.join(projectDir, config.prdDir);
11648
+ const prdDir = path28.join(projectDir, config.prdDir);
11423
11649
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
11424
- const pendingPath = path27.join(prdDir, normalized);
11425
- const donePath = path27.join(prdDir, "done", normalized);
11426
- if (fs26.existsSync(pendingPath)) {
11650
+ const pendingPath = path28.join(prdDir, normalized);
11651
+ const donePath = path28.join(prdDir, "done", normalized);
11652
+ if (fs27.existsSync(pendingPath)) {
11427
11653
  res.json({ message: `"${normalized}" is already pending` });
11428
11654
  return;
11429
11655
  }
11430
- if (!fs26.existsSync(donePath)) {
11656
+ if (!fs27.existsSync(donePath)) {
11431
11657
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
11432
11658
  return;
11433
11659
  }
11434
- fs26.renameSync(donePath, pendingPath);
11660
+ fs27.renameSync(donePath, pendingPath);
11435
11661
  res.json({ message: `Moved "${normalized}" back to pending` });
11436
11662
  } catch (error2) {
11437
11663
  res.status(500).json({
@@ -11449,11 +11675,11 @@ function createActionRouteHandlers(ctx) {
11449
11675
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
11450
11676
  return;
11451
11677
  }
11452
- if (fs26.existsSync(lockPath)) {
11453
- fs26.unlinkSync(lockPath);
11678
+ if (fs27.existsSync(lockPath)) {
11679
+ fs27.unlinkSync(lockPath);
11454
11680
  }
11455
- const prdDir = path27.join(projectDir, config.prdDir);
11456
- if (fs26.existsSync(prdDir)) {
11681
+ const prdDir = path28.join(projectDir, config.prdDir);
11682
+ if (fs27.existsSync(prdDir)) {
11457
11683
  cleanOrphanedClaims(prdDir);
11458
11684
  }
11459
11685
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -11839,6 +12065,9 @@ function validateConfigChanges(changes) {
11839
12065
  if (changes.cronScheduleOffset !== void 0 && (typeof changes.cronScheduleOffset !== "number" || changes.cronScheduleOffset < 0 || changes.cronScheduleOffset > 59)) {
11840
12066
  return "cronScheduleOffset must be a number between 0 and 59";
11841
12067
  }
12068
+ if (changes.schedulingPriority !== void 0 && (typeof changes.schedulingPriority !== "number" || !Number.isInteger(changes.schedulingPriority) || changes.schedulingPriority < 1 || changes.schedulingPriority > 5)) {
12069
+ return "schedulingPriority must be an integer between 1 and 5";
12070
+ }
11842
12071
  if (changes.fallbackOnRateLimit !== void 0 && typeof changes.fallbackOnRateLimit !== "boolean") {
11843
12072
  return "fallbackOnRateLimit must be a boolean";
11844
12073
  }
@@ -11894,6 +12123,35 @@ function validateConfigChanges(changes) {
11894
12123
  return "audit.maxRuntime must be a number >= 60";
11895
12124
  }
11896
12125
  }
12126
+ if (changes.queue !== void 0) {
12127
+ if (typeof changes.queue !== "object" || changes.queue === null) {
12128
+ return "queue must be an object";
12129
+ }
12130
+ const queue = changes.queue;
12131
+ if (queue.enabled !== void 0 && typeof queue.enabled !== "boolean") {
12132
+ return "queue.enabled must be a boolean";
12133
+ }
12134
+ if (queue.maxWaitTime !== void 0 && (typeof queue.maxWaitTime !== "number" || queue.maxWaitTime < 300 || queue.maxWaitTime > 14400)) {
12135
+ return "queue.maxWaitTime must be a number between 300 and 14400";
12136
+ }
12137
+ if (queue.maxConcurrency !== void 0 && queue.maxConcurrency !== 1) {
12138
+ return "queue.maxConcurrency is currently fixed at 1";
12139
+ }
12140
+ if (queue.priority !== void 0) {
12141
+ if (typeof queue.priority !== "object" || queue.priority === null) {
12142
+ return "queue.priority must be an object";
12143
+ }
12144
+ const validQueueJobs = ["executor", "reviewer", "qa", "audit", "slicer"];
12145
+ for (const [jobType, value] of Object.entries(queue.priority)) {
12146
+ if (!validQueueJobs.includes(jobType)) {
12147
+ return `queue.priority contains invalid job type: ${jobType}`;
12148
+ }
12149
+ if (typeof value !== "number" || Number.isNaN(value)) {
12150
+ return `queue.priority.${jobType} must be a number`;
12151
+ }
12152
+ }
12153
+ }
12154
+ }
11897
12155
  if (changes.boardProvider !== void 0) {
11898
12156
  if (typeof changes.boardProvider !== "object" || changes.boardProvider === null) {
11899
12157
  return "boardProvider must be an object";
@@ -12001,7 +12259,7 @@ function runDoctorChecks(projectDir, config) {
12001
12259
  });
12002
12260
  }
12003
12261
  try {
12004
- const projectName = path28.basename(projectDir);
12262
+ const projectName = path29.basename(projectDir);
12005
12263
  const marker = generateMarker(projectName);
12006
12264
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
12007
12265
  if (crontabEntries.length > 0) {
@@ -12024,8 +12282,8 @@ function runDoctorChecks(projectDir, config) {
12024
12282
  detail: "Failed to check crontab"
12025
12283
  });
12026
12284
  }
12027
- const configPath = path28.join(projectDir, CONFIG_FILE_NAME);
12028
- if (fs27.existsSync(configPath)) {
12285
+ const configPath = path29.join(projectDir, CONFIG_FILE_NAME);
12286
+ if (fs28.existsSync(configPath)) {
12029
12287
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
12030
12288
  } else {
12031
12289
  checks.push({
@@ -12034,9 +12292,9 @@ function runDoctorChecks(projectDir, config) {
12034
12292
  detail: "Config file not found (using defaults)"
12035
12293
  });
12036
12294
  }
12037
- const prdDir = path28.join(projectDir, config.prdDir);
12038
- if (fs27.existsSync(prdDir)) {
12039
- const prds = fs27.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
12295
+ const prdDir = path29.join(projectDir, config.prdDir);
12296
+ if (fs28.existsSync(prdDir)) {
12297
+ const prds = fs28.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
12040
12298
  checks.push({
12041
12299
  name: "prdDir",
12042
12300
  status: "pass",
@@ -12094,7 +12352,7 @@ function createLogRoutes(deps) {
12094
12352
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12095
12353
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12096
12354
  const fileName = LOG_FILE_NAMES[name] || name;
12097
- const logPath = path29.join(projectDir, LOG_DIR, `${fileName}.log`);
12355
+ const logPath = path30.join(projectDir, LOG_DIR, `${fileName}.log`);
12098
12356
  const logLines = getLastLogLines(logPath, linesToRead);
12099
12357
  res.json({ name, lines: logLines });
12100
12358
  } catch (error2) {
@@ -12120,7 +12378,7 @@ function createProjectLogRoutes() {
12120
12378
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12121
12379
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12122
12380
  const fileName = LOG_FILE_NAMES[name] || name;
12123
- const logPath = path29.join(projectDir, LOG_DIR, `${fileName}.log`);
12381
+ const logPath = path30.join(projectDir, LOG_DIR, `${fileName}.log`);
12124
12382
  const logLines = getLastLogLines(logPath, linesToRead);
12125
12383
  res.json({ name, lines: logLines });
12126
12384
  } catch (error2) {
@@ -12158,7 +12416,7 @@ function createRoadmapRouteHandlers(ctx) {
12158
12416
  const config = ctx.getConfig(req);
12159
12417
  const projectDir = ctx.getProjectDir(req);
12160
12418
  const status = getRoadmapStatus(projectDir, config);
12161
- const prdDir = path30.join(projectDir, config.prdDir);
12419
+ const prdDir = path31.join(projectDir, config.prdDir);
12162
12420
  const state = loadRoadmapState(prdDir);
12163
12421
  res.json({
12164
12422
  ...status,
@@ -12260,17 +12518,6 @@ data: ${JSON.stringify(snapshot)}
12260
12518
  });
12261
12519
  return router;
12262
12520
  }
12263
- function applyScheduleOffset2(schedule, offset) {
12264
- if (offset === 0) {
12265
- return schedule;
12266
- }
12267
- const parts = schedule.trim().split(/\s+/);
12268
- if (parts.length < 5 || !/^\d+$/.test(parts[0])) {
12269
- return schedule.trim();
12270
- }
12271
- parts[0] = String(offset);
12272
- return parts.join(" ");
12273
- }
12274
12521
  function computeNextRun(cronExpr) {
12275
12522
  try {
12276
12523
  const interval = CronExpressionParser2.parse(cronExpr);
@@ -12283,13 +12530,12 @@ function hasScheduledCommand(entries, command) {
12283
12530
  const commandPattern = new RegExp(`\\s${command}\\s+>>`);
12284
12531
  return entries.some((entry) => commandPattern.test(entry));
12285
12532
  }
12286
- function buildScheduleInfoResponse(config, entries, installed) {
12287
- const offset = config.cronScheduleOffset ?? 0;
12288
- const executorSchedule = applyScheduleOffset2(config.cronSchedule, offset);
12289
- const reviewerSchedule = applyScheduleOffset2(config.reviewerSchedule, offset);
12290
- const qaSchedule = applyScheduleOffset2(config.qa.schedule, offset);
12291
- const auditSchedule = applyScheduleOffset2(config.audit.schedule, offset);
12292
- const plannerSchedule = applyScheduleOffset2(config.roadmapScanner.slicerSchedule, offset);
12533
+ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
12534
+ const executorPlan = getSchedulingPlan(projectDir, config, "executor");
12535
+ const reviewerPlan = getSchedulingPlan(projectDir, config, "reviewer");
12536
+ const qaPlan = getSchedulingPlan(projectDir, config, "qa");
12537
+ const auditPlan = getSchedulingPlan(projectDir, config, "audit");
12538
+ const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
12293
12539
  const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
12294
12540
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
12295
12541
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
@@ -12297,31 +12543,47 @@ function buildScheduleInfoResponse(config, entries, installed) {
12297
12543
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
12298
12544
  return {
12299
12545
  executor: {
12300
- schedule: executorSchedule,
12546
+ schedule: config.cronSchedule,
12301
12547
  installed: executorInstalled,
12302
- nextRun: executorInstalled ? computeNextRun(executorSchedule) : null
12548
+ nextRun: executorInstalled ? addDelayToIsoString(computeNextRun(config.cronSchedule), executorPlan.totalDelayMinutes) : null,
12549
+ delayMinutes: executorPlan.totalDelayMinutes,
12550
+ manualDelayMinutes: executorPlan.manualDelayMinutes,
12551
+ balancedDelayMinutes: executorPlan.balancedDelayMinutes
12303
12552
  },
12304
12553
  reviewer: {
12305
- schedule: reviewerSchedule,
12554
+ schedule: config.reviewerSchedule,
12306
12555
  installed: reviewerInstalled,
12307
- nextRun: reviewerInstalled ? computeNextRun(reviewerSchedule) : null
12556
+ nextRun: reviewerInstalled ? addDelayToIsoString(computeNextRun(config.reviewerSchedule), reviewerPlan.totalDelayMinutes) : null,
12557
+ delayMinutes: reviewerPlan.totalDelayMinutes,
12558
+ manualDelayMinutes: reviewerPlan.manualDelayMinutes,
12559
+ balancedDelayMinutes: reviewerPlan.balancedDelayMinutes
12308
12560
  },
12309
12561
  qa: {
12310
- schedule: qaSchedule,
12562
+ schedule: config.qa.schedule,
12311
12563
  installed: qaInstalled,
12312
- nextRun: qaInstalled ? computeNextRun(qaSchedule) : null
12564
+ nextRun: qaInstalled ? addDelayToIsoString(computeNextRun(config.qa.schedule), qaPlan.totalDelayMinutes) : null,
12565
+ delayMinutes: qaPlan.totalDelayMinutes,
12566
+ manualDelayMinutes: qaPlan.manualDelayMinutes,
12567
+ balancedDelayMinutes: qaPlan.balancedDelayMinutes
12313
12568
  },
12314
12569
  audit: {
12315
- schedule: auditSchedule,
12570
+ schedule: config.audit.schedule,
12316
12571
  installed: auditInstalled,
12317
- nextRun: auditInstalled ? computeNextRun(auditSchedule) : null
12572
+ nextRun: auditInstalled ? addDelayToIsoString(computeNextRun(config.audit.schedule), auditPlan.totalDelayMinutes) : null,
12573
+ delayMinutes: auditPlan.totalDelayMinutes,
12574
+ manualDelayMinutes: auditPlan.manualDelayMinutes,
12575
+ balancedDelayMinutes: auditPlan.balancedDelayMinutes
12318
12576
  },
12319
12577
  planner: {
12320
- schedule: plannerSchedule,
12578
+ schedule: config.roadmapScanner.slicerSchedule,
12321
12579
  installed: plannerInstalled,
12322
- nextRun: plannerInstalled ? computeNextRun(plannerSchedule) : null
12580
+ nextRun: plannerInstalled ? addDelayToIsoString(computeNextRun(config.roadmapScanner.slicerSchedule), plannerPlan.totalDelayMinutes) : null,
12581
+ delayMinutes: plannerPlan.totalDelayMinutes,
12582
+ manualDelayMinutes: plannerPlan.manualDelayMinutes,
12583
+ balancedDelayMinutes: plannerPlan.balancedDelayMinutes
12323
12584
  },
12324
12585
  paused: !installed,
12586
+ schedulingPriority: config.schedulingPriority,
12325
12587
  entries
12326
12588
  };
12327
12589
  }
@@ -12332,7 +12594,7 @@ function createScheduleInfoRoutes(deps) {
12332
12594
  try {
12333
12595
  const config = getConfig();
12334
12596
  const snapshot = await fetchStatusSnapshot(projectDir, config);
12335
- res.json(buildScheduleInfoResponse(config, snapshot.crontab.entries, snapshot.crontab.installed));
12597
+ res.json(buildScheduleInfoResponse(projectDir, config, snapshot.crontab.entries, snapshot.crontab.installed));
12336
12598
  } catch (error2) {
12337
12599
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12338
12600
  }
@@ -12388,7 +12650,7 @@ data: ${JSON.stringify(snapshot)}
12388
12650
  const config = req.projectConfig;
12389
12651
  const projectDir = req.projectDir;
12390
12652
  const snapshot = await fetchStatusSnapshot(projectDir, config);
12391
- res.json(buildScheduleInfoResponse(config, snapshot.crontab.entries, snapshot.crontab.installed));
12653
+ res.json(buildScheduleInfoResponse(projectDir, config, snapshot.crontab.entries, snapshot.crontab.installed));
12392
12654
  } catch (error2) {
12393
12655
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12394
12656
  }
@@ -12423,14 +12685,14 @@ function createQueueRoutes(deps) {
12423
12685
  var __filename2 = fileURLToPath3(import.meta.url);
12424
12686
  var __dirname3 = dirname7(__filename2);
12425
12687
  function resolveWebDistPath() {
12426
- const bundled = path31.join(__dirname3, "web");
12427
- if (fs28.existsSync(path31.join(bundled, "index.html")))
12688
+ const bundled = path32.join(__dirname3, "web");
12689
+ if (fs29.existsSync(path32.join(bundled, "index.html")))
12428
12690
  return bundled;
12429
12691
  let d = __dirname3;
12430
12692
  for (let i = 0; i < 8; i++) {
12431
- if (fs28.existsSync(path31.join(d, "turbo.json"))) {
12432
- const dev = path31.join(d, "web/dist");
12433
- if (fs28.existsSync(path31.join(dev, "index.html")))
12693
+ if (fs29.existsSync(path32.join(d, "turbo.json"))) {
12694
+ const dev = path32.join(d, "web/dist");
12695
+ if (fs29.existsSync(path32.join(dev, "index.html")))
12434
12696
  return dev;
12435
12697
  break;
12436
12698
  }
@@ -12440,7 +12702,7 @@ function resolveWebDistPath() {
12440
12702
  }
12441
12703
  function setupStaticFiles(app) {
12442
12704
  const webDistPath = resolveWebDistPath();
12443
- if (fs28.existsSync(webDistPath)) {
12705
+ if (fs29.existsSync(webDistPath)) {
12444
12706
  app.use(express.static(webDistPath));
12445
12707
  }
12446
12708
  app.use((req, res, next) => {
@@ -12448,8 +12710,8 @@ function setupStaticFiles(app) {
12448
12710
  next();
12449
12711
  return;
12450
12712
  }
12451
- const indexPath = path31.resolve(webDistPath, "index.html");
12452
- if (fs28.existsSync(indexPath)) {
12713
+ const indexPath = path32.resolve(webDistPath, "index.html");
12714
+ if (fs29.existsSync(indexPath)) {
12453
12715
  res.sendFile(indexPath, (err) => {
12454
12716
  if (err)
12455
12717
  next();
@@ -12558,7 +12820,7 @@ function createGlobalApp() {
12558
12820
  return app;
12559
12821
  }
12560
12822
  function bootContainer() {
12561
- initContainer(path31.dirname(getDbPath()));
12823
+ initContainer(path32.dirname(getDbPath()));
12562
12824
  }
12563
12825
  function startServer(projectDir, port) {
12564
12826
  bootContainer();
@@ -12609,9 +12871,9 @@ function isProcessRunning2(pid) {
12609
12871
  }
12610
12872
  function readPid(lockPath) {
12611
12873
  try {
12612
- if (!fs29.existsSync(lockPath))
12874
+ if (!fs30.existsSync(lockPath))
12613
12875
  return null;
12614
- const raw = fs29.readFileSync(lockPath, "utf-8").trim();
12876
+ const raw = fs30.readFileSync(lockPath, "utf-8").trim();
12615
12877
  const pid = parseInt(raw, 10);
12616
12878
  return Number.isFinite(pid) ? pid : null;
12617
12879
  } catch {
@@ -12623,10 +12885,10 @@ function acquireServeLock(mode, port) {
12623
12885
  let stalePidCleaned;
12624
12886
  for (let attempt = 0; attempt < 2; attempt++) {
12625
12887
  try {
12626
- const fd = fs29.openSync(lockPath, "wx");
12627
- fs29.writeFileSync(fd, `${process.pid}
12888
+ const fd = fs30.openSync(lockPath, "wx");
12889
+ fs30.writeFileSync(fd, `${process.pid}
12628
12890
  `);
12629
- fs29.closeSync(fd);
12891
+ fs30.closeSync(fd);
12630
12892
  return { acquired: true, lockPath, stalePidCleaned };
12631
12893
  } catch (error2) {
12632
12894
  const err = error2;
@@ -12647,7 +12909,7 @@ function acquireServeLock(mode, port) {
12647
12909
  };
12648
12910
  }
12649
12911
  try {
12650
- fs29.unlinkSync(lockPath);
12912
+ fs30.unlinkSync(lockPath);
12651
12913
  if (existingPid) {
12652
12914
  stalePidCleaned = existingPid;
12653
12915
  }
@@ -12670,12 +12932,12 @@ function acquireServeLock(mode, port) {
12670
12932
  }
12671
12933
  function releaseServeLock(lockPath) {
12672
12934
  try {
12673
- if (!fs29.existsSync(lockPath))
12935
+ if (!fs30.existsSync(lockPath))
12674
12936
  return;
12675
12937
  const lockPid = readPid(lockPath);
12676
12938
  if (lockPid !== null && lockPid !== process.pid)
12677
12939
  return;
12678
- fs29.unlinkSync(lockPath);
12940
+ fs30.unlinkSync(lockPath);
12679
12941
  } catch {
12680
12942
  }
12681
12943
  }
@@ -12761,7 +13023,7 @@ function parseProjectDirs(projects, cwd) {
12761
13023
  if (!projects || projects.trim().length === 0) {
12762
13024
  return [cwd];
12763
13025
  }
12764
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path32.resolve(cwd, entry));
13026
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path33.resolve(cwd, entry));
12765
13027
  return Array.from(new Set(dirs));
12766
13028
  }
12767
13029
  function shouldInstallGlobal(options) {
@@ -12803,7 +13065,7 @@ function updateCommand(program2) {
12803
13065
  }
12804
13066
  const nightWatchBin = resolveNightWatchBin();
12805
13067
  for (const projectDir of projectDirs) {
12806
- if (!fs30.existsSync(projectDir) || !fs30.statSync(projectDir).isDirectory()) {
13068
+ if (!fs31.existsSync(projectDir) || !fs31.statSync(projectDir).isDirectory()) {
12807
13069
  warn(`Skipping invalid project directory: ${projectDir}`);
12808
13070
  continue;
12809
13071
  }
@@ -12850,26 +13112,26 @@ function normalizePrdName(name) {
12850
13112
  return name;
12851
13113
  }
12852
13114
  function getDonePrds(doneDir) {
12853
- if (!fs31.existsSync(doneDir)) {
13115
+ if (!fs32.existsSync(doneDir)) {
12854
13116
  return [];
12855
13117
  }
12856
- return fs31.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
13118
+ return fs32.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
12857
13119
  }
12858
13120
  function retryCommand(program2) {
12859
13121
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
12860
13122
  const projectDir = process.cwd();
12861
13123
  const config = loadConfig(projectDir);
12862
- const prdDir = path33.join(projectDir, config.prdDir);
12863
- const doneDir = path33.join(prdDir, "done");
13124
+ const prdDir = path34.join(projectDir, config.prdDir);
13125
+ const doneDir = path34.join(prdDir, "done");
12864
13126
  const normalizedPrdName = normalizePrdName(prdName);
12865
- const pendingPath = path33.join(prdDir, normalizedPrdName);
12866
- if (fs31.existsSync(pendingPath)) {
13127
+ const pendingPath = path34.join(prdDir, normalizedPrdName);
13128
+ if (fs32.existsSync(pendingPath)) {
12867
13129
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
12868
13130
  return;
12869
13131
  }
12870
- const donePath = path33.join(doneDir, normalizedPrdName);
12871
- if (fs31.existsSync(donePath)) {
12872
- fs31.renameSync(donePath, pendingPath);
13132
+ const donePath = path34.join(doneDir, normalizedPrdName);
13133
+ if (fs32.existsSync(donePath)) {
13134
+ fs32.renameSync(donePath, pendingPath);
12873
13135
  success(`Moved "${normalizedPrdName}" back to pending.`);
12874
13136
  dim(`From: ${donePath}`);
12875
13137
  dim(`To: ${pendingPath}`);
@@ -13121,16 +13383,16 @@ async function promptConfirmation(prompt2) {
13121
13383
  input: process.stdin,
13122
13384
  output: process.stdout
13123
13385
  });
13124
- return new Promise((resolve9) => {
13386
+ return new Promise((resolve10) => {
13125
13387
  rl.question(`${prompt2} `, (answer) => {
13126
13388
  rl.close();
13127
13389
  const normalized = answer.toLowerCase().trim();
13128
- resolve9(normalized === "y" || normalized === "yes");
13390
+ resolve10(normalized === "y" || normalized === "yes");
13129
13391
  });
13130
13392
  });
13131
13393
  }
13132
13394
  function sleep3(ms) {
13133
- return new Promise((resolve9) => setTimeout(resolve9, ms));
13395
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
13134
13396
  }
13135
13397
  function isProcessRunning3(pid) {
13136
13398
  try {
@@ -13151,7 +13413,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
13151
13413
  const pid = lockStatus.pid;
13152
13414
  if (!lockStatus.running) {
13153
13415
  try {
13154
- fs32.unlinkSync(lockPath);
13416
+ fs33.unlinkSync(lockPath);
13155
13417
  return {
13156
13418
  success: true,
13157
13419
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -13189,7 +13451,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
13189
13451
  await sleep3(3e3);
13190
13452
  if (!isProcessRunning3(pid)) {
13191
13453
  try {
13192
- fs32.unlinkSync(lockPath);
13454
+ fs33.unlinkSync(lockPath);
13193
13455
  } catch {
13194
13456
  }
13195
13457
  return {
@@ -13224,7 +13486,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
13224
13486
  await sleep3(500);
13225
13487
  if (!isProcessRunning3(pid)) {
13226
13488
  try {
13227
- fs32.unlinkSync(lockPath);
13489
+ fs33.unlinkSync(lockPath);
13228
13490
  } catch {
13229
13491
  }
13230
13492
  return {
@@ -13288,24 +13550,24 @@ function plannerLockPath2(projectDir) {
13288
13550
  }
13289
13551
  function acquirePlannerLock(projectDir) {
13290
13552
  const lockFile = plannerLockPath2(projectDir);
13291
- if (fs33.existsSync(lockFile)) {
13292
- const pidRaw = fs33.readFileSync(lockFile, "utf-8").trim();
13553
+ if (fs34.existsSync(lockFile)) {
13554
+ const pidRaw = fs34.readFileSync(lockFile, "utf-8").trim();
13293
13555
  const pid = parseInt(pidRaw, 10);
13294
13556
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
13295
13557
  return { acquired: false, lockFile, pid };
13296
13558
  }
13297
13559
  try {
13298
- fs33.unlinkSync(lockFile);
13560
+ fs34.unlinkSync(lockFile);
13299
13561
  } catch {
13300
13562
  }
13301
13563
  }
13302
- fs33.writeFileSync(lockFile, String(process.pid));
13564
+ fs34.writeFileSync(lockFile, String(process.pid));
13303
13565
  return { acquired: true, lockFile };
13304
13566
  }
13305
13567
  function releasePlannerLock(lockFile) {
13306
13568
  try {
13307
- if (fs33.existsSync(lockFile)) {
13308
- fs33.unlinkSync(lockFile);
13569
+ if (fs34.existsSync(lockFile)) {
13570
+ fs34.unlinkSync(lockFile);
13309
13571
  }
13310
13572
  } catch {
13311
13573
  }
@@ -13314,12 +13576,12 @@ function resolvePlannerIssueColumn(config) {
13314
13576
  return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
13315
13577
  }
13316
13578
  function buildPlannerIssueBody(projectDir, config, result) {
13317
- const relativePrdPath = path34.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
13318
- const absolutePrdPath = path34.join(projectDir, config.prdDir, result.file ?? "");
13579
+ const relativePrdPath = path35.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
13580
+ const absolutePrdPath = path35.join(projectDir, config.prdDir, result.file ?? "");
13319
13581
  const sourceItem = result.item;
13320
13582
  let prdContent = "";
13321
13583
  try {
13322
- prdContent = fs33.readFileSync(absolutePrdPath, "utf-8");
13584
+ prdContent = fs34.readFileSync(absolutePrdPath, "utf-8");
13323
13585
  } catch {
13324
13586
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
13325
13587
  }
@@ -13487,10 +13749,11 @@ function sliceCommand(program2) {
13487
13749
  const spinner = createSpinner("Running Planner...");
13488
13750
  spinner.start();
13489
13751
  try {
13752
+ await maybeApplyCronSchedulingDelay(config, "slicer", projectDir);
13490
13753
  if (!options.dryRun) {
13491
13754
  await sendNotifications(config, {
13492
13755
  event: "run_started",
13493
- projectName: path34.basename(projectDir),
13756
+ projectName: path35.basename(projectDir),
13494
13757
  exitCode: 0,
13495
13758
  provider: config.provider
13496
13759
  });
@@ -13523,7 +13786,7 @@ function sliceCommand(program2) {
13523
13786
  if (!options.dryRun && result.sliced) {
13524
13787
  await sendNotifications(config, {
13525
13788
  event: "run_succeeded",
13526
- projectName: path34.basename(projectDir),
13789
+ projectName: path35.basename(projectDir),
13527
13790
  exitCode,
13528
13791
  provider: config.provider,
13529
13792
  prTitle: result.item?.title
@@ -13531,7 +13794,7 @@ function sliceCommand(program2) {
13531
13794
  } else if (!options.dryRun && !nothingPending) {
13532
13795
  await sendNotifications(config, {
13533
13796
  event: "run_failed",
13534
- projectName: path34.basename(projectDir),
13797
+ projectName: path35.basename(projectDir),
13535
13798
  exitCode,
13536
13799
  provider: config.provider
13537
13800
  });
@@ -13549,13 +13812,13 @@ function createStateCommand() {
13549
13812
  const state = new Command("state");
13550
13813
  state.description("Manage Night Watch state");
13551
13814
  state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
13552
- const nightWatchHome = process.env.NIGHT_WATCH_HOME || path35.join(os6.homedir(), GLOBAL_CONFIG_DIR);
13815
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path36.join(os6.homedir(), GLOBAL_CONFIG_DIR);
13553
13816
  if (opts.dryRun) {
13554
13817
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
13555
13818
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
13556
- console.log(` ${path35.join(nightWatchHome, "projects.json")}`);
13557
- console.log(` ${path35.join(nightWatchHome, "history.json")}`);
13558
- console.log(` ${path35.join(nightWatchHome, "prd-states.json")}`);
13819
+ console.log(` ${path36.join(nightWatchHome, "projects.json")}`);
13820
+ console.log(` ${path36.join(nightWatchHome, "history.json")}`);
13821
+ console.log(` ${path36.join(nightWatchHome, "prd-states.json")}`);
13559
13822
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
13560
13823
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
13561
13824
  return;
@@ -13603,7 +13866,7 @@ function getProvider(config, cwd) {
13603
13866
  return createBoardProvider(bp, cwd);
13604
13867
  }
13605
13868
  function defaultBoardTitle(cwd) {
13606
- return `${path36.basename(cwd)} Night Watch`;
13869
+ return `${path37.basename(cwd)} Night Watch`;
13607
13870
  }
13608
13871
  async function ensureBoardConfigured(config, cwd, provider, options) {
13609
13872
  if (config.boardProvider?.projectNumber) {
@@ -13634,10 +13897,10 @@ async function confirmPrompt(question) {
13634
13897
  input: process.stdin,
13635
13898
  output: process.stdout
13636
13899
  });
13637
- return new Promise((resolve9) => {
13900
+ return new Promise((resolve10) => {
13638
13901
  rl.question(question, (answer) => {
13639
13902
  rl.close();
13640
- resolve9(answer.trim().toLowerCase() === "y");
13903
+ resolve10(answer.trim().toLowerCase() === "y");
13641
13904
  });
13642
13905
  });
13643
13906
  }
@@ -13783,11 +14046,11 @@ function boardCommand(program2) {
13783
14046
  let body = options.body ?? "";
13784
14047
  if (options.bodyFile) {
13785
14048
  const filePath = options.bodyFile;
13786
- if (!fs34.existsSync(filePath)) {
14049
+ if (!fs35.existsSync(filePath)) {
13787
14050
  console.error(`File not found: ${filePath}`);
13788
14051
  process.exit(1);
13789
14052
  }
13790
- body = fs34.readFileSync(filePath, "utf-8");
14053
+ body = fs35.readFileSync(filePath, "utf-8");
13791
14054
  }
13792
14055
  const labels = [];
13793
14056
  if (options.label) {
@@ -13995,12 +14258,12 @@ function boardCommand(program2) {
13995
14258
  const config = loadConfig(cwd);
13996
14259
  const provider = getProvider(config, cwd);
13997
14260
  await ensureBoardConfigured(config, cwd, provider);
13998
- const roadmapPath = options.roadmap ?? path36.join(cwd, "ROADMAP.md");
13999
- if (!fs34.existsSync(roadmapPath)) {
14261
+ const roadmapPath = options.roadmap ?? path37.join(cwd, "ROADMAP.md");
14262
+ if (!fs35.existsSync(roadmapPath)) {
14000
14263
  console.error(`Roadmap file not found: ${roadmapPath}`);
14001
14264
  process.exit(1);
14002
14265
  }
14003
- const roadmapContent = fs34.readFileSync(roadmapPath, "utf-8");
14266
+ const roadmapContent = fs35.readFileSync(roadmapPath, "utf-8");
14004
14267
  const items = parseRoadmap(roadmapContent);
14005
14268
  const uncheckedItems = getUncheckedItems(items);
14006
14269
  if (uncheckedItems.length === 0) {
@@ -14231,8 +14494,9 @@ function createQueueCommand() {
14231
14494
  process.exit(1);
14232
14495
  }
14233
14496
  }
14234
- const projectName = path37.basename(projectDir);
14235
- const id = enqueueJob(projectDir, projectName, jobType, envVars);
14497
+ const projectName = path38.basename(projectDir);
14498
+ const queueConfig = loadConfig(projectDir).queue;
14499
+ const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
14236
14500
  console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
14237
14501
  });
14238
14502
  queue.command("dispatch").description("Dispatch the next pending job (used by cron scripts)").option("--log <file>", "Log file to write dispatch output").action((_opts) => {
@@ -14255,15 +14519,21 @@ function createQueueCommand() {
14255
14519
  };
14256
14520
  const scriptPath = getScriptPath(scriptName);
14257
14521
  logger.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
14258
- const child = spawn6("bash", [scriptPath, entry.projectPath], {
14259
- detached: true,
14260
- stdio: "ignore",
14261
- env,
14262
- cwd: entry.projectPath
14263
- });
14264
- child.unref();
14265
- logger.info(`Spawned PID: ${child.pid}`);
14266
- markJobRunning(entry.id);
14522
+ try {
14523
+ const child = spawn6("bash", [scriptPath, entry.projectPath], {
14524
+ detached: true,
14525
+ stdio: "ignore",
14526
+ env,
14527
+ cwd: entry.projectPath
14528
+ });
14529
+ child.unref();
14530
+ logger.info(`Spawned PID: ${child.pid}`);
14531
+ markJobRunning(entry.id);
14532
+ } catch (error2) {
14533
+ updateJobStatus(entry.id, "pending");
14534
+ logger.error(`Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`);
14535
+ process.exit(1);
14536
+ }
14267
14537
  });
14268
14538
  queue.command("complete <id>").description("Remove a completed queue entry (used by cron scripts)").action((id) => {
14269
14539
  const queueId = parseInt(id, 10);
@@ -14273,6 +14543,10 @@ function createQueueCommand() {
14273
14543
  }
14274
14544
  removeJob(queueId);
14275
14545
  });
14546
+ queue.command("can-start").description("Return a zero exit status when the global queue has an available slot").action(() => {
14547
+ const queueConfig = loadConfig(process.cwd()).queue;
14548
+ process.exit(canStartJob(queueConfig) ? 0 : 1);
14549
+ });
14276
14550
  queue.command("expire").description("Expire stale queued jobs").option("--max-wait <seconds>", "Maximum wait time in seconds", String(DEFAULT_QUEUE_MAX_WAIT_TIME)).action((opts) => {
14277
14551
  const maxWait = parseInt(opts.maxWait, 10);
14278
14552
  if (isNaN(maxWait) || maxWait < 60) {
@@ -14312,14 +14586,14 @@ var __dirname4 = dirname8(__filename3);
14312
14586
  function findPackageRoot(dir) {
14313
14587
  let d = dir;
14314
14588
  for (let i = 0; i < 5; i++) {
14315
- if (existsSync28(join33(d, "package.json")))
14589
+ if (existsSync29(join34(d, "package.json")))
14316
14590
  return d;
14317
14591
  d = dirname8(d);
14318
14592
  }
14319
14593
  return dir;
14320
14594
  }
14321
14595
  var packageRoot = findPackageRoot(__dirname4);
14322
- var packageJson = JSON.parse(readFileSync17(join33(packageRoot, "package.json"), "utf-8"));
14596
+ var packageJson = JSON.parse(readFileSync17(join34(packageRoot, "package.json"), "utf-8"));
14323
14597
  var program = new Command3();
14324
14598
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
14325
14599
  initCommand(program);