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