@jonit-dev/night-watch-cli 1.7.47 → 1.7.48

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,17 +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
- import "reflect-metadata";
15
- import "reflect-metadata";
16
- import "reflect-metadata";
17
- import "reflect-metadata";
18
- import "reflect-metadata";
19
8
  import * as fs from "fs";
20
9
  import * as path from "path";
21
10
  import { fileURLToPath } from "url";
@@ -38,8 +27,10 @@ import * as path2 from "path";
38
27
  import Database7 from "better-sqlite3";
39
28
  import "reflect-metadata";
40
29
  import { container } from "tsyringe";
41
- import { execFileSync } from "child_process";
42
- import { execFileSync as execFileSync2 } from "child_process";
30
+ import { execFile } from "child_process";
31
+ import { promisify } from "util";
32
+ import { execFile as execFile2 } from "child_process";
33
+ import { promisify as promisify2 } from "util";
43
34
  import * as fs3 from "fs";
44
35
  import * as path3 from "path";
45
36
  import { execSync } from "child_process";
@@ -47,18 +38,19 @@ import * as fs4 from "fs";
47
38
  import * as os2 from "os";
48
39
  import * as path4 from "path";
49
40
  import { createHash } from "crypto";
50
- import { execSync as execSync2 } from "child_process";
41
+ import { exec } from "child_process";
42
+ import { promisify as promisify3 } from "util";
51
43
  import * as fs5 from "fs";
52
44
  import * as path5 from "path";
53
45
  import * as fs6 from "fs";
54
46
  import * as fs7 from "fs";
55
47
  import * as path6 from "path";
56
- import { execSync as execSync3 } from "child_process";
48
+ import { execSync as execSync2 } from "child_process";
57
49
  import * as fs8 from "fs";
58
50
  import * as path7 from "path";
59
51
  import * as os3 from "os";
60
52
  import * as path8 from "path";
61
- import { execFileSync as execFileSync3 } from "child_process";
53
+ import { execFileSync } from "child_process";
62
54
  import chalk from "chalk";
63
55
  import ora from "ora";
64
56
  import Table from "cli-table3";
@@ -74,26 +66,27 @@ import * as path11 from "path";
74
66
  import * as fs13 from "fs";
75
67
  import * as path12 from "path";
76
68
  import { spawn } from "child_process";
69
+ import { createHash as createHash3 } from "crypto";
77
70
  import { spawn as spawn2 } from "child_process";
78
71
  import "reflect-metadata";
79
72
  import { Command as Command2 } from "commander";
80
- import { existsSync as existsSync25, readFileSync as readFileSync15 } from "fs";
73
+ import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
81
74
  import { fileURLToPath as fileURLToPath4 } from "url";
82
75
  import { dirname as dirname8, join as join30 } from "path";
83
76
  import fs14 from "fs";
84
77
  import path13 from "path";
85
- import { execSync as execSync4 } from "child_process";
78
+ import { execSync as execSync3 } from "child_process";
86
79
  import { fileURLToPath as fileURLToPath2 } from "url";
87
80
  import { dirname as dirname4, join as join13 } from "path";
88
81
  import * as readline from "readline";
89
82
  import * as fs15 from "fs";
90
83
  import * as path14 from "path";
91
- import { execFileSync as execFileSync4 } from "child_process";
84
+ import { execFileSync as execFileSync2 } from "child_process";
92
85
  import * as path15 from "path";
93
86
  import * as path16 from "path";
94
87
  import * as fs16 from "fs";
95
88
  import * as path17 from "path";
96
- import { execSync as execSync5 } from "child_process";
89
+ import { execSync as execSync4 } from "child_process";
97
90
  import * as path18 from "path";
98
91
  import * as fs17 from "fs";
99
92
  import * as path19 from "path";
@@ -134,7 +127,7 @@ import { Router as Router3 } from "express";
134
127
  import { Router as Router4 } from "express";
135
128
  import * as fs25 from "fs";
136
129
  import * as path25 from "path";
137
- import { execSync as execSync6 } from "child_process";
130
+ import { execSync as execSync5 } from "child_process";
138
131
  import { Router as Router5 } from "express";
139
132
  import * as path26 from "path";
140
133
  import { Router as Router6 } from "express";
@@ -150,16 +143,17 @@ import * as fs29 from "fs";
150
143
  import * as path30 from "path";
151
144
  import chalk3 from "chalk";
152
145
  import chalk4 from "chalk";
153
- import { execSync as execSync7 } from "child_process";
146
+ import { execSync as execSync6 } from "child_process";
154
147
  import * as fs30 from "fs";
155
148
  import * as readline3 from "readline";
149
+ import * as fs31 from "fs";
156
150
  import * as path31 from "path";
157
151
  import * as os5 from "os";
158
152
  import * as path32 from "path";
159
153
  import chalk5 from "chalk";
160
154
  import { Command } from "commander";
161
- import { execFileSync as execFileSync5 } from "child_process";
162
- import * as fs31 from "fs";
155
+ import { execFileSync as execFileSync3 } from "child_process";
156
+ import * as fs32 from "fs";
163
157
  import * as path33 from "path";
164
158
  import * as readline4 from "readline";
165
159
  import chalk6 from "chalk";
@@ -185,11 +179,14 @@ var DEFAULT_CRON_SCHEDULE;
185
179
  var DEFAULT_REVIEWER_SCHEDULE;
186
180
  var DEFAULT_CRON_SCHEDULE_OFFSET;
187
181
  var DEFAULT_MAX_RETRIES;
182
+ var DEFAULT_REVIEWER_MAX_RETRIES;
183
+ var DEFAULT_REVIEWER_RETRY_DELAY;
188
184
  var DEFAULT_BRANCH_PREFIX;
189
185
  var DEFAULT_BRANCH_PATTERNS;
190
186
  var DEFAULT_MIN_REVIEW_SCORE;
191
187
  var DEFAULT_MAX_LOG_SIZE;
192
188
  var DEFAULT_PROVIDER;
189
+ var DEFAULT_EXECUTOR_ENABLED;
193
190
  var DEFAULT_REVIEWER_ENABLED;
194
191
  var DEFAULT_PROVIDER_ENV;
195
192
  var DEFAULT_FALLBACK_ON_RATE_LIMIT;
@@ -220,6 +217,7 @@ var DEFAULT_AUDIT_SCHEDULE;
220
217
  var DEFAULT_AUDIT_MAX_RUNTIME;
221
218
  var DEFAULT_AUDIT;
222
219
  var AUDIT_LOG_NAME;
220
+ var PLANNER_LOG_NAME;
223
221
  var VALID_PROVIDERS;
224
222
  var VALID_JOB_TYPES;
225
223
  var DEFAULT_JOB_PROVIDERS;
@@ -243,18 +241,21 @@ var init_constants = __esm({
243
241
  "../core/dist/constants.js"() {
244
242
  "use strict";
245
243
  DEFAULT_DEFAULT_BRANCH = "";
246
- DEFAULT_PRD_DIR = "docs/PRDs/night-watch";
244
+ DEFAULT_PRD_DIR = "docs/prds";
247
245
  DEFAULT_MAX_RUNTIME = 7200;
248
246
  DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
249
247
  DEFAULT_CRON_SCHEDULE = "0 0-21 * * *";
250
248
  DEFAULT_REVIEWER_SCHEDULE = "0 0,3,6,9,12,15,18,21 * * *";
251
249
  DEFAULT_CRON_SCHEDULE_OFFSET = 0;
252
250
  DEFAULT_MAX_RETRIES = 3;
251
+ DEFAULT_REVIEWER_MAX_RETRIES = 2;
252
+ DEFAULT_REVIEWER_RETRY_DELAY = 30;
253
253
  DEFAULT_BRANCH_PREFIX = "night-watch";
254
254
  DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
255
255
  DEFAULT_MIN_REVIEW_SCORE = 80;
256
256
  DEFAULT_MAX_LOG_SIZE = 524288;
257
257
  DEFAULT_PROVIDER = "claude";
258
+ DEFAULT_EXECUTOR_ENABLED = true;
258
259
  DEFAULT_REVIEWER_ENABLED = true;
259
260
  DEFAULT_PROVIDER_ENV = {};
260
261
  DEFAULT_FALLBACK_ON_RATE_LIMIT = false;
@@ -269,7 +270,7 @@ var init_constants = __esm({
269
270
  DEFAULT_SLICER_SCHEDULE = "0 */6 * * *";
270
271
  DEFAULT_SLICER_MAX_RUNTIME = 600;
271
272
  DEFAULT_ROADMAP_SCANNER = {
272
- enabled: false,
273
+ enabled: true,
273
274
  roadmapPath: "ROADMAP.md",
274
275
  autoScanInterval: 300,
275
276
  slicerSchedule: DEFAULT_SLICER_SCHEDULE,
@@ -309,6 +310,7 @@ var init_constants = __esm({
309
310
  maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
310
311
  };
311
312
  AUDIT_LOG_NAME = "audit";
313
+ PLANNER_LOG_NAME = "slicer";
312
314
  VALID_PROVIDERS = ["claude", "codex"];
313
315
  VALID_JOB_TYPES = ["executor", "reviewer", "qa", "audit", "slicer"];
314
316
  DEFAULT_JOB_PROVIDERS = {};
@@ -327,7 +329,9 @@ var init_constants = __esm({
327
329
  LOG_FILE_NAMES = {
328
330
  executor: EXECUTOR_LOG_NAME,
329
331
  reviewer: REVIEWER_LOG_NAME,
330
- qa: QA_LOG_NAME
332
+ qa: QA_LOG_NAME,
333
+ audit: AUDIT_LOG_NAME,
334
+ planner: PLANNER_LOG_NAME
331
335
  };
332
336
  GLOBAL_CONFIG_DIR = ".night-watch";
333
337
  REGISTRY_FILE_NAME = "projects.json";
@@ -353,8 +357,12 @@ function getDefaultConfig() {
353
357
  reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
354
358
  cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
355
359
  maxRetries: DEFAULT_MAX_RETRIES,
360
+ // Reviewer retry configuration
361
+ reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
362
+ reviewerRetryDelay: DEFAULT_REVIEWER_RETRY_DELAY,
356
363
  // Provider configuration
357
364
  provider: DEFAULT_PROVIDER,
365
+ executorEnabled: DEFAULT_EXECUTOR_ENABLED,
358
366
  reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
359
367
  providerEnv: { ...DEFAULT_PROVIDER_ENV },
360
368
  // Notification configuration
@@ -416,7 +424,10 @@ function normalizeConfig(rawConfig) {
416
424
  normalized.reviewerSchedule = readString(rawConfig.reviewerSchedule) ?? readString(cron?.reviewerSchedule);
417
425
  normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
418
426
  normalized.maxRetries = readNumber(rawConfig.maxRetries);
427
+ normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
428
+ normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
419
429
  normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
430
+ normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
420
431
  normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
421
432
  const rawProviderEnv = readObject(rawConfig.providerEnv);
422
433
  if (rawProviderEnv) {
@@ -556,6 +567,28 @@ function sanitizeMaxRetries(value, fallback) {
556
567
  const normalized = Math.floor(value);
557
568
  return normalized >= 1 ? normalized : fallback;
558
569
  }
570
+ function sanitizeReviewerMaxRetries(value, fallback) {
571
+ if (!Number.isFinite(value)) {
572
+ return fallback;
573
+ }
574
+ const normalized = Math.floor(value);
575
+ if (normalized < 0)
576
+ return 0;
577
+ if (normalized > 10)
578
+ return 10;
579
+ return normalized;
580
+ }
581
+ function sanitizeReviewerRetryDelay(value, fallback) {
582
+ if (!Number.isFinite(value)) {
583
+ return fallback;
584
+ }
585
+ const normalized = Math.floor(value);
586
+ if (normalized < 0)
587
+ return 0;
588
+ if (normalized > 300)
589
+ return 300;
590
+ return normalized;
591
+ }
559
592
  function mergeConfigs(base, fileConfig, envConfig) {
560
593
  const merged = { ...base };
561
594
  if (fileConfig) {
@@ -583,8 +616,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
583
616
  merged.cronScheduleOffset = fileConfig.cronScheduleOffset;
584
617
  if (fileConfig.maxRetries !== void 0)
585
618
  merged.maxRetries = fileConfig.maxRetries;
619
+ if (fileConfig.reviewerMaxRetries !== void 0)
620
+ merged.reviewerMaxRetries = fileConfig.reviewerMaxRetries;
621
+ if (fileConfig.reviewerRetryDelay !== void 0)
622
+ merged.reviewerRetryDelay = fileConfig.reviewerRetryDelay;
586
623
  if (fileConfig.provider !== void 0)
587
624
  merged.provider = fileConfig.provider;
625
+ if (fileConfig.executorEnabled !== void 0)
626
+ merged.executorEnabled = fileConfig.executorEnabled;
588
627
  if (fileConfig.reviewerEnabled !== void 0)
589
628
  merged.reviewerEnabled = fileConfig.reviewerEnabled;
590
629
  if (fileConfig.providerEnv !== void 0)
@@ -609,6 +648,8 @@ function mergeConfigs(base, fileConfig, envConfig) {
609
648
  merged.claudeModel = fileConfig.claudeModel;
610
649
  if (fileConfig.qa !== void 0)
611
650
  merged.qa = { ...merged.qa, ...fileConfig.qa };
651
+ if (fileConfig.audit !== void 0)
652
+ merged.audit = { ...merged.audit, ...fileConfig.audit };
612
653
  if (fileConfig.jobProviders !== void 0)
613
654
  merged.jobProviders = { ...fileConfig.jobProviders };
614
655
  }
@@ -636,8 +677,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
636
677
  merged.cronScheduleOffset = envConfig.cronScheduleOffset;
637
678
  if (envConfig.maxRetries !== void 0)
638
679
  merged.maxRetries = envConfig.maxRetries;
680
+ if (envConfig.reviewerMaxRetries !== void 0)
681
+ merged.reviewerMaxRetries = envConfig.reviewerMaxRetries;
682
+ if (envConfig.reviewerRetryDelay !== void 0)
683
+ merged.reviewerRetryDelay = envConfig.reviewerRetryDelay;
639
684
  if (envConfig.provider !== void 0)
640
685
  merged.provider = envConfig.provider;
686
+ if (envConfig.executorEnabled !== void 0)
687
+ merged.executorEnabled = envConfig.executorEnabled;
641
688
  if (envConfig.reviewerEnabled !== void 0)
642
689
  merged.reviewerEnabled = envConfig.reviewerEnabled;
643
690
  if (envConfig.providerEnv !== void 0)
@@ -662,9 +709,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
662
709
  merged.claudeModel = envConfig.claudeModel;
663
710
  if (envConfig.qa !== void 0)
664
711
  merged.qa = { ...merged.qa, ...envConfig.qa };
712
+ if (envConfig.audit !== void 0)
713
+ merged.audit = { ...merged.audit, ...envConfig.audit };
665
714
  if (envConfig.jobProviders !== void 0)
666
715
  merged.jobProviders = { ...envConfig.jobProviders };
667
716
  merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
717
+ merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
718
+ merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
668
719
  return merged;
669
720
  }
670
721
  function loadConfig(projectDir) {
@@ -730,6 +781,18 @@ function loadConfig(projectDir) {
730
781
  envConfig.maxRetries = retries;
731
782
  }
732
783
  }
784
+ if (process.env.NW_REVIEWER_MAX_RETRIES !== void 0) {
785
+ const reviewerMaxRetries = parseInt(process.env.NW_REVIEWER_MAX_RETRIES, 10);
786
+ if (!isNaN(reviewerMaxRetries) && reviewerMaxRetries >= 0) {
787
+ envConfig.reviewerMaxRetries = reviewerMaxRetries;
788
+ }
789
+ }
790
+ if (process.env.NW_REVIEWER_RETRY_DELAY !== void 0) {
791
+ const reviewerRetryDelay = parseInt(process.env.NW_REVIEWER_RETRY_DELAY, 10);
792
+ if (!isNaN(reviewerRetryDelay) && reviewerRetryDelay >= 0) {
793
+ envConfig.reviewerRetryDelay = reviewerRetryDelay;
794
+ }
795
+ }
733
796
  if (process.env.NW_PROVIDER) {
734
797
  const provider = validateProvider(process.env.NW_PROVIDER);
735
798
  if (provider !== null) {
@@ -742,6 +805,12 @@ function loadConfig(projectDir) {
742
805
  envConfig.reviewerEnabled = reviewerEnabled;
743
806
  }
744
807
  }
808
+ if (process.env.NW_EXECUTOR_ENABLED) {
809
+ const executorEnabled = parseBoolean(process.env.NW_EXECUTOR_ENABLED);
810
+ if (executorEnabled !== null) {
811
+ envConfig.executorEnabled = executorEnabled;
812
+ }
813
+ }
745
814
  if (process.env.NW_NOTIFICATIONS) {
746
815
  try {
747
816
  const parsed = JSON.parse(process.env.NW_NOTIFICATIONS);
@@ -2365,7 +2434,7 @@ var init_container = __esm({
2365
2434
  DATABASE_TOKEN = "Database";
2366
2435
  }
2367
2436
  });
2368
- function graphql(query, variables, cwd) {
2437
+ async function graphql(query, variables, cwd) {
2369
2438
  const args = ["api", "graphql", "-f", `query=${query}`];
2370
2439
  for (const [key, value] of Object.entries(variables)) {
2371
2440
  if (typeof value === "number") {
@@ -2374,10 +2443,9 @@ function graphql(query, variables, cwd) {
2374
2443
  args.push("-f", `${key}=${String(value)}`);
2375
2444
  }
2376
2445
  }
2377
- const output = execFileSync("gh", args, {
2446
+ const { stdout: output } = await execFileAsync("gh", args, {
2378
2447
  cwd,
2379
- encoding: "utf-8",
2380
- stdio: ["pipe", "pipe", "pipe"]
2448
+ encoding: "utf-8"
2381
2449
  });
2382
2450
  const parsed = JSON.parse(output);
2383
2451
  if (parsed.errors?.length) {
@@ -2385,25 +2453,29 @@ function graphql(query, variables, cwd) {
2385
2453
  }
2386
2454
  return parsed.data;
2387
2455
  }
2388
- function getRepoNwo(cwd) {
2389
- const output = execFileSync("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
2456
+ async function getRepoNwo(cwd) {
2457
+ const { stdout: output } = await execFileAsync("gh", ["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"], { cwd, encoding: "utf-8" });
2390
2458
  return output.trim();
2391
2459
  }
2392
- function getViewerLogin(cwd) {
2393
- const result = graphql(`query { viewer { login } }`, {}, cwd);
2460
+ async function getViewerLogin(cwd) {
2461
+ const result = await graphql(`query { viewer { login } }`, {}, cwd);
2394
2462
  return result.viewer.login;
2395
2463
  }
2464
+ var execFileAsync;
2396
2465
  var init_github_graphql = __esm({
2397
2466
  "../core/dist/board/providers/github-graphql.js"() {
2398
2467
  "use strict";
2468
+ execFileAsync = promisify(execFile);
2399
2469
  }
2400
2470
  });
2471
+ var execFileAsync2;
2401
2472
  var GitHubProjectsProvider;
2402
2473
  var init_github_projects = __esm({
2403
2474
  "../core/dist/board/providers/github-projects.js"() {
2404
2475
  "use strict";
2405
2476
  init_types2();
2406
2477
  init_github_graphql();
2478
+ execFileAsync2 = promisify2(execFile2);
2407
2479
  GitHubProjectsProvider = class {
2408
2480
  config;
2409
2481
  cwd;
@@ -2419,26 +2491,26 @@ var init_github_projects = __esm({
2419
2491
  // -------------------------------------------------------------------------
2420
2492
  // Helpers
2421
2493
  // -------------------------------------------------------------------------
2422
- getRepo() {
2494
+ async getRepo() {
2423
2495
  return this.config.repo ?? getRepoNwo(this.cwd);
2424
2496
  }
2425
- getRepoParts() {
2426
- const repo = this.getRepo();
2497
+ async getRepoParts() {
2498
+ const repo = await this.getRepo();
2427
2499
  const [owner, name] = repo.split("/");
2428
2500
  if (!owner || !name) {
2429
2501
  throw new Error(`Invalid repository slug: "${repo}". Expected "owner/repo".`);
2430
2502
  }
2431
2503
  return { owner, name };
2432
2504
  }
2433
- getRepoOwnerLogin() {
2434
- return this.getRepoParts().owner;
2505
+ async getRepoOwnerLogin() {
2506
+ return (await this.getRepoParts()).owner;
2435
2507
  }
2436
- getRepoOwner() {
2508
+ async getRepoOwner() {
2437
2509
  if (this.cachedOwner && this.cachedRepositoryId) {
2438
2510
  return this.cachedOwner;
2439
2511
  }
2440
- const { owner, name } = this.getRepoParts();
2441
- const data = graphql(`query ResolveRepoOwner($owner: String!, $name: String!) {
2512
+ const { owner, name } = await this.getRepoParts();
2513
+ const data = await graphql(`query ResolveRepoOwner($owner: String!, $name: String!) {
2442
2514
  repository(owner: $owner, name: $name) {
2443
2515
  id
2444
2516
  owner {
@@ -2463,20 +2535,20 @@ var init_github_projects = __esm({
2463
2535
  };
2464
2536
  return this.cachedOwner;
2465
2537
  }
2466
- getRepositoryNodeId() {
2538
+ async getRepositoryNodeId() {
2467
2539
  if (this.cachedRepositoryId) {
2468
2540
  return this.cachedRepositoryId;
2469
2541
  }
2470
- this.getRepoOwner();
2542
+ await this.getRepoOwner();
2471
2543
  if (!this.cachedRepositoryId) {
2472
- throw new Error(`Failed to resolve repository ID for ${this.getRepo()}.`);
2544
+ throw new Error(`Failed to resolve repository ID for ${await this.getRepo()}.`);
2473
2545
  }
2474
2546
  return this.cachedRepositoryId;
2475
2547
  }
2476
- linkProjectToRepository(projectId) {
2477
- const repositoryId = this.getRepositoryNodeId();
2548
+ async linkProjectToRepository(projectId) {
2549
+ const repositoryId = await this.getRepositoryNodeId();
2478
2550
  try {
2479
- graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
2551
+ await graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
2480
2552
  linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
2481
2553
  repository {
2482
2554
  id
@@ -2492,8 +2564,8 @@ var init_github_projects = __esm({
2492
2564
  throw err;
2493
2565
  }
2494
2566
  }
2495
- fetchStatusField(projectId) {
2496
- const fieldData = graphql(`query GetStatusField($projectId: ID!) {
2567
+ async fetchStatusField(projectId) {
2568
+ const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
2497
2569
  node(id: $projectId) {
2498
2570
  ... on ProjectV2 {
2499
2571
  field(name: "Status") {
@@ -2530,7 +2602,7 @@ var init_github_projects = __esm({
2530
2602
  };
2531
2603
  }
2532
2604
  if (this.cachedProjectId !== null) {
2533
- const statusField2 = this.fetchStatusField(this.cachedProjectId);
2605
+ const statusField2 = await this.fetchStatusField(this.cachedProjectId);
2534
2606
  this.cachedFieldId = statusField2.fieldId;
2535
2607
  this.cachedOptionIds = statusField2.optionIds;
2536
2608
  return {
@@ -2543,23 +2615,23 @@ var init_github_projects = __esm({
2543
2615
  if (!projectNumber) {
2544
2616
  throw new Error("No projectNumber configured. Run `night-watch board setup` first.");
2545
2617
  }
2546
- const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
2618
+ const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
2547
2619
  try {
2548
- ownerLogins.add(getViewerLogin(this.cwd));
2620
+ ownerLogins.add(await getViewerLogin(this.cwd));
2549
2621
  } catch {
2550
2622
  }
2551
2623
  let projectNode = null;
2552
2624
  for (const login of ownerLogins) {
2553
- projectNode = this.fetchProjectNode(login, projectNumber);
2625
+ projectNode = await this.fetchProjectNode(login, projectNumber);
2554
2626
  if (projectNode) {
2555
2627
  break;
2556
2628
  }
2557
2629
  }
2558
2630
  if (!projectNode) {
2559
- throw new Error(`GitHub Project #${projectNumber} not found for repository owner "${this.getRepoOwnerLogin()}".`);
2631
+ throw new Error(`GitHub Project #${projectNumber} not found for repository owner "${await this.getRepoOwnerLogin()}".`);
2560
2632
  }
2561
2633
  this.cachedProjectId = projectNode.id;
2562
- const statusField = this.fetchStatusField(projectNode.id);
2634
+ const statusField = await this.fetchStatusField(projectNode.id);
2563
2635
  this.cachedFieldId = statusField.fieldId;
2564
2636
  this.cachedOptionIds = statusField.optionIds;
2565
2637
  return {
@@ -2569,9 +2641,9 @@ var init_github_projects = __esm({
2569
2641
  };
2570
2642
  }
2571
2643
  /** Try user query first, fall back to org query. */
2572
- fetchProjectNode(login, projectNumber) {
2644
+ async fetchProjectNode(login, projectNumber) {
2573
2645
  try {
2574
- const userData = graphql(`query GetProject($login: String!, $number: Int!) {
2646
+ const userData = await graphql(`query GetProject($login: String!, $number: Int!) {
2575
2647
  user(login: $login) {
2576
2648
  projectV2(number: $number) {
2577
2649
  id
@@ -2587,7 +2659,7 @@ var init_github_projects = __esm({
2587
2659
  } catch {
2588
2660
  }
2589
2661
  try {
2590
- const orgData = graphql(`query GetOrgProject($login: String!, $number: Int!) {
2662
+ const orgData = await graphql(`query GetOrgProject($login: String!, $number: Int!) {
2591
2663
  organization(login: $login) {
2592
2664
  projectV2(number: $number) {
2593
2665
  id
@@ -2633,6 +2705,117 @@ var init_github_projects = __esm({
2633
2705
  assignees: content.assignees?.nodes.map((a) => a.login) ?? []
2634
2706
  };
2635
2707
  }
2708
+ /**
2709
+ * Fetch ALL items from a GitHub ProjectV2 using cursor-based pagination.
2710
+ *
2711
+ * The API caps each page at 100 items. We loop until `hasNextPage` is false,
2712
+ * accumulating every item node so callers never see a truncated board.
2713
+ */
2714
+ async fetchAllProjectItems(projectId) {
2715
+ const allNodes = [];
2716
+ let cursor = null;
2717
+ const query = `query GetProjectItems($projectId: ID!, $cursor: String) {
2718
+ node(id: $projectId) {
2719
+ ... on ProjectV2 {
2720
+ items(first: 100, after: $cursor) {
2721
+ pageInfo {
2722
+ hasNextPage
2723
+ endCursor
2724
+ }
2725
+ nodes {
2726
+ id
2727
+ content {
2728
+ ... on Issue {
2729
+ number
2730
+ title
2731
+ body
2732
+ url
2733
+ id
2734
+ labels(first: 10) { nodes { name } }
2735
+ assignees(first: 10) { nodes { login } }
2736
+ }
2737
+ }
2738
+ fieldValues(first: 10) {
2739
+ nodes {
2740
+ ... on ProjectV2ItemFieldSingleSelectValue {
2741
+ name
2742
+ field {
2743
+ ... on ProjectV2SingleSelectField {
2744
+ name
2745
+ }
2746
+ }
2747
+ }
2748
+ }
2749
+ }
2750
+ }
2751
+ }
2752
+ }
2753
+ }
2754
+ }`;
2755
+ do {
2756
+ const variables = { projectId };
2757
+ if (cursor !== null) {
2758
+ variables.cursor = cursor;
2759
+ }
2760
+ const data = await graphql(query, variables, this.cwd);
2761
+ const page = data.node.items;
2762
+ allNodes.push(...page.nodes);
2763
+ cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
2764
+ } while (cursor !== null);
2765
+ return allNodes;
2766
+ }
2767
+ /**
2768
+ * Fetch project items for moveIssue — only needs id, content.number, and
2769
+ * fieldValues. Uses the same paginated approach to ensure items beyond
2770
+ * position 100 are reachable.
2771
+ */
2772
+ async fetchAllProjectItemsForMove(projectId) {
2773
+ const allNodes = [];
2774
+ let cursor = null;
2775
+ const query = `query GetProjectItemsForMove($projectId: ID!, $cursor: String) {
2776
+ node(id: $projectId) {
2777
+ ... on ProjectV2 {
2778
+ items(first: 100, after: $cursor) {
2779
+ pageInfo {
2780
+ hasNextPage
2781
+ endCursor
2782
+ }
2783
+ nodes {
2784
+ id
2785
+ content {
2786
+ ... on Issue {
2787
+ number
2788
+ }
2789
+ }
2790
+ fieldValues(first: 10) {
2791
+ nodes {
2792
+ ... on ProjectV2ItemFieldSingleSelectValue {
2793
+ name
2794
+ field {
2795
+ ... on ProjectV2SingleSelectField {
2796
+ name
2797
+ }
2798
+ }
2799
+ }
2800
+ }
2801
+ }
2802
+ }
2803
+ }
2804
+ }
2805
+ }
2806
+ }`;
2807
+ do {
2808
+ const variables = { projectId };
2809
+ if (cursor !== null) {
2810
+ variables.cursor = cursor;
2811
+ }
2812
+ const data = await graphql(query, variables, this.cwd);
2813
+ const page = data.node.items;
2814
+ allNodes.push(...page.nodes);
2815
+ cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
2816
+ } while (cursor !== null);
2817
+ return allNodes;
2818
+ }
2636
2819
  // -------------------------------------------------------------------------
2637
2820
  // IBoardProvider implementation
2638
2821
  // -------------------------------------------------------------------------
@@ -2640,10 +2823,10 @@ var init_github_projects = __esm({
2640
2823
  * Find an existing project by title among the repository owner's first 50 projects.
2641
2824
  * Returns null if not found.
2642
2825
  */
2643
- findExistingProject(owner, title) {
2826
+ async findExistingProject(owner, title) {
2644
2827
  try {
2645
2828
  if (owner.type === "User") {
2646
- const data2 = graphql(`query ListUserProjects($login: String!) {
2829
+ const data2 = await graphql(`query ListUserProjects($login: String!) {
2647
2830
  user(login: $login) {
2648
2831
  projectsV2(first: 50) {
2649
2832
  nodes { id number title url }
@@ -2652,7 +2835,7 @@ var init_github_projects = __esm({
2652
2835
  }`, { login: owner.login }, this.cwd);
2653
2836
  return data2.user?.projectsV2.nodes.find((p) => p.title === title) ?? null;
2654
2837
  }
2655
- const data = graphql(`query ListOrgProjects($login: String!) {
2838
+ const data = await graphql(`query ListOrgProjects($login: String!) {
2656
2839
  organization(login: $login) {
2657
2840
  projectsV2(first: 50) {
2658
2841
  nodes { id number title url }
@@ -2668,8 +2851,8 @@ var init_github_projects = __esm({
2668
2851
  * Ensure the Status field on an existing project has all five Night Watch
2669
2852
  * lifecycle columns, updating it via GraphQL if any are missing.
2670
2853
  */
2671
- ensureStatusColumns(projectId) {
2672
- const fieldData = graphql(`query GetStatusField($projectId: ID!) {
2854
+ async ensureStatusColumns(projectId) {
2855
+ const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
2673
2856
  node(id: $projectId) {
2674
2857
  ... on ProjectV2 {
2675
2858
  field(name: "Status") {
@@ -2701,7 +2884,7 @@ var init_github_projects = __esm({
2701
2884
  color: colorMap[name],
2702
2885
  description: ""
2703
2886
  }));
2704
- graphql(`mutation UpdateField($fieldId: ID!) {
2887
+ await graphql(`mutation UpdateField($fieldId: ID!) {
2705
2888
  updateProjectV2Field(input: {
2706
2889
  fieldId: $fieldId,
2707
2890
  singleSelectOptions: [
@@ -2722,15 +2905,15 @@ var init_github_projects = __esm({
2722
2905
  }`, { fieldId: field.id, allOptions }, this.cwd);
2723
2906
  }
2724
2907
  async setupBoard(title) {
2725
- const owner = this.getRepoOwner();
2726
- const existing = this.findExistingProject(owner, title);
2908
+ const owner = await this.getRepoOwner();
2909
+ const existing = await this.findExistingProject(owner, title);
2727
2910
  if (existing) {
2728
2911
  this.cachedProjectId = existing.id;
2729
- this.linkProjectToRepository(existing.id);
2730
- this.ensureStatusColumns(existing.id);
2912
+ await this.linkProjectToRepository(existing.id);
2913
+ await this.ensureStatusColumns(existing.id);
2731
2914
  return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
2732
2915
  }
2733
- const createData = graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
2916
+ const createData = await graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
2734
2917
  createProjectV2(input: { ownerId: $ownerId, title: $title }) {
2735
2918
  projectV2 {
2736
2919
  id
@@ -2742,13 +2925,13 @@ var init_github_projects = __esm({
2742
2925
  }`, { ownerId: owner.id, title }, this.cwd);
2743
2926
  const project = createData.createProjectV2.projectV2;
2744
2927
  this.cachedProjectId = project.id;
2745
- this.linkProjectToRepository(project.id);
2928
+ await this.linkProjectToRepository(project.id);
2746
2929
  try {
2747
- const statusField = this.fetchStatusField(project.id);
2930
+ const statusField = await this.fetchStatusField(project.id);
2748
2931
  this.cachedFieldId = statusField.fieldId;
2749
2932
  this.cachedOptionIds = statusField.optionIds;
2750
- this.ensureStatusColumns(project.id);
2751
- const refreshed = this.fetchStatusField(project.id);
2933
+ await this.ensureStatusColumns(project.id);
2934
+ const refreshed = await this.fetchStatusField(project.id);
2752
2935
  this.cachedFieldId = refreshed.fieldId;
2753
2936
  this.cachedOptionIds = refreshed.optionIds;
2754
2937
  } catch (err) {
@@ -2756,7 +2939,7 @@ var init_github_projects = __esm({
2756
2939
  if (!message.includes("Status field not found")) {
2757
2940
  throw err;
2758
2941
  }
2759
- const createFieldData = graphql(`mutation CreateStatusField($projectId: ID!) {
2942
+ const createFieldData = await graphql(`mutation CreateStatusField($projectId: ID!) {
2760
2943
  createProjectV2Field(input: {
2761
2944
  projectId: $projectId,
2762
2945
  dataType: SINGLE_SELECT,
@@ -2789,14 +2972,14 @@ var init_github_projects = __esm({
2789
2972
  return null;
2790
2973
  }
2791
2974
  try {
2792
- const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
2975
+ const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
2793
2976
  try {
2794
- ownerLogins.add(getViewerLogin(this.cwd));
2977
+ ownerLogins.add(await getViewerLogin(this.cwd));
2795
2978
  } catch {
2796
2979
  }
2797
2980
  let node = null;
2798
2981
  for (const login of ownerLogins) {
2799
- node = this.fetchProjectNode(login, projectNumber);
2982
+ node = await this.fetchProjectNode(login, projectNumber);
2800
2983
  if (node) {
2801
2984
  break;
2802
2985
  }
@@ -2817,7 +3000,7 @@ var init_github_projects = __esm({
2817
3000
  }));
2818
3001
  }
2819
3002
  async createIssue(input) {
2820
- const repo = this.getRepo();
3003
+ const repo = await this.getRepo();
2821
3004
  const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2822
3005
  const issueArgs = [
2823
3006
  "issue",
@@ -2832,19 +3015,20 @@ var init_github_projects = __esm({
2832
3015
  if (input.labels && input.labels.length > 0) {
2833
3016
  issueArgs.push("--label", input.labels.join(","));
2834
3017
  }
2835
- const issueUrl = execFileSync2("gh", issueArgs, {
3018
+ const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
2836
3019
  cwd: this.cwd,
2837
- encoding: "utf-8",
2838
- stdio: ["pipe", "pipe", "pipe"]
2839
- }).trim();
3020
+ encoding: "utf-8"
3021
+ });
3022
+ const issueUrl = issueUrlRaw.trim();
2840
3023
  const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
2841
3024
  if (!issueNumber) {
2842
3025
  throw new Error(`Failed to parse issue number from URL: ${issueUrl}`);
2843
3026
  }
2844
3027
  const [owner, repoName] = repo.split("/");
2845
- const nodeIdOutput = execFileSync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3028
+ const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
3029
+ const nodeIdOutput = nodeIdRaw.trim();
2846
3030
  const issueJson = { number: issueNumber, id: nodeIdOutput, url: issueUrl };
2847
- const addData = graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
3031
+ const addData = await graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2848
3032
  addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2849
3033
  item {
2850
3034
  id
@@ -2855,7 +3039,7 @@ var init_github_projects = __esm({
2855
3039
  const targetColumn = input.column ?? "Draft";
2856
3040
  const optionId = optionIds.get(targetColumn);
2857
3041
  if (optionId) {
2858
- graphql(`mutation UpdateItemField(
3042
+ await graphql(`mutation UpdateItemField(
2859
3043
  $projectId: ID!,
2860
3044
  $itemId: ID!,
2861
3045
  $fieldId: ID!,
@@ -2889,10 +3073,10 @@ var init_github_projects = __esm({
2889
3073
  };
2890
3074
  }
2891
3075
  async getIssue(issueNumber) {
2892
- const repo = this.getRepo();
3076
+ const repo = await this.getRepo();
2893
3077
  let rawIssue;
2894
3078
  try {
2895
- const output = execFileSync2("gh", [
3079
+ const { stdout: output } = await execFileAsync2("gh", [
2896
3080
  "issue",
2897
3081
  "view",
2898
3082
  String(issueNumber),
@@ -2900,7 +3084,7 @@ var init_github_projects = __esm({
2900
3084
  repo,
2901
3085
  "--json",
2902
3086
  "number,title,body,url,id,labels,assignees"
2903
- ], { cwd: this.cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3087
+ ], { cwd: this.cwd, encoding: "utf-8" });
2904
3088
  rawIssue = JSON.parse(output);
2905
3089
  } catch {
2906
3090
  return null;
@@ -2931,42 +3115,9 @@ var init_github_projects = __esm({
2931
3115
  }
2932
3116
  async getAllIssues() {
2933
3117
  const { projectId } = await this.ensureProjectCache();
2934
- const data = graphql(`query GetProjectItems($projectId: ID!) {
2935
- node(id: $projectId) {
2936
- ... on ProjectV2 {
2937
- items(first: 100) {
2938
- nodes {
2939
- id
2940
- content {
2941
- ... on Issue {
2942
- number
2943
- title
2944
- body
2945
- url
2946
- id
2947
- labels(first: 10) { nodes { name } }
2948
- assignees(first: 10) { nodes { login } }
2949
- }
2950
- }
2951
- fieldValues(first: 10) {
2952
- nodes {
2953
- ... on ProjectV2ItemFieldSingleSelectValue {
2954
- name
2955
- field {
2956
- ... on ProjectV2SingleSelectField {
2957
- name
2958
- }
2959
- }
2960
- }
2961
- }
2962
- }
2963
- }
2964
- }
2965
- }
2966
- }
2967
- }`, { projectId }, this.cwd);
3118
+ const allNodes = await this.fetchAllProjectItems(projectId);
2968
3119
  const results = [];
2969
- for (const item of data.node.items.nodes) {
3120
+ for (const item of allNodes) {
2970
3121
  const parsed = this.parseItem(item);
2971
3122
  if (parsed) {
2972
3123
  results.push(parsed);
@@ -2976,35 +3127,8 @@ var init_github_projects = __esm({
2976
3127
  }
2977
3128
  async moveIssue(issueNumber, targetColumn) {
2978
3129
  const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2979
- const data = graphql(`query GetProjectItems($projectId: ID!) {
2980
- node(id: $projectId) {
2981
- ... on ProjectV2 {
2982
- items(first: 100) {
2983
- nodes {
2984
- id
2985
- content {
2986
- ... on Issue {
2987
- number
2988
- }
2989
- }
2990
- fieldValues(first: 10) {
2991
- nodes {
2992
- ... on ProjectV2ItemFieldSingleSelectValue {
2993
- name
2994
- field {
2995
- ... on ProjectV2SingleSelectField {
2996
- name
2997
- }
2998
- }
2999
- }
3000
- }
3001
- }
3002
- }
3003
- }
3004
- }
3005
- }
3006
- }`, { projectId }, this.cwd);
3007
- const itemNode = data.node.items.nodes.find((n) => n.content?.number === issueNumber);
3130
+ const allNodes = await this.fetchAllProjectItemsForMove(projectId);
3131
+ const itemNode = allNodes.find((n) => n.content?.number === issueNumber);
3008
3132
  if (!itemNode) {
3009
3133
  throw new Error(`Issue #${issueNumber} not found on the project board.`);
3010
3134
  }
@@ -3012,7 +3136,7 @@ var init_github_projects = __esm({
3012
3136
  if (!optionId) {
3013
3137
  throw new Error(`Column "${targetColumn}" not found on the project board.`);
3014
3138
  }
3015
- graphql(`mutation UpdateItemField(
3139
+ await graphql(`mutation UpdateItemField(
3016
3140
  $projectId: ID!,
3017
3141
  $itemId: ID!,
3018
3142
  $fieldId: ID!,
@@ -3031,12 +3155,12 @@ var init_github_projects = __esm({
3031
3155
  }`, { projectId, itemId: itemNode.id, fieldId, optionId }, this.cwd);
3032
3156
  }
3033
3157
  async closeIssue(issueNumber) {
3034
- const repo = this.getRepo();
3035
- execFileSync2("gh", ["issue", "close", String(issueNumber), "--repo", repo], { cwd: this.cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3158
+ const repo = await this.getRepo();
3159
+ await execFileAsync2("gh", ["issue", "close", String(issueNumber), "--repo", repo], { cwd: this.cwd, encoding: "utf-8" });
3036
3160
  }
3037
3161
  async commentOnIssue(issueNumber, body) {
3038
- const repo = this.getRepo();
3039
- execFileSync2("gh", ["issue", "comment", String(issueNumber), "--repo", repo, "--body", body], { cwd: this.cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3162
+ const repo = await this.getRepo();
3163
+ await execFileAsync2("gh", ["issue", "comment", String(issueNumber), "--repo", repo, "--body", body], { cwd: this.cwd, encoding: "utf-8" });
3040
3164
  }
3041
3165
  };
3042
3166
  }
@@ -3497,7 +3621,7 @@ function collectPrdDirs(projectPaths) {
3497
3621
  const prdDirs = [];
3498
3622
  for (const projectPath of projectPaths) {
3499
3623
  const configPath = path3.join(projectPath, CONFIG_FILE_NAME);
3500
- let prdDir = "docs/PRDs/night-watch";
3624
+ let prdDir = "docs/prds";
3501
3625
  if (fs3.existsSync(configPath)) {
3502
3626
  try {
3503
3627
  const config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
@@ -4127,9 +4251,15 @@ function executorLockPath(projectDir) {
4127
4251
  function reviewerLockPath(projectDir) {
4128
4252
  return `${LOCK_FILE_PREFIX}pr-reviewer-${projectRuntimeKey(projectDir)}.lock`;
4129
4253
  }
4254
+ function qaLockPath(projectDir) {
4255
+ return `${LOCK_FILE_PREFIX}qa-${projectRuntimeKey(projectDir)}.lock`;
4256
+ }
4130
4257
  function auditLockPath(projectDir) {
4131
4258
  return `${LOCK_FILE_PREFIX}audit-${projectRuntimeKey(projectDir)}.lock`;
4132
4259
  }
4260
+ function plannerLockPath(projectDir) {
4261
+ return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
4262
+ }
4133
4263
  function isProcessRunning(pid) {
4134
4264
  try {
4135
4265
  process.kill(pid, 0);
@@ -4306,24 +4436,26 @@ function collectPrdInfo(projectDir, prdDir, maxRuntime) {
4306
4436
  }
4307
4437
  return prds;
4308
4438
  }
4309
- function countOpenPRs(projectDir, branchPatterns) {
4439
+ async function countOpenPRs(projectDir, branchPatterns) {
4310
4440
  try {
4311
- execSync2("git rev-parse --git-dir", {
4441
+ await execAsync("git rev-parse --git-dir", {
4312
4442
  cwd: projectDir,
4313
- encoding: "utf-8",
4314
- stdio: ["pipe", "pipe", "pipe"]
4443
+ encoding: "utf-8"
4315
4444
  });
4316
4445
  try {
4317
- execSync2("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4446
+ await execAsync("which gh", { encoding: "utf-8" });
4318
4447
  } catch {
4319
4448
  return 0;
4320
4449
  }
4321
- const output = execSync2("gh pr list --state open --json headRefName,number", {
4450
+ const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number", {
4322
4451
  cwd: projectDir,
4323
- encoding: "utf-8",
4324
- stdio: ["pipe", "pipe", "pipe"]
4452
+ encoding: "utf-8"
4325
4453
  });
4326
- const prs = JSON.parse(output);
4454
+ const trimmed = output.trim();
4455
+ if (!trimmed || trimmed === "[]") {
4456
+ return 0;
4457
+ }
4458
+ const prs = JSON.parse(trimmed);
4327
4459
  const matchingPRs = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
4328
4460
  return matchingPRs.length;
4329
4461
  } catch {
@@ -4389,27 +4521,29 @@ function deriveReviewScore(reviewDecision) {
4389
4521
  return null;
4390
4522
  }
4391
4523
  }
4392
- function collectPrInfo(projectDir, branchPatterns) {
4524
+ async function collectPrInfo(projectDir, branchPatterns) {
4393
4525
  try {
4394
- execSync2("git rev-parse --git-dir", {
4526
+ await execAsync("git rev-parse --git-dir", {
4395
4527
  cwd: projectDir,
4396
- encoding: "utf-8",
4397
- stdio: ["pipe", "pipe", "pipe"]
4528
+ encoding: "utf-8"
4398
4529
  });
4399
4530
  try {
4400
- execSync2("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4531
+ await execAsync("which gh", { encoding: "utf-8" });
4401
4532
  } catch {
4402
4533
  return [];
4403
4534
  }
4404
- const output = execSync2("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision", {
4535
+ const { stdout: output } = await execAsync("gh pr list --state open --json headRefName,number,title,url,statusCheckRollup,reviewDecision", {
4405
4536
  cwd: projectDir,
4406
- encoding: "utf-8",
4407
- stdio: ["pipe", "pipe", "pipe"]
4537
+ encoding: "utf-8"
4408
4538
  });
4409
4539
  if (process.env.DEBUG_PR_DATA === "1") {
4410
4540
  console.error("[DEBUG] Raw gh pr list output:", output);
4411
4541
  }
4412
- const prs = JSON.parse(output);
4542
+ const trimmed = output.trim();
4543
+ if (!trimmed || trimmed === "[]") {
4544
+ return [];
4545
+ }
4546
+ const prs = JSON.parse(trimmed);
4413
4547
  return prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern))).map((pr) => {
4414
4548
  if (process.env.DEBUG_PR_DATA === "1") {
4415
4549
  console.error(`[DEBUG] PR #${pr.number}:`);
@@ -4454,7 +4588,9 @@ function collectLogInfo(projectDir) {
4454
4588
  const logEntries = [
4455
4589
  { name: "executor", fileName: "executor.log" },
4456
4590
  { name: "reviewer", fileName: "reviewer.log" },
4457
- { name: "qa", fileName: `${QA_LOG_NAME}.log` }
4591
+ { name: "qa", fileName: `${QA_LOG_NAME}.log` },
4592
+ { name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
4593
+ { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
4458
4594
  ];
4459
4595
  return logEntries.map(({ name, fileName }) => {
4460
4596
  const logPath = path5.join(projectDir, LOG_DIR, fileName);
@@ -4476,16 +4612,22 @@ function getCrontabInfo(projectName, projectDir) {
4476
4612
  entries: crontabEntries
4477
4613
  };
4478
4614
  }
4479
- function fetchStatusSnapshot(projectDir, config) {
4615
+ async function fetchStatusSnapshot(projectDir, config) {
4480
4616
  const projectName = getProjectName(projectDir);
4481
4617
  const executorLock = checkLockFile(executorLockPath(projectDir));
4482
4618
  const reviewerLock = checkLockFile(reviewerLockPath(projectDir));
4619
+ const qaLock = checkLockFile(qaLockPath(projectDir));
4620
+ const auditLock = checkLockFile(auditLockPath(projectDir));
4621
+ const plannerLock = checkLockFile(plannerLockPath(projectDir));
4483
4622
  const processes = [
4484
4623
  { name: "executor", running: executorLock.running, pid: executorLock.pid },
4485
- { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid }
4624
+ { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
4625
+ { name: "qa", running: qaLock.running, pid: qaLock.pid },
4626
+ { name: "audit", running: auditLock.running, pid: auditLock.pid },
4627
+ { name: "planner", running: plannerLock.running, pid: plannerLock.pid }
4486
4628
  ];
4487
4629
  const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
4488
- const prs = collectPrInfo(projectDir, config.branchPatterns);
4630
+ const prs = await collectPrInfo(projectDir, config.branchPatterns);
4489
4631
  const logs = collectLogInfo(projectDir);
4490
4632
  const crontab = getCrontabInfo(projectName, projectDir);
4491
4633
  const activePrd = prds.find((p) => p.status === "in-progress")?.name ?? null;
@@ -4502,12 +4644,14 @@ function fetchStatusSnapshot(projectDir, config) {
4502
4644
  timestamp: /* @__PURE__ */ new Date()
4503
4645
  };
4504
4646
  }
4647
+ var execAsync;
4505
4648
  var init_status_data = __esm({
4506
4649
  "../core/dist/utils/status-data.js"() {
4507
4650
  "use strict";
4508
4651
  init_constants();
4509
4652
  init_prd_states();
4510
4653
  init_crontab();
4654
+ execAsync = promisify3(exec);
4511
4655
  }
4512
4656
  });
4513
4657
  function getLockFilePaths(projectDir) {
@@ -4626,7 +4770,7 @@ function checkGitRepo(cwd) {
4626
4770
  }
4627
4771
  function checkGhCli() {
4628
4772
  try {
4629
- execSync3("gh auth status", {
4773
+ execSync2("gh auth status", {
4630
4774
  encoding: "utf-8",
4631
4775
  stdio: ["pipe", "pipe", "pipe"]
4632
4776
  });
@@ -4645,7 +4789,7 @@ function checkGhCli() {
4645
4789
  }
4646
4790
  function checkProviderCli(provider) {
4647
4791
  try {
4648
- execSync3(`which ${provider}`, {
4792
+ execSync2(`which ${provider}`, {
4649
4793
  encoding: "utf-8",
4650
4794
  stdio: ["pipe", "pipe", "pipe"]
4651
4795
  });
@@ -4666,7 +4810,7 @@ function detectProviders() {
4666
4810
  const providers = [];
4667
4811
  for (const provider of VALID_PROVIDERS) {
4668
4812
  try {
4669
- execSync3(`which ${provider}`, {
4813
+ execSync2(`which ${provider}`, {
4670
4814
  encoding: "utf-8",
4671
4815
  stdio: ["pipe", "pipe", "pipe"]
4672
4816
  });
@@ -4760,7 +4904,7 @@ function checkLogsDirectory(projectDir) {
4760
4904
  }
4761
4905
  function checkCrontabAccess() {
4762
4906
  try {
4763
- execSync3("crontab -l", {
4907
+ execSync2("crontab -l", {
4764
4908
  encoding: "utf-8",
4765
4909
  stdio: ["pipe", "pipe", "pipe"]
4766
4910
  });
@@ -4910,7 +5054,7 @@ function parsePrDetails(raw) {
4910
5054
  }
4911
5055
  function fetchPrBySelector(selector, cwd) {
4912
5056
  try {
4913
- const output = execFileSync3("gh", ["pr", "view", selector, "--json", "number,title,url,body,additions,deletions,changedFiles"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5057
+ const output = execFileSync("gh", ["pr", "view", selector, "--json", "number,title,url,body,additions,deletions,changedFiles"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4914
5058
  return parsePrDetails(output);
4915
5059
  } catch {
4916
5060
  return null;
@@ -4924,7 +5068,7 @@ function fetchPrDetailsByNumber(prNumber, cwd) {
4924
5068
  }
4925
5069
  function fetchPrDetails(branchPrefix, cwd) {
4926
5070
  try {
4927
- const listOutput = execFileSync3("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5071
+ const listOutput = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4928
5072
  const prs = JSON.parse(listOutput);
4929
5073
  const matching = prs.filter((pr) => pr.headRefName.startsWith(branchPrefix + "/"));
4930
5074
  if (matching.length === 0) {
@@ -4938,7 +5082,7 @@ function fetchPrDetails(branchPrefix, cwd) {
4938
5082
  }
4939
5083
  function fetchReviewedPrDetails(branchPatterns, cwd) {
4940
5084
  try {
4941
- const listOutput = execFileSync3("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5085
+ const listOutput = execFileSync("gh", ["pr", "list", "--state", "open", "--json", "number,headRefName", "--limit", "20"], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4942
5086
  const prs = JSON.parse(listOutput);
4943
5087
  const matching = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
4944
5088
  if (matching.length === 0) {
@@ -5138,6 +5282,14 @@ function buildDescription(ctx) {
5138
5282
  if (ctx.duration !== void 0) {
5139
5283
  lines.push(`Duration: ${ctx.duration}s`);
5140
5284
  }
5285
+ if (ctx.event === "review_completed" && ctx.attempts !== void 0 && ctx.attempts > 1) {
5286
+ const retryInfo = `Attempts: ${ctx.attempts}`;
5287
+ if (ctx.finalScore !== void 0) {
5288
+ lines.push(`${retryInfo} (final score: ${ctx.finalScore}/100)`);
5289
+ } else {
5290
+ lines.push(retryInfo);
5291
+ }
5292
+ }
5141
5293
  return lines.join("\n");
5142
5294
  }
5143
5295
  function escapeMarkdownV2(text) {
@@ -5192,7 +5344,7 @@ function formatDiscordPayload(ctx) {
5192
5344
  }
5193
5345
  function formatTelegramPayload(ctx) {
5194
5346
  const emoji = getEventEmoji(ctx.event);
5195
- const title = getEventTitle(ctx.event);
5347
+ const title = ctx.event === "run_succeeded" ? "PR Opened" : getEventTitle(ctx.event);
5196
5348
  if (ctx.prUrl && ctx.prTitle) {
5197
5349
  const lines = [];
5198
5350
  lines.push(`*${escapeMarkdownV2(emoji + " " + title)}*`);
@@ -5219,6 +5371,14 @@ function formatTelegramPayload(ctx) {
5219
5371
  }
5220
5372
  lines.push(escapeMarkdownV2(stats.join(" | ")));
5221
5373
  }
5374
+ if (ctx.event === "review_completed" && ctx.attempts !== void 0 && ctx.attempts > 1) {
5375
+ lines.push("");
5376
+ if (ctx.finalScore !== void 0) {
5377
+ lines.push(escapeMarkdownV2(`\u{1F501} Attempts: ${ctx.attempts} (final score: ${ctx.finalScore}/100)`));
5378
+ } else {
5379
+ lines.push(escapeMarkdownV2(`\u{1F501} Attempts: ${ctx.attempts}`));
5380
+ }
5381
+ }
5222
5382
  lines.push("");
5223
5383
  lines.push(escapeMarkdownV2(`\u2699\uFE0F Project: ${ctx.projectName} | Provider: ${ctx.provider}`));
5224
5384
  return {
@@ -5696,6 +5856,8 @@ The PRD directory is: \`{{PRD_DIR}}\`
5696
5856
 
5697
5857
  ## Your Task
5698
5858
 
5859
+ 0. **Load Planner Skill** - Read and apply \`.claude/skills/prd-creator/SKILL.md\` before writing the PRD. If unavailable, continue with this template.
5860
+
5699
5861
  1. **Explore the Codebase** - Read relevant existing files to understand the project structure, patterns, and conventions.
5700
5862
 
5701
5863
  2. **Assess Complexity** - Score the complexity using the rubric and determine whether this is LOW, MEDIUM, or HIGH complexity.
@@ -5755,6 +5917,116 @@ DO NOT forget to write the file.
5755
5917
  cachedTemplate = null;
5756
5918
  }
5757
5919
  });
5920
+ function normalizeAuditSeverity(raw) {
5921
+ const normalized = raw.trim().toLowerCase();
5922
+ if (normalized === "critical")
5923
+ return "critical";
5924
+ if (normalized === "high")
5925
+ return "high";
5926
+ if (normalized === "low")
5927
+ return "low";
5928
+ return "medium";
5929
+ }
5930
+ function extractAuditField(block, field) {
5931
+ const pattern = new RegExp(`- \\*\\*${field}\\*\\*:\\s*([\\s\\S]*?)(?=\\n- \\*\\*|\\n###\\s+Finding\\s+\\d+|$)`, "i");
5932
+ const match = block.match(pattern);
5933
+ if (!match)
5934
+ return "";
5935
+ return match[1].replace(/`/g, "").replace(/\r/g, "").trim();
5936
+ }
5937
+ function parseAuditFindings(reportContent) {
5938
+ const headingRegex = /^###\s+Finding\s+(\d+)\s*$/gm;
5939
+ const headings = [];
5940
+ let match;
5941
+ while ((match = headingRegex.exec(reportContent)) !== null) {
5942
+ const number = parseInt(match[1], 10);
5943
+ if (!Number.isNaN(number)) {
5944
+ headings.push({
5945
+ number,
5946
+ bodyStart: headingRegex.lastIndex,
5947
+ headingStart: match.index
5948
+ });
5949
+ }
5950
+ }
5951
+ if (headings.length === 0) {
5952
+ return [];
5953
+ }
5954
+ const findings = [];
5955
+ for (let i = 0; i < headings.length; i++) {
5956
+ const current = headings[i];
5957
+ const next = headings[i + 1];
5958
+ const block = reportContent.slice(current.bodyStart, next?.headingStart ?? reportContent.length);
5959
+ const severityRaw = extractAuditField(block, "Severity");
5960
+ const category = extractAuditField(block, "Category") || "uncategorized";
5961
+ const location = extractAuditField(block, "Location") || "unknown location";
5962
+ const description = extractAuditField(block, "Description") || "No description provided";
5963
+ const suggestedFix = extractAuditField(block, "Suggested Fix") || "No suggested fix provided";
5964
+ findings.push({
5965
+ number: current.number,
5966
+ severity: normalizeAuditSeverity(severityRaw),
5967
+ category,
5968
+ location,
5969
+ description,
5970
+ suggestedFix
5971
+ });
5972
+ }
5973
+ return findings;
5974
+ }
5975
+ function auditSeverityRank(severity) {
5976
+ switch (severity) {
5977
+ case "critical":
5978
+ return 0;
5979
+ case "high":
5980
+ return 1;
5981
+ case "medium":
5982
+ return 2;
5983
+ case "low":
5984
+ return 3;
5985
+ default:
5986
+ return 4;
5987
+ }
5988
+ }
5989
+ function auditFindingToRoadmapItem(finding) {
5990
+ const title = `Audit Finding ${finding.number}: ${finding.category} (${finding.severity}) at ${finding.location}`;
5991
+ const hashSource = [
5992
+ finding.severity,
5993
+ finding.category,
5994
+ finding.location,
5995
+ finding.description,
5996
+ finding.suggestedFix
5997
+ ].join("|");
5998
+ const hash = createHash3("sha256").update(hashSource).digest("hex").slice(0, 8);
5999
+ const description = [
6000
+ "Source: logs/audit-report.md",
6001
+ `Severity: ${finding.severity}`,
6002
+ `Category: ${finding.category}`,
6003
+ `Location: ${finding.location}`,
6004
+ "",
6005
+ finding.description,
6006
+ "",
6007
+ `Suggested fix: ${finding.suggestedFix}`
6008
+ ].join("\n");
6009
+ return {
6010
+ hash,
6011
+ title,
6012
+ description,
6013
+ checked: false,
6014
+ section: "Audit Findings"
6015
+ };
6016
+ }
6017
+ function collectAuditPlannerItems(projectDir) {
6018
+ const reportPath = path12.join(projectDir, "logs", "audit-report.md");
6019
+ if (!fs13.existsSync(reportPath)) {
6020
+ return [];
6021
+ }
6022
+ const reportContent = fs13.readFileSync(reportPath, "utf-8");
6023
+ if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
6024
+ return [];
6025
+ }
6026
+ const findings = parseAuditFindings(reportContent);
6027
+ findings.sort((a, b) => auditSeverityRank(a.severity) - auditSeverityRank(b.severity) || a.number - b.number);
6028
+ return findings.map(auditFindingToRoadmapItem);
6029
+ }
5758
6030
  function getRoadmapStatus(projectDir, config) {
5759
6031
  const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
5760
6032
  const scannerEnabled = config.roadmapScanner.enabled;
@@ -5920,15 +6192,16 @@ async function sliceNextItem(projectDir, config) {
5920
6192
  };
5921
6193
  }
5922
6194
  const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
5923
- if (!fs13.existsSync(roadmapPath)) {
6195
+ const auditItems = collectAuditPlannerItems(projectDir);
6196
+ const roadmapExists = fs13.existsSync(roadmapPath);
6197
+ if (!roadmapExists && auditItems.length === 0) {
5924
6198
  return {
5925
6199
  sliced: false,
5926
6200
  error: "ROADMAP.md not found"
5927
6201
  };
5928
6202
  }
5929
- const content = fs13.readFileSync(roadmapPath, "utf-8");
5930
- const items = parseRoadmap(content);
5931
- if (items.length === 0) {
6203
+ const roadmapItems = roadmapExists ? parseRoadmap(fs13.readFileSync(roadmapPath, "utf-8")) : [];
6204
+ if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
5932
6205
  return {
5933
6206
  sliced: false,
5934
6207
  error: "No items in roadmap"
@@ -5937,21 +6210,23 @@ async function sliceNextItem(projectDir, config) {
5937
6210
  const prdDir = path12.join(projectDir, config.prdDir);
5938
6211
  const state = loadRoadmapState(prdDir);
5939
6212
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5940
- let targetItem;
5941
- for (const item of items) {
5942
- if (item.checked) {
5943
- continue;
5944
- }
5945
- if (isItemProcessed(state, item.hash)) {
5946
- continue;
5947
- }
5948
- const itemSlug = slugify(item.title);
5949
- if (existingPrdSlugs.has(itemSlug)) {
5950
- continue;
6213
+ const pickEligibleItem = (items) => {
6214
+ for (const item of items) {
6215
+ if (item.checked) {
6216
+ continue;
6217
+ }
6218
+ if (isItemProcessed(state, item.hash)) {
6219
+ continue;
6220
+ }
6221
+ const itemSlug = slugify(item.title);
6222
+ if (existingPrdSlugs.has(itemSlug)) {
6223
+ continue;
6224
+ }
6225
+ return item;
5951
6226
  }
5952
- targetItem = item;
5953
- break;
5954
- }
6227
+ return void 0;
6228
+ };
6229
+ const targetItem = pickEligibleItem(auditItems) ?? pickEligibleItem(roadmapItems);
5955
6230
  if (!targetItem) {
5956
6231
  return {
5957
6232
  sliced: false,
@@ -6050,11 +6325,11 @@ var init_script_result = __esm({
6050
6325
  RESULT_PREFIX = "NIGHT_WATCH_RESULT:";
6051
6326
  }
6052
6327
  });
6053
- async function executeScript(scriptPath, args = [], env = {}) {
6054
- const result = await executeScriptWithOutput(scriptPath, args, env);
6328
+ async function executeScript(scriptPath, args = [], env = {}, options = {}) {
6329
+ const result = await executeScriptWithOutput(scriptPath, args, env, options);
6055
6330
  return result.exitCode;
6056
6331
  }
6057
- async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
6332
+ async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
6058
6333
  return new Promise((resolve9, reject) => {
6059
6334
  const childEnv = {
6060
6335
  ...process.env,
@@ -6064,6 +6339,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
6064
6339
  const stderrChunks = [];
6065
6340
  const child = spawn2("bash", [scriptPath, ...args], {
6066
6341
  env: childEnv,
6342
+ cwd: options.cwd,
6067
6343
  stdio: ["inherit", "pipe", "pipe"]
6068
6344
  });
6069
6345
  child.stdout?.on("data", (data) => {
@@ -6342,6 +6618,7 @@ __export(dist_exports, {
6342
6618
  DEFAULT_CRON_SCHEDULE: () => DEFAULT_CRON_SCHEDULE,
6343
6619
  DEFAULT_CRON_SCHEDULE_OFFSET: () => DEFAULT_CRON_SCHEDULE_OFFSET,
6344
6620
  DEFAULT_DEFAULT_BRANCH: () => DEFAULT_DEFAULT_BRANCH,
6621
+ DEFAULT_EXECUTOR_ENABLED: () => DEFAULT_EXECUTOR_ENABLED,
6345
6622
  DEFAULT_FALLBACK_ON_RATE_LIMIT: () => DEFAULT_FALLBACK_ON_RATE_LIMIT,
6346
6623
  DEFAULT_JOB_PROVIDERS: () => DEFAULT_JOB_PROVIDERS,
6347
6624
  DEFAULT_LOCAL_BOARD_INFO: () => DEFAULT_LOCAL_BOARD_INFO,
@@ -6362,7 +6639,9 @@ __export(dist_exports, {
6362
6639
  DEFAULT_QA_SCHEDULE: () => DEFAULT_QA_SCHEDULE,
6363
6640
  DEFAULT_QA_SKIP_LABEL: () => DEFAULT_QA_SKIP_LABEL,
6364
6641
  DEFAULT_REVIEWER_ENABLED: () => DEFAULT_REVIEWER_ENABLED,
6642
+ DEFAULT_REVIEWER_MAX_RETRIES: () => DEFAULT_REVIEWER_MAX_RETRIES,
6365
6643
  DEFAULT_REVIEWER_MAX_RUNTIME: () => DEFAULT_REVIEWER_MAX_RUNTIME,
6644
+ DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
6366
6645
  DEFAULT_REVIEWER_SCHEDULE: () => DEFAULT_REVIEWER_SCHEDULE,
6367
6646
  DEFAULT_ROADMAP_SCANNER: () => DEFAULT_ROADMAP_SCANNER,
6368
6647
  DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
@@ -6381,6 +6660,7 @@ __export(dist_exports, {
6381
6660
  Logger: () => Logger,
6382
6661
  MAX_HISTORY_RECORDS_PER_PRD: () => MAX_HISTORY_RECORDS_PER_PRD,
6383
6662
  NIGHT_WATCH_LABELS: () => NIGHT_WATCH_LABELS,
6663
+ PLANNER_LOG_NAME: () => PLANNER_LOG_NAME,
6384
6664
  PRD_STATES_FILE_NAME: () => PRD_STATES_FILE_NAME,
6385
6665
  PRD_TEMPLATE: () => PRD_TEMPLATE,
6386
6666
  PRIORITY_LABELS: () => PRIORITY_LABELS,
@@ -6509,7 +6789,9 @@ __export(dist_exports, {
6509
6789
  parseRoadmap: () => parseRoadmap,
6510
6790
  parseScriptResult: () => parseScriptResult,
6511
6791
  performCancel: () => performCancel,
6792
+ plannerLockPath: () => plannerLockPath,
6512
6793
  projectRuntimeKey: () => projectRuntimeKey,
6794
+ qaLockPath: () => qaLockPath,
6513
6795
  readCrontab: () => readCrontab,
6514
6796
  readPrdStates: () => readPrdStates,
6515
6797
  recordExecution: () => recordExecution,
@@ -6624,7 +6906,7 @@ function detectPlaywright(cwd) {
6624
6906
  return true;
6625
6907
  }
6626
6908
  try {
6627
- execSync4("playwright --version", {
6909
+ execSync3("playwright --version", {
6628
6910
  cwd,
6629
6911
  encoding: "utf-8",
6630
6912
  stdio: ["pipe", "pipe", "pipe"],
@@ -6668,12 +6950,12 @@ function promptYesNo(question, defaultNo = true) {
6668
6950
  function installPlaywrightForQa(cwd) {
6669
6951
  try {
6670
6952
  const installCmd = resolvePlaywrightInstallCommand(cwd);
6671
- execSync4(installCmd, {
6953
+ execSync3(installCmd, {
6672
6954
  cwd,
6673
6955
  encoding: "utf-8",
6674
6956
  stdio: ["pipe", "pipe", "pipe"]
6675
6957
  });
6676
- execSync4("npx playwright install chromium", {
6958
+ execSync3("npx playwright install chromium", {
6677
6959
  cwd,
6678
6960
  encoding: "utf-8",
6679
6961
  stdio: ["pipe", "pipe", "pipe"]
@@ -6686,7 +6968,7 @@ function installPlaywrightForQa(cwd) {
6686
6968
  function getDefaultBranch(cwd) {
6687
6969
  const getRefTimestamp = (ref) => {
6688
6970
  try {
6689
- const timestamp = execSync4(`git log -1 --format=%ct ${ref}`, {
6971
+ const timestamp = execSync3(`git log -1 --format=%ct ${ref}`, {
6690
6972
  encoding: "utf-8",
6691
6973
  cwd,
6692
6974
  stdio: ["pipe", "pipe", "pipe"]
@@ -6720,7 +7002,7 @@ function getDefaultBranch(cwd) {
6720
7002
  if (masterTimestamp !== null) {
6721
7003
  return "master";
6722
7004
  }
6723
- const remoteRef = execSync4('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo ""', {
7005
+ const remoteRef = execSync3('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null || echo ""', {
6724
7006
  encoding: "utf-8",
6725
7007
  cwd,
6726
7008
  stdio: ["pipe", "pipe", "pipe"]
@@ -6976,7 +7258,7 @@ function initCommand(program2) {
6976
7258
  } else {
6977
7259
  let hasGitHubRemote = false;
6978
7260
  try {
6979
- const remoteUrl = execSync4("git remote get-url origin", {
7261
+ const remoteUrl = execSync3("git remote get-url origin", {
6980
7262
  cwd,
6981
7263
  encoding: "utf-8",
6982
7264
  stdio: ["pipe", "pipe", "pipe"]
@@ -7038,7 +7320,7 @@ function initCommand(program2) {
7038
7320
  label("Reviewer", reviewerEnabled ? "Enabled" : "Disabled");
7039
7321
  console.log();
7040
7322
  header("Next Steps");
7041
- info("1. Add your PRD files to docs/PRDs/night-watch/");
7323
+ info(`1. Add your PRD files to ${prdDir}/`);
7042
7324
  info("2. Run `night-watch install` to set up cron jobs");
7043
7325
  info("3. Or run `night-watch run` to execute PRDs manually");
7044
7326
  console.log();
@@ -7058,10 +7340,10 @@ function resolveRunNotificationEvent(exitCode, scriptStatus) {
7058
7340
  return null;
7059
7341
  }
7060
7342
  function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7061
- if (options.dryRun) {
7343
+ if (options.crossProjectFallback !== true) {
7062
7344
  return false;
7063
7345
  }
7064
- if (options.crossProjectFallback === false) {
7346
+ if (options.dryRun) {
7065
7347
  return false;
7066
7348
  }
7067
7349
  if (process.env.NW_CROSS_PROJECT_FALLBACK_ACTIVE === "1") {
@@ -7136,7 +7418,7 @@ async function runCrossProjectFallback(currentProjectDir, options) {
7136
7418
  const envVars = buildEnvVars(candidateConfig, options);
7137
7419
  envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = "1";
7138
7420
  try {
7139
- const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars);
7421
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars, { cwd: candidate.path });
7140
7422
  const scriptResult = parseScriptResult(`${stdout}
7141
7423
  ${stderr}`);
7142
7424
  if (!options.dryRun) {
@@ -7255,10 +7537,14 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7255
7537
  return { pending, completed };
7256
7538
  }
7257
7539
  function runCommand(program2) {
7258
- program2.command("run").description("Run PRD executor now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds").option("--provider <string>", "AI provider to use (claude or codex)").option("--no-cross-project-fallback", "Do not check other registered projects when this project has no eligible work").action(async (options) => {
7540
+ program2.command("run").description("Run PRD executor now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds").option("--provider <string>", "AI provider to use (claude or codex)").option("--cross-project-fallback", "Check other registered projects when this project has no eligible work").option("--no-cross-project-fallback", "Deprecated alias; cross-project fallback is disabled by default").action(async (options) => {
7259
7541
  const projectDir = process.cwd();
7260
7542
  let config = loadConfig(projectDir);
7261
7543
  config = applyCliOverrides(config, options);
7544
+ if (config.executorEnabled === false && !options.dryRun) {
7545
+ info("Executor is disabled in config; skipping run.");
7546
+ process.exit(0);
7547
+ }
7262
7548
  const envVars = buildEnvVars(config, options);
7263
7549
  const scriptPath = getScriptPath("night-watch-cron.sh");
7264
7550
  if (options.dryRun) {
@@ -7348,7 +7634,7 @@ function runCommand(program2) {
7348
7634
  const spinner = createSpinner("Running PRD executor...");
7349
7635
  spinner.start();
7350
7636
  try {
7351
- const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
7637
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars, { cwd: projectDir });
7352
7638
  const scriptResult = parseScriptResult(`${stdout}
7353
7639
  ${stderr}`);
7354
7640
  if (exitCode === 0) {
@@ -7392,6 +7678,23 @@ function parseAutoMergedPrNumbers(raw) {
7392
7678
  }
7393
7679
  return raw.split(",").map((token) => parseInt(token.trim().replace(/^#/, ""), 10)).filter((value) => !Number.isNaN(value));
7394
7680
  }
7681
+ function parseRetryAttempts(raw) {
7682
+ if (!raw) {
7683
+ return 1;
7684
+ }
7685
+ const parsed = parseInt(raw, 10);
7686
+ return Number.isNaN(parsed) || parsed < 1 ? 1 : parsed;
7687
+ }
7688
+ function parseFinalReviewScore(raw) {
7689
+ if (!raw) {
7690
+ return void 0;
7691
+ }
7692
+ const parsed = parseInt(raw, 10);
7693
+ if (Number.isNaN(parsed)) {
7694
+ return void 0;
7695
+ }
7696
+ return parsed;
7697
+ }
7395
7698
  function buildEnvVars2(config, options) {
7396
7699
  const env = {};
7397
7700
  const reviewerProvider = resolveJobProvider(config, "reviewer");
@@ -7400,6 +7703,8 @@ function buildEnvVars2(config, options) {
7400
7703
  env.NW_DEFAULT_BRANCH = config.defaultBranch;
7401
7704
  }
7402
7705
  env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
7706
+ env.NW_REVIEWER_MAX_RETRIES = String(config.reviewerMaxRetries);
7707
+ env.NW_REVIEWER_RETRY_DELAY = String(config.reviewerRetryDelay);
7403
7708
  env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
7404
7709
  env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
7405
7710
  if (config.providerEnv) {
@@ -7412,10 +7717,6 @@ function buildEnvVars2(config, options) {
7412
7717
  if (options.dryRun) {
7413
7718
  env.NW_DRY_RUN = "1";
7414
7719
  }
7415
- if (config.autoMerge) {
7416
- env.NW_AUTO_MERGE = "1";
7417
- }
7418
- env.NW_AUTO_MERGE_METHOD = config.autoMergeMethod;
7419
7720
  env.NW_EXECUTION_CONTEXT = "agent";
7420
7721
  return env;
7421
7722
  }
@@ -7441,7 +7742,7 @@ function getOpenPrsNeedingWork(branchPatterns) {
7441
7742
  for (const pattern of branchPatterns) {
7442
7743
  args.push("--head", pattern);
7443
7744
  }
7444
- const result = execFileSync4("gh", args, {
7745
+ const result = execFileSync2("gh", args, {
7445
7746
  encoding: "utf-8",
7446
7747
  stdio: ["pipe", "pipe", "pipe"]
7447
7748
  });
@@ -7460,6 +7761,10 @@ function reviewCommand(program2) {
7460
7761
  const projectDir = process.cwd();
7461
7762
  let config = loadConfig(projectDir);
7462
7763
  config = applyCliOverrides2(config, options);
7764
+ if (!config.reviewerEnabled && !options.dryRun) {
7765
+ info("Reviewer is disabled in config; skipping review.");
7766
+ process.exit(0);
7767
+ }
7463
7768
  const envVars = buildEnvVars2(config, options);
7464
7769
  const scriptPath = getScriptPath("night-watch-pr-reviewer-cron.sh");
7465
7770
  if (options.dryRun) {
@@ -7479,6 +7784,8 @@ function reviewCommand(program2) {
7479
7784
  "Auto-merge",
7480
7785
  config.autoMerge ? `Enabled (${config.autoMergeMethod})` : "Disabled"
7481
7786
  ]);
7787
+ configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
7788
+ configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
7482
7789
  console.log(configTable.toString());
7483
7790
  header("Open PRs Needing Work");
7484
7791
  const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
@@ -7538,6 +7845,8 @@ ${stderr}`);
7538
7845
  }
7539
7846
  }
7540
7847
  if (!skipNotification) {
7848
+ const attempts = parseRetryAttempts(scriptResult?.data.attempts);
7849
+ const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
7541
7850
  const _reviewCtx = {
7542
7851
  event: "review_completed",
7543
7852
  projectName: path15.basename(projectDir),
@@ -7549,7 +7858,9 @@ ${stderr}`);
7549
7858
  prNumber: prDetails?.number,
7550
7859
  filesChanged: prDetails?.changedFiles,
7551
7860
  additions: prDetails?.additions,
7552
- deletions: prDetails?.deletions
7861
+ deletions: prDetails?.deletions,
7862
+ attempts,
7863
+ finalScore
7553
7864
  };
7554
7865
  await sendNotifications(config, _reviewCtx);
7555
7866
  }
@@ -7603,6 +7914,9 @@ function parseQaPrNumbers(prsRaw) {
7603
7914
  }
7604
7915
  return numbers;
7605
7916
  }
7917
+ function getTelegramStatusWebhooks(config) {
7918
+ return (config.notifications?.webhooks ?? []).filter((wh) => wh.type === "telegram" && typeof wh.botToken === "string" && wh.botToken.trim().length > 0 && typeof wh.chatId === "string" && wh.chatId.trim().length > 0).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
7919
+ }
7606
7920
  function buildEnvVars3(config, options) {
7607
7921
  const env = {};
7608
7922
  const qaProvider = resolveJobProvider(config, "qa");
@@ -7619,6 +7933,12 @@ function buildEnvVars3(config, options) {
7619
7933
  if (config.providerEnv) {
7620
7934
  Object.assign(env, config.providerEnv);
7621
7935
  }
7936
+ const telegramWebhooks = getTelegramStatusWebhooks(config);
7937
+ if (telegramWebhooks.length > 0) {
7938
+ env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
7939
+ env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
7940
+ env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
7941
+ }
7622
7942
  if (options.dryRun) {
7623
7943
  env.NW_DRY_RUN = "1";
7624
7944
  }
@@ -7643,6 +7963,10 @@ function qaCommand(program2) {
7643
7963
  const projectDir = process.cwd();
7644
7964
  let config = loadConfig(projectDir);
7645
7965
  config = applyCliOverrides3(config, options);
7966
+ if (!config.qa.enabled && !options.dryRun) {
7967
+ info("QA is disabled in config; skipping run.");
7968
+ process.exit(0);
7969
+ }
7646
7970
  const envVars = buildEnvVars3(config, options);
7647
7971
  const scriptPath = getScriptPath("night-watch-qa-cron.sh");
7648
7972
  if (options.dryRun) {
@@ -7725,6 +8049,9 @@ ${stderr}`);
7725
8049
  });
7726
8050
  }
7727
8051
  init_dist();
8052
+ function getTelegramStatusWebhooks2(config) {
8053
+ return (config.notifications?.webhooks ?? []).filter((wh) => wh.type === "telegram" && typeof wh.botToken === "string" && wh.botToken.trim().length > 0 && typeof wh.chatId === "string" && wh.chatId.trim().length > 0).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
8054
+ }
7728
8055
  function buildEnvVars4(config, options) {
7729
8056
  const env = {};
7730
8057
  const auditProvider = resolveJobProvider(config, "audit");
@@ -7736,6 +8063,12 @@ function buildEnvVars4(config, options) {
7736
8063
  if (config.providerEnv) {
7737
8064
  Object.assign(env, config.providerEnv);
7738
8065
  }
8066
+ const telegramWebhooks = getTelegramStatusWebhooks2(config);
8067
+ if (telegramWebhooks.length > 0) {
8068
+ env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
8069
+ env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
8070
+ env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
8071
+ }
7739
8072
  if (options.dryRun) {
7740
8073
  env.NW_DRY_RUN = "1";
7741
8074
  }
@@ -7758,6 +8091,10 @@ function auditCommand(program2) {
7758
8091
  _cliProviderOverride: options.provider
7759
8092
  };
7760
8093
  }
8094
+ if (!config.audit.enabled && !options.dryRun) {
8095
+ info("Audit is disabled in config; skipping run.");
8096
+ process.exit(0);
8097
+ }
7761
8098
  const envVars = buildEnvVars4(config, options);
7762
8099
  const scriptPath = getScriptPath("night-watch-audit-cron.sh");
7763
8100
  if (options.dryRun) {
@@ -7827,7 +8164,7 @@ function shellQuote(value) {
7827
8164
  }
7828
8165
  function getNightWatchBinPath() {
7829
8166
  try {
7830
- const npmBin = execSync5("npm bin -g", { encoding: "utf-8" }).trim();
8167
+ const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
7831
8168
  const binPath = path18.join(npmBin, "night-watch");
7832
8169
  if (fs17.existsSync(binPath)) {
7833
8170
  return binPath;
@@ -7835,14 +8172,14 @@ function getNightWatchBinPath() {
7835
8172
  } catch {
7836
8173
  }
7837
8174
  try {
7838
- return execSync5("which night-watch", { encoding: "utf-8" }).trim();
8175
+ return execSync4("which night-watch", { encoding: "utf-8" }).trim();
7839
8176
  } catch {
7840
8177
  return "night-watch";
7841
8178
  }
7842
8179
  }
7843
8180
  function getNodeBinDir() {
7844
8181
  try {
7845
- const nodePath = execSync5("which node", { encoding: "utf-8" }).trim();
8182
+ const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
7846
8183
  return path18.dirname(nodePath);
7847
8184
  } catch {
7848
8185
  return "";
@@ -7901,8 +8238,11 @@ function performInstall(projectDir, config, options) {
7901
8238
  const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
7902
8239
  providerEnvPrefix = exports + " && ";
7903
8240
  }
7904
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
7905
- entries.push(executorEntry);
8241
+ const installExecutor = config.executorEnabled !== false;
8242
+ if (installExecutor) {
8243
+ const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
8244
+ entries.push(executorEntry);
8245
+ }
7906
8246
  const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
7907
8247
  if (installReviewer) {
7908
8248
  const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
@@ -7912,7 +8252,7 @@ function performInstall(projectDir, config, options) {
7912
8252
  if (installSlicer) {
7913
8253
  const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
7914
8254
  const slicerLog = path18.join(logDir, "slicer.log");
7915
- const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} slice >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8255
+ const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
7916
8256
  entries.push(slicerEntry);
7917
8257
  }
7918
8258
  const disableQa = options?.noQa === true || options?.qa === false;
@@ -7979,8 +8319,11 @@ function installCommand(program2) {
7979
8319
  const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
7980
8320
  providerEnvPrefix = exports + " && ";
7981
8321
  }
7982
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
7983
- entries.push(executorEntry);
8322
+ const installExecutor = config.executorEnabled !== false;
8323
+ if (installExecutor) {
8324
+ const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
8325
+ entries.push(executorEntry);
8326
+ }
7984
8327
  const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
7985
8328
  if (installReviewer) {
7986
8329
  const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
@@ -7991,7 +8334,7 @@ function installCommand(program2) {
7991
8334
  if (installSlicer) {
7992
8335
  slicerLog = path18.join(logDir, "slicer.log");
7993
8336
  const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
7994
- const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} slice >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8337
+ const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
7995
8338
  entries.push(slicerEntry);
7996
8339
  }
7997
8340
  const disableQa = options.noQa === true || options.qa === false;
@@ -8021,12 +8364,14 @@ function installCommand(program2) {
8021
8364
  entries.forEach((entry) => dim(` ${entry}`));
8022
8365
  console.log();
8023
8366
  header("Log Files");
8024
- dim(` Executor: ${executorLog}`);
8367
+ if (installExecutor) {
8368
+ dim(` Executor: ${executorLog}`);
8369
+ }
8025
8370
  if (installReviewer) {
8026
8371
  dim(` Reviewer: ${reviewerLog}`);
8027
8372
  }
8028
8373
  if (installSlicer && slicerLog) {
8029
- dim(` Slicer: ${slicerLog}`);
8374
+ dim(` Planner: ${slicerLog}`);
8030
8375
  }
8031
8376
  if (installQa && qaLog) {
8032
8377
  dim(` QA: ${qaLog}`);
@@ -8145,11 +8490,17 @@ function statusCommand(program2) {
8145
8490
  try {
8146
8491
  const projectDir = process.cwd();
8147
8492
  const config = loadConfig(projectDir);
8148
- const snapshot = fetchStatusSnapshot(projectDir, config);
8493
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
8149
8494
  const executorProc = snapshot.processes.find((p) => p.name === "executor");
8150
8495
  const reviewerProc = snapshot.processes.find((p) => p.name === "reviewer");
8496
+ const qaProc = snapshot.processes.find((p) => p.name === "qa");
8497
+ const auditProc = snapshot.processes.find((p) => p.name === "audit");
8498
+ const plannerProc = snapshot.processes.find((p) => p.name === "planner");
8151
8499
  const executorLog = snapshot.logs.find((l) => l.name === "executor");
8152
8500
  const reviewerLog = snapshot.logs.find((l) => l.name === "reviewer");
8501
+ const qaLog = snapshot.logs.find((l) => l.name === "qa");
8502
+ const auditLog = snapshot.logs.find((l) => l.name === "audit");
8503
+ const plannerLog = snapshot.logs.find((l) => l.name === "planner");
8153
8504
  const pendingPrds = snapshot.prds.filter((p) => p.status === "ready" || p.status === "blocked").length;
8154
8505
  const claimedPrds = snapshot.prds.filter((p) => p.status === "in-progress").length;
8155
8506
  const donePrds = snapshot.prds.filter((p) => p.status === "done").length;
@@ -8162,6 +8513,9 @@ function statusCommand(program2) {
8162
8513
  autoMergeMethod: config.autoMergeMethod,
8163
8514
  executor: { running: executorProc?.running ?? false, pid: executorProc?.pid ?? null },
8164
8515
  reviewer: { running: reviewerProc?.running ?? false, pid: reviewerProc?.pid ?? null },
8516
+ qa: { running: qaProc?.running ?? false, pid: qaProc?.pid ?? null },
8517
+ audit: { running: auditProc?.running ?? false, pid: auditProc?.pid ?? null },
8518
+ planner: { running: plannerProc?.running ?? false, pid: plannerProc?.pid ?? null },
8165
8519
  prds: { pending: pendingPrds, claimed: claimedPrds, done: donePrds },
8166
8520
  prs: { open: snapshot.prs.length },
8167
8521
  crontab: snapshot.crontab,
@@ -8177,6 +8531,24 @@ function statusCommand(program2) {
8177
8531
  lastLines: reviewerLog.lastLines,
8178
8532
  exists: reviewerLog.exists,
8179
8533
  size: reviewerLog.size
8534
+ } : void 0,
8535
+ qa: qaLog ? {
8536
+ path: qaLog.path,
8537
+ lastLines: qaLog.lastLines,
8538
+ exists: qaLog.exists,
8539
+ size: qaLog.size
8540
+ } : void 0,
8541
+ audit: auditLog ? {
8542
+ path: auditLog.path,
8543
+ lastLines: auditLog.lastLines,
8544
+ exists: auditLog.exists,
8545
+ size: auditLog.size
8546
+ } : void 0,
8547
+ planner: plannerLog ? {
8548
+ path: plannerLog.path,
8549
+ lastLines: plannerLog.lastLines,
8550
+ exists: plannerLog.exists,
8551
+ size: plannerLog.size
8180
8552
  } : void 0
8181
8553
  }
8182
8554
  };
@@ -8208,6 +8580,12 @@ function statusCommand(program2) {
8208
8580
  "Reviewer",
8209
8581
  formatRunningStatus(status.reviewer.running, status.reviewer.pid)
8210
8582
  ]);
8583
+ processTable.push(["QA", formatRunningStatus(status.qa.running, status.qa.pid)]);
8584
+ processTable.push(["Audit", formatRunningStatus(status.audit.running, status.audit.pid)]);
8585
+ processTable.push([
8586
+ "Planner",
8587
+ formatRunningStatus(status.planner.running, status.planner.pid)
8588
+ ]);
8211
8589
  console.log(processTable.toString());
8212
8590
  header("PRD Status");
8213
8591
  const prdTable = createTable({ head: ["Status", "Count"] });
@@ -8243,6 +8621,27 @@ function statusCommand(program2) {
8243
8621
  status.logs.reviewer.exists ? "Exists" : "Not found"
8244
8622
  ]);
8245
8623
  }
8624
+ if (status.logs.qa) {
8625
+ logTable.push([
8626
+ "QA",
8627
+ status.logs.qa.exists ? formatBytes(status.logs.qa.size) : "-",
8628
+ status.logs.qa.exists ? "Exists" : "Not found"
8629
+ ]);
8630
+ }
8631
+ if (status.logs.audit) {
8632
+ logTable.push([
8633
+ "Audit",
8634
+ status.logs.audit.exists ? formatBytes(status.logs.audit.size) : "-",
8635
+ status.logs.audit.exists ? "Exists" : "Not found"
8636
+ ]);
8637
+ }
8638
+ if (status.logs.planner) {
8639
+ logTable.push([
8640
+ "Planner",
8641
+ status.logs.planner.exists ? formatBytes(status.logs.planner.size) : "-",
8642
+ status.logs.planner.exists ? "Exists" : "Not found"
8643
+ ]);
8644
+ }
8246
8645
  console.log(logTable.toString());
8247
8646
  if (options.verbose) {
8248
8647
  if (status.logs.executor?.exists && status.logs.executor.lastLines.length > 0) {
@@ -8253,12 +8652,27 @@ function statusCommand(program2) {
8253
8652
  dim(" Reviewer last 5 lines:");
8254
8653
  status.logs.reviewer.lastLines.forEach((line) => dim(` ${line}`));
8255
8654
  }
8655
+ if (status.logs.qa?.exists && status.logs.qa.lastLines.length > 0) {
8656
+ dim(" QA last 5 lines:");
8657
+ status.logs.qa.lastLines.forEach((line) => dim(` ${line}`));
8658
+ }
8659
+ if (status.logs.audit?.exists && status.logs.audit.lastLines.length > 0) {
8660
+ dim(" Audit last 5 lines:");
8661
+ status.logs.audit.lastLines.forEach((line) => dim(` ${line}`));
8662
+ }
8663
+ if (status.logs.planner?.exists && status.logs.planner.lastLines.length > 0) {
8664
+ dim(" Planner last 5 lines:");
8665
+ status.logs.planner.lastLines.forEach((line) => dim(` ${line}`));
8666
+ }
8256
8667
  }
8257
8668
  header("Commands");
8258
8669
  dim(" night-watch install - Install crontab entries");
8259
8670
  dim(" night-watch logs - View logs");
8260
8671
  dim(" night-watch run - Run executor now");
8261
8672
  dim(" night-watch review - Run reviewer now");
8673
+ dim(" night-watch qa - Run QA now");
8674
+ dim(" night-watch audit - Run audit now");
8675
+ dim(" night-watch planner - Run planner now");
8262
8676
  console.log();
8263
8677
  } catch (error2) {
8264
8678
  console.error(`Error getting status: ${error2 instanceof Error ? error2.message : String(error2)}`);
@@ -8297,22 +8711,36 @@ function followLog(filePath) {
8297
8711
  });
8298
8712
  }
8299
8713
  function logsCommand(program2) {
8300
- 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 (run|review|all)", "all").action(async (options) => {
8714
+ 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) => {
8301
8715
  try {
8302
8716
  const projectDir = process.cwd();
8303
8717
  const logDir = path20.join(projectDir, LOG_DIR);
8304
8718
  const lineCount = parseInt(options.lines || "50", 10);
8305
8719
  const executorLog = path20.join(logDir, EXECUTOR_LOG_FILE);
8306
8720
  const reviewerLog = path20.join(logDir, REVIEWER_LOG_FILE);
8721
+ const qaLog = path20.join(logDir, `${QA_LOG_NAME}.log`);
8722
+ const auditLog = path20.join(logDir, `${AUDIT_LOG_NAME}.log`);
8723
+ const plannerLog = path20.join(logDir, `${PLANNER_LOG_NAME}.log`);
8307
8724
  const logType = options.type?.toLowerCase() || "all";
8308
8725
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
8309
8726
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
8727
+ const showQa = logType === "all" || logType === "qa";
8728
+ const showAudit = logType === "all" || logType === "audit";
8729
+ const showPlanner = logType === "all" || logType === "planner" || logType === "slice" || logType === "slicer";
8310
8730
  if (options.follow) {
8311
8731
  if (logType === "all") {
8312
8732
  dim("Note: Following all logs is not supported. Showing executor log.");
8313
- dim("Use --type review to follow reviewer log.\n");
8314
- }
8315
- const targetLog = showReviewer ? reviewerLog : executorLog;
8733
+ dim("Use --type reviewer|qa|audit|planner for other logs.\n");
8734
+ }
8735
+ let targetLog = executorLog;
8736
+ if (showReviewer)
8737
+ targetLog = reviewerLog;
8738
+ else if (showQa)
8739
+ targetLog = qaLog;
8740
+ else if (showAudit)
8741
+ targetLog = auditLog;
8742
+ else if (showPlanner)
8743
+ targetLog = plannerLog;
8316
8744
  followLog(targetLog);
8317
8745
  return;
8318
8746
  }
@@ -8329,10 +8757,28 @@ function logsCommand(program2) {
8329
8757
  console.log();
8330
8758
  console.log(getLastLines(reviewerLog, lineCount));
8331
8759
  }
8760
+ if (showQa) {
8761
+ header("QA Log");
8762
+ dim(`File: ${qaLog}`);
8763
+ console.log();
8764
+ console.log(getLastLines(qaLog, lineCount));
8765
+ }
8766
+ if (showAudit) {
8767
+ header("Audit Log");
8768
+ dim(`File: ${auditLog}`);
8769
+ console.log();
8770
+ console.log(getLastLines(auditLog, lineCount));
8771
+ }
8772
+ if (showPlanner) {
8773
+ header("Planner Log");
8774
+ dim(`File: ${plannerLog}`);
8775
+ console.log();
8776
+ console.log(getLastLines(plannerLog, lineCount));
8777
+ }
8332
8778
  console.log();
8333
8779
  dim("---");
8334
8780
  dim("Tip: Use -f to follow logs in real-time");
8335
- dim(" Use --type run or --type review to view specific logs");
8781
+ dim(" Use --type executor|reviewer|qa|audit|planner to view specific logs");
8336
8782
  } catch (err) {
8337
8783
  console.error(`Error reading logs: ${err instanceof Error ? err.message : String(err)}`);
8338
8784
  process.exit(1);
@@ -9845,10 +10291,14 @@ function createSchedulesTab() {
9845
10291
  ctx.showMessage(`Saved but cron install failed: ${installResult.error}`, "error");
9846
10292
  }
9847
10293
  ctx.config = newConfig;
9848
- const snap = ctx.refreshSnapshot();
9849
- ctx.snapshot = snap;
9850
- renderCrontab(ctx);
9851
- renderScheduleSettings(ctx);
10294
+ ctx.refreshSnapshot().then((snap) => {
10295
+ ctx.snapshot = snap;
10296
+ renderCrontab(ctx);
10297
+ renderScheduleSettings(ctx);
10298
+ }).catch(() => {
10299
+ renderCrontab(ctx);
10300
+ renderScheduleSettings(ctx);
10301
+ });
9852
10302
  }
9853
10303
  function showCustomCronInput(ctx, field, label2) {
9854
10304
  const currentValue = ctx.config[field];
@@ -9946,10 +10396,13 @@ function createSchedulesTab() {
9946
10396
  } else {
9947
10397
  ctx.showMessage(`Install failed: ${result.error}`, "error");
9948
10398
  }
9949
- const snap = ctx.refreshSnapshot();
9950
- ctx.snapshot = snap;
9951
- renderCrontab(ctx);
9952
- ctx.screen.render();
10399
+ ctx.refreshSnapshot().then((snap) => {
10400
+ ctx.snapshot = snap;
10401
+ renderCrontab(ctx);
10402
+ ctx.screen.render();
10403
+ }).catch(() => {
10404
+ ctx.screen.render();
10405
+ });
9953
10406
  }
9954
10407
  ],
9955
10408
  [
@@ -9961,10 +10414,13 @@ function createSchedulesTab() {
9961
10414
  } else {
9962
10415
  ctx.showMessage(`Uninstall failed: ${result.error}`, "error");
9963
10416
  }
9964
- const snap = ctx.refreshSnapshot();
9965
- ctx.snapshot = snap;
9966
- renderCrontab(ctx);
9967
- ctx.screen.render();
10417
+ ctx.refreshSnapshot().then((snap) => {
10418
+ ctx.snapshot = snap;
10419
+ renderCrontab(ctx);
10420
+ ctx.screen.render();
10421
+ }).catch(() => {
10422
+ ctx.screen.render();
10423
+ });
9968
10424
  }
9969
10425
  ],
9970
10426
  [
@@ -9977,10 +10433,13 @@ function createSchedulesTab() {
9977
10433
  } else {
9978
10434
  ctx.showMessage(`Reinstall failed: ${result.error}`, "error");
9979
10435
  }
9980
- const snap = ctx.refreshSnapshot();
9981
- ctx.snapshot = snap;
9982
- renderCrontab(ctx);
9983
- ctx.screen.render();
10436
+ ctx.refreshSnapshot().then((snap) => {
10437
+ ctx.snapshot = snap;
10438
+ renderCrontab(ctx);
10439
+ ctx.screen.render();
10440
+ }).catch(() => {
10441
+ ctx.screen.render();
10442
+ });
9984
10443
  }
9985
10444
  ]
9986
10445
  ];
@@ -10108,9 +10567,12 @@ ${result.entries.map((e) => ` ${e}`).join("\n")}`);
10108
10567
  outputBox.setContent(`{red-fg}Install failed: ${result.error}{/red-fg}`);
10109
10568
  ctx.showMessage("Install failed", "error");
10110
10569
  }
10111
- const snap = ctx.refreshSnapshot();
10112
- ctx.snapshot = snap;
10113
- ctx.screen.render();
10570
+ ctx.refreshSnapshot().then((snap) => {
10571
+ ctx.snapshot = snap;
10572
+ ctx.screen.render();
10573
+ }).catch(() => {
10574
+ ctx.screen.render();
10575
+ });
10114
10576
  }
10115
10577
  },
10116
10578
  {
@@ -10126,9 +10588,12 @@ Removed ${result.removedCount} entries.`);
10126
10588
  outputBox.setContent(`{red-fg}Uninstall failed: ${result.error}{/red-fg}`);
10127
10589
  ctx.showMessage("Uninstall failed", "error");
10128
10590
  }
10129
- const snap = ctx.refreshSnapshot();
10130
- ctx.snapshot = snap;
10131
- ctx.screen.render();
10591
+ ctx.refreshSnapshot().then((snap) => {
10592
+ ctx.snapshot = snap;
10593
+ ctx.screen.render();
10594
+ }).catch(() => {
10595
+ ctx.screen.render();
10596
+ });
10132
10597
  }
10133
10598
  },
10134
10599
  {
@@ -10521,7 +10986,7 @@ function dashboardCommand(program2) {
10521
10986
  }
10522
10987
  let activeTabIndex = 0;
10523
10988
  let isEditing = false;
10524
- let snapshot = fetchStatusSnapshot(projectDir, config);
10989
+ let snapshot = await fetchStatusSnapshot(projectDir, config);
10525
10990
  const ctx = {
10526
10991
  screen,
10527
10992
  projectDir,
@@ -10532,8 +10997,8 @@ function dashboardCommand(program2) {
10532
10997
  ctx.config = config;
10533
10998
  return config;
10534
10999
  },
10535
- refreshSnapshot: () => {
10536
- snapshot = fetchStatusSnapshot(projectDir, config);
11000
+ refreshSnapshot: async () => {
11001
+ snapshot = await fetchStatusSnapshot(projectDir, config);
10537
11002
  ctx.snapshot = snapshot;
10538
11003
  return snapshot;
10539
11004
  },
@@ -10577,10 +11042,10 @@ function dashboardCommand(program2) {
10577
11042
  function updateHeader() {
10578
11043
  headerBox.setContent(`{center}Night Watch: ${snapshot.projectName} | Provider: ${config.provider} | Last: ${snapshot.timestamp.toLocaleTimeString()} | Next: ${countdown}s{/center}`);
10579
11044
  }
10580
- function refreshData() {
11045
+ async function refreshData() {
10581
11046
  config = loadConfig(projectDir);
10582
11047
  ctx.config = config;
10583
- snapshot = fetchStatusSnapshot(projectDir, config);
11048
+ snapshot = await fetchStatusSnapshot(projectDir, config);
10584
11049
  ctx.snapshot = snapshot;
10585
11050
  countdown = intervalSeconds;
10586
11051
  updateHeader();
@@ -10592,7 +11057,8 @@ function dashboardCommand(program2) {
10592
11057
  updateHeader();
10593
11058
  screen.render();
10594
11059
  if (countdown <= 0) {
10595
- refreshData();
11060
+ refreshData().catch(() => {
11061
+ });
10596
11062
  }
10597
11063
  }, 1e3);
10598
11064
  screen.key(["q", "escape"], () => {
@@ -10608,7 +11074,8 @@ function dashboardCommand(program2) {
10608
11074
  screen.key(["r"], () => {
10609
11075
  if (isEditing)
10610
11076
  return;
10611
- refreshData();
11077
+ refreshData().catch(() => {
11078
+ });
10612
11079
  });
10613
11080
  for (let i = 0; i < tabs.length; i++) {
10614
11081
  const idx = i;
@@ -10907,8 +11374,7 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
10907
11374
  return setInterval(() => {
10908
11375
  if (clients.size === 0)
10909
11376
  return;
10910
- try {
10911
- const snapshot = fetchStatusSnapshot(projectDir, getConfig());
11377
+ fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
10912
11378
  const hash = JSON.stringify({
10913
11379
  processes: snapshot.processes,
10914
11380
  prds: snapshot.prds.map((p) => ({ n: p.name, s: p.status }))
@@ -10917,8 +11383,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
10917
11383
  lastSnapshotHash = hash;
10918
11384
  broadcastSSE(clients, "status_changed", snapshot);
10919
11385
  }
10920
- } catch {
10921
- }
11386
+ }).catch(() => {
11387
+ });
10922
11388
  }, 2e3);
10923
11389
  }
10924
11390
  init_dist();
@@ -10989,11 +11455,17 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
10989
11455
  lockPath = executorLockPath(projectDir);
10990
11456
  } else if (command[0] === "review") {
10991
11457
  lockPath = reviewerLockPath(projectDir);
11458
+ } else if (command[0] === "planner") {
11459
+ lockPath = plannerLockPath(projectDir);
10992
11460
  }
10993
11461
  if (lockPath) {
10994
11462
  const lock = checkLockFile(lockPath);
10995
11463
  if (lock.running) {
10996
- const processType = command[0] === "run" ? "Executor" : "Reviewer";
11464
+ let processType = "Planner";
11465
+ if (command[0] === "run")
11466
+ processType = "Executor";
11467
+ else if (command[0] === "review")
11468
+ processType = "Reviewer";
10997
11469
  res.status(409).json({
10998
11470
  error: `${processType} is already running (PID ${lock.pid})`,
10999
11471
  pid: lock.pid
@@ -11037,25 +11509,36 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
11037
11509
  });
11038
11510
  }
11039
11511
  }
11040
- function createActionRoutes(deps) {
11041
- const { projectDir, getConfig, sseClients } = deps;
11042
- const router = Router();
11043
- router.post("/run", (req, res) => {
11512
+ function createActionRouteHandlers(ctx) {
11513
+ const router = Router({ mergeParams: true });
11514
+ const p = ctx.pathPrefix;
11515
+ router.post(`/${p}run`, (req, res) => {
11516
+ const projectDir = ctx.getProjectDir(req);
11044
11517
  spawnAction2(projectDir, ["run"], req, res, (pid) => {
11045
- broadcastSSE(sseClients, "executor_started", { pid });
11518
+ broadcastSSE(ctx.getSseClients(req), "executor_started", { pid });
11046
11519
  });
11047
11520
  });
11048
- router.post("/review", (req, res) => {
11049
- spawnAction2(projectDir, ["review"], req, res);
11521
+ router.post(`/${p}review`, (req, res) => {
11522
+ spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
11523
+ });
11524
+ router.post(`/${p}qa`, (req, res) => {
11525
+ spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
11526
+ });
11527
+ router.post(`/${p}audit`, (req, res) => {
11528
+ spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
11050
11529
  });
11051
- router.post("/install-cron", (req, res) => {
11052
- spawnAction2(projectDir, ["install"], req, res);
11530
+ router.post(`/${p}planner`, (req, res) => {
11531
+ spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
11053
11532
  });
11054
- router.post("/uninstall-cron", (req, res) => {
11055
- spawnAction2(projectDir, ["uninstall"], req, res);
11533
+ router.post(`/${p}install-cron`, (req, res) => {
11534
+ spawnAction2(ctx.getProjectDir(req), ["install"], req, res);
11056
11535
  });
11057
- router.post("/cancel", async (req, res) => {
11536
+ router.post(`/${p}uninstall-cron`, (req, res) => {
11537
+ spawnAction2(ctx.getProjectDir(req), ["uninstall"], req, res);
11538
+ });
11539
+ router.post(`/${p}cancel`, async (req, res) => {
11058
11540
  try {
11541
+ const projectDir = ctx.getProjectDir(req);
11059
11542
  const { type = "all" } = req.body;
11060
11543
  const validTypes = ["run", "review", "all"];
11061
11544
  if (!validTypes.includes(type)) {
@@ -11076,9 +11559,10 @@ function createActionRoutes(deps) {
11076
11559
  });
11077
11560
  }
11078
11561
  });
11079
- router.post("/retry", (req, res) => {
11562
+ router.post(`/${p}retry`, (req, res) => {
11080
11563
  try {
11081
- const config = getConfig();
11564
+ const projectDir = ctx.getProjectDir(req);
11565
+ const config = ctx.getConfig(req);
11082
11566
  const { prdName } = req.body;
11083
11567
  if (!prdName || typeof prdName !== "string") {
11084
11568
  res.status(400).json({ error: "prdName is required" });
@@ -11108,9 +11592,10 @@ function createActionRoutes(deps) {
11108
11592
  });
11109
11593
  }
11110
11594
  });
11111
- router.post("/clear-lock", (req, res) => {
11595
+ router.post(`/${p}clear-lock`, async (req, res) => {
11112
11596
  try {
11113
- const config = getConfig();
11597
+ const projectDir = ctx.getProjectDir(req);
11598
+ const config = ctx.getConfig(req);
11114
11599
  const lockPath = executorLockPath(projectDir);
11115
11600
  const lock = checkLockFile(lockPath);
11116
11601
  if (lock.running) {
@@ -11124,7 +11609,7 @@ function createActionRoutes(deps) {
11124
11609
  if (fs24.existsSync(prdDir)) {
11125
11610
  cleanOrphanedClaims(prdDir);
11126
11611
  }
11127
- broadcastSSE(sseClients, "status_changed", fetchStatusSnapshot(projectDir, config));
11612
+ broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
11128
11613
  res.json({ cleared: true });
11129
11614
  } catch (error2) {
11130
11615
  res.status(500).json({
@@ -11134,110 +11619,28 @@ function createActionRoutes(deps) {
11134
11619
  });
11135
11620
  return router;
11136
11621
  }
11622
+ function createActionRoutes(deps) {
11623
+ return createActionRouteHandlers({
11624
+ getConfig: () => deps.getConfig(),
11625
+ getProjectDir: () => deps.projectDir,
11626
+ getSseClients: () => deps.sseClients,
11627
+ pathPrefix: ""
11628
+ });
11629
+ }
11137
11630
  function createProjectActionRoutes(deps) {
11138
11631
  const { projectSseClients } = deps;
11139
- const router = Router({ mergeParams: true });
11140
- router.post("/actions/run", (req, res) => {
11141
- const projectDir = req.projectDir;
11142
- spawnAction2(projectDir, ["run"], req, res, (pid) => {
11143
- const clients = projectSseClients.get(projectDir);
11144
- if (clients) {
11145
- broadcastSSE(clients, "executor_started", { pid });
11146
- }
11147
- });
11148
- });
11149
- router.post("/actions/review", (req, res) => {
11150
- spawnAction2(req.projectDir, ["review"], req, res);
11151
- });
11152
- router.post("/actions/install-cron", (req, res) => {
11153
- spawnAction2(req.projectDir, ["install"], req, res);
11154
- });
11155
- router.post("/actions/uninstall-cron", (req, res) => {
11156
- spawnAction2(req.projectDir, ["uninstall"], req, res);
11157
- });
11158
- router.post("/actions/cancel", async (req, res) => {
11159
- try {
11160
- const projectDir = req.projectDir;
11161
- const { type = "all" } = req.body;
11162
- const validTypes = ["run", "review", "all"];
11163
- if (!validTypes.includes(type)) {
11164
- res.status(400).json({
11165
- error: `Invalid type. Must be one of: ${validTypes.join(", ")}`
11166
- });
11167
- return;
11168
- }
11169
- const results = await performCancel(projectDir, {
11170
- type,
11171
- force: true
11172
- });
11173
- const hasFailure = results.some((r) => !r.success);
11174
- res.status(hasFailure ? 500 : 200).json({ results });
11175
- } catch (error2) {
11176
- res.status(500).json({
11177
- error: error2 instanceof Error ? error2.message : String(error2)
11178
- });
11179
- }
11180
- });
11181
- router.post("/actions/retry", (req, res) => {
11182
- try {
11183
- const projectDir = req.projectDir;
11184
- const config = req.projectConfig;
11185
- const { prdName } = req.body;
11186
- if (!prdName || typeof prdName !== "string") {
11187
- res.status(400).json({ error: "prdName is required" });
11188
- return;
11189
- }
11190
- if (!validatePrdName(prdName)) {
11191
- res.status(400).json({ error: "Invalid PRD name" });
11192
- return;
11193
- }
11194
- const prdDir = path24.join(projectDir, config.prdDir);
11195
- const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
11196
- const pendingPath = path24.join(prdDir, normalized);
11197
- const donePath = path24.join(prdDir, "done", normalized);
11198
- if (fs24.existsSync(pendingPath)) {
11199
- res.json({ message: `"${normalized}" is already pending` });
11200
- return;
11201
- }
11202
- if (!fs24.existsSync(donePath)) {
11203
- res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
11204
- return;
11205
- }
11206
- fs24.renameSync(donePath, pendingPath);
11207
- res.json({ message: `Moved "${normalized}" back to pending` });
11208
- } catch (error2) {
11209
- res.status(500).json({
11210
- error: error2 instanceof Error ? error2.message : String(error2)
11211
- });
11212
- }
11213
- });
11214
- router.post("/actions/clear-lock", (req, res) => {
11215
- try {
11632
+ return createActionRouteHandlers({
11633
+ getConfig: (req) => req.projectConfig,
11634
+ getProjectDir: (req) => req.projectDir,
11635
+ getSseClients: (req) => {
11216
11636
  const projectDir = req.projectDir;
11217
- const config = req.projectConfig;
11218
- const lockPath = executorLockPath(projectDir);
11219
- const lock = checkLockFile(lockPath);
11220
- if (lock.running) {
11221
- res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
11222
- return;
11223
- }
11224
- if (fs24.existsSync(lockPath)) {
11225
- fs24.unlinkSync(lockPath);
11637
+ if (!projectSseClients.has(projectDir)) {
11638
+ projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
11226
11639
  }
11227
- const prdDir = path24.join(projectDir, config.prdDir);
11228
- if (fs24.existsSync(prdDir)) {
11229
- cleanOrphanedClaims(prdDir);
11230
- }
11231
- const clients = projectSseClients.get(projectDir) ?? /* @__PURE__ */ new Set();
11232
- broadcastSSE(clients, "status_changed", fetchStatusSnapshot(projectDir, config));
11233
- res.json({ cleared: true });
11234
- } catch (error2) {
11235
- res.status(500).json({
11236
- error: error2 instanceof Error ? error2.message : String(error2)
11237
- });
11238
- }
11640
+ return projectSseClients.get(projectDir);
11641
+ },
11642
+ pathPrefix: "actions/"
11239
11643
  });
11240
- return router;
11241
11644
  }
11242
11645
  init_dist();
11243
11646
  function createAgentRoutes() {
@@ -11340,12 +11743,13 @@ function createAgentRoutes() {
11340
11743
  return router;
11341
11744
  }
11342
11745
  init_dist();
11343
- function createBoardRoutes(deps) {
11344
- const { projectDir, getConfig } = deps;
11345
- const router = Router3();
11346
- router.get("/status", async (_req, res) => {
11746
+ function createBoardRouteHandlers(ctx) {
11747
+ const router = Router3({ mergeParams: true });
11748
+ const p = ctx.pathPrefix;
11749
+ router.get(`/${p}status`, async (req, res) => {
11347
11750
  try {
11348
- const config = getConfig();
11751
+ const config = ctx.getConfig(req);
11752
+ const projectDir = ctx.getProjectDir(req);
11349
11753
  const provider = getBoardProvider(config, projectDir);
11350
11754
  if (!provider) {
11351
11755
  res.status(404).json({ error: "Board not configured" });
@@ -11375,9 +11779,10 @@ function createBoardRoutes(deps) {
11375
11779
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
11376
11780
  }
11377
11781
  });
11378
- router.get("/issues", async (_req, res) => {
11782
+ router.get(`/${p}issues`, async (req, res) => {
11379
11783
  try {
11380
- const config = getConfig();
11784
+ const config = ctx.getConfig(req);
11785
+ const projectDir = ctx.getProjectDir(req);
11381
11786
  const provider = getBoardProvider(config, projectDir);
11382
11787
  if (!provider) {
11383
11788
  res.status(404).json({ error: "Board not configured" });
@@ -11389,9 +11794,10 @@ function createBoardRoutes(deps) {
11389
11794
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
11390
11795
  }
11391
11796
  });
11392
- router.post("/issues", async (req, res) => {
11797
+ router.post(`/${p}issues`, async (req, res) => {
11393
11798
  try {
11394
- const config = getConfig();
11799
+ const config = ctx.getConfig(req);
11800
+ const projectDir = ctx.getProjectDir(req);
11395
11801
  const provider = getBoardProvider(config, projectDir);
11396
11802
  if (!provider) {
11397
11803
  res.status(404).json({ error: "Board not configured" });
@@ -11419,9 +11825,10 @@ function createBoardRoutes(deps) {
11419
11825
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
11420
11826
  }
11421
11827
  });
11422
- router.patch("/issues/:number/move", async (req, res) => {
11828
+ router.patch(`/${p}issues/:number/move`, async (req, res) => {
11423
11829
  try {
11424
- const config = getConfig();
11830
+ const config = ctx.getConfig(req);
11831
+ const projectDir = ctx.getProjectDir(req);
11425
11832
  const provider = getBoardProvider(config, projectDir);
11426
11833
  if (!provider) {
11427
11834
  res.status(404).json({ error: "Board not configured" });
@@ -11448,9 +11855,10 @@ function createBoardRoutes(deps) {
11448
11855
  });
11449
11856
  }
11450
11857
  });
11451
- router.post("/issues/:number/comment", async (req, res) => {
11858
+ router.post(`/${p}issues/:number/comment`, async (req, res) => {
11452
11859
  try {
11453
- const config = getConfig();
11860
+ const config = ctx.getConfig(req);
11861
+ const projectDir = ctx.getProjectDir(req);
11454
11862
  const provider = getBoardProvider(config, projectDir);
11455
11863
  if (!provider) {
11456
11864
  res.status(404).json({ error: "Board not configured" });
@@ -11475,9 +11883,10 @@ function createBoardRoutes(deps) {
11475
11883
  });
11476
11884
  }
11477
11885
  });
11478
- router.delete("/issues/:number", async (req, res) => {
11886
+ router.delete(`/${p}issues/:number`, async (req, res) => {
11479
11887
  try {
11480
- const config = getConfig();
11888
+ const config = ctx.getConfig(req);
11889
+ const projectDir = ctx.getProjectDir(req);
11481
11890
  const provider = getBoardProvider(config, projectDir);
11482
11891
  if (!provider) {
11483
11892
  res.status(404).json({ error: "Board not configured" });
@@ -11499,175 +11908,19 @@ function createBoardRoutes(deps) {
11499
11908
  });
11500
11909
  return router;
11501
11910
  }
11502
- function createProjectBoardRoutes() {
11503
- const router = Router3({ mergeParams: true });
11504
- router.get("/board/status", async (req, res) => {
11505
- try {
11506
- const config = req.projectConfig;
11507
- const projectDir = req.projectDir;
11508
- const provider = getBoardProvider(config, projectDir);
11509
- if (!provider) {
11510
- res.status(404).json({ error: "Board not configured" });
11511
- return;
11512
- }
11513
- const cached = getCachedBoardData(projectDir);
11514
- if (cached) {
11515
- res.json(cached);
11516
- return;
11517
- }
11518
- const issues = await provider.getAllIssues();
11519
- const columns = {
11520
- Draft: [],
11521
- Ready: [],
11522
- "In Progress": [],
11523
- Review: [],
11524
- Done: []
11525
- };
11526
- for (const issue of issues) {
11527
- const col = issue.column ?? "Draft";
11528
- columns[col].push(issue);
11529
- }
11530
- const result = { enabled: true, columns };
11531
- setCachedBoardData(projectDir, result);
11532
- res.json(result);
11533
- } catch (error2) {
11534
- res.status(500).json({
11535
- error: error2 instanceof Error ? error2.message : String(error2)
11536
- });
11537
- }
11538
- });
11539
- router.get("/board/issues", async (_req, res) => {
11540
- try {
11541
- const config = _req.projectConfig;
11542
- const projectDir = _req.projectDir;
11543
- const provider = getBoardProvider(config, projectDir);
11544
- if (!provider) {
11545
- res.status(404).json({ error: "Board not configured" });
11546
- return;
11547
- }
11548
- const issues = await provider.getAllIssues();
11549
- res.json(issues);
11550
- } catch (error2) {
11551
- res.status(500).json({
11552
- error: error2 instanceof Error ? error2.message : String(error2)
11553
- });
11554
- }
11555
- });
11556
- router.post("/board/issues", async (req, res) => {
11557
- try {
11558
- const config = req.projectConfig;
11559
- const projectDir = req.projectDir;
11560
- const provider = getBoardProvider(config, projectDir);
11561
- if (!provider) {
11562
- res.status(404).json({ error: "Board not configured" });
11563
- return;
11564
- }
11565
- const { title, body, column } = req.body;
11566
- if (!title || typeof title !== "string" || title.trim().length === 0) {
11567
- res.status(400).json({ error: "title is required" });
11568
- return;
11569
- }
11570
- if (column && !BOARD_COLUMNS.includes(column)) {
11571
- res.status(400).json({
11572
- error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
11573
- });
11574
- return;
11575
- }
11576
- const issue = await provider.createIssue({
11577
- title: title.trim(),
11578
- body: body ?? "",
11579
- column
11580
- });
11581
- invalidateBoardCache(projectDir);
11582
- res.status(201).json(issue);
11583
- } catch (error2) {
11584
- res.status(500).json({
11585
- error: error2 instanceof Error ? error2.message : String(error2)
11586
- });
11587
- }
11588
- });
11589
- router.patch("/board/issues/:number/move", async (req, res) => {
11590
- try {
11591
- const config = req.projectConfig;
11592
- const projectDir = req.projectDir;
11593
- const provider = getBoardProvider(config, projectDir);
11594
- if (!provider) {
11595
- res.status(404).json({ error: "Board not configured" });
11596
- return;
11597
- }
11598
- const issueNumber = parseInt(req.params.number, 10);
11599
- if (isNaN(issueNumber)) {
11600
- res.status(400).json({ error: "Invalid issue number" });
11601
- return;
11602
- }
11603
- const { column } = req.body;
11604
- if (!column || !BOARD_COLUMNS.includes(column)) {
11605
- res.status(400).json({
11606
- error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
11607
- });
11608
- return;
11609
- }
11610
- await provider.moveIssue(issueNumber, column);
11611
- invalidateBoardCache(projectDir);
11612
- res.json({ moved: true });
11613
- } catch (error2) {
11614
- res.status(500).json({
11615
- error: error2 instanceof Error ? error2.message : String(error2)
11616
- });
11617
- }
11618
- });
11619
- router.post("/board/issues/:number/comment", async (req, res) => {
11620
- try {
11621
- const config = req.projectConfig;
11622
- const projectDir = req.projectDir;
11623
- const provider = getBoardProvider(config, projectDir);
11624
- if (!provider) {
11625
- res.status(404).json({ error: "Board not configured" });
11626
- return;
11627
- }
11628
- const issueNumber = parseInt(req.params.number, 10);
11629
- if (isNaN(issueNumber)) {
11630
- res.status(400).json({ error: "Invalid issue number" });
11631
- return;
11632
- }
11633
- const { body } = req.body;
11634
- if (!body || typeof body !== "string" || body.trim().length === 0) {
11635
- res.status(400).json({ error: "body is required" });
11636
- return;
11637
- }
11638
- await provider.commentOnIssue(issueNumber, body);
11639
- invalidateBoardCache(projectDir);
11640
- res.json({ commented: true });
11641
- } catch (error2) {
11642
- res.status(500).json({
11643
- error: error2 instanceof Error ? error2.message : String(error2)
11644
- });
11645
- }
11911
+ function createBoardRoutes(deps) {
11912
+ return createBoardRouteHandlers({
11913
+ getConfig: () => deps.getConfig(),
11914
+ getProjectDir: () => deps.projectDir,
11915
+ pathPrefix: ""
11646
11916
  });
11647
- router.delete("/board/issues/:number", async (req, res) => {
11648
- try {
11649
- const config = req.projectConfig;
11650
- const projectDir = req.projectDir;
11651
- const provider = getBoardProvider(config, projectDir);
11652
- if (!provider) {
11653
- res.status(404).json({ error: "Board not configured" });
11654
- return;
11655
- }
11656
- const issueNumber = parseInt(req.params.number, 10);
11657
- if (isNaN(issueNumber)) {
11658
- res.status(400).json({ error: "Invalid issue number" });
11659
- return;
11660
- }
11661
- await provider.closeIssue(issueNumber);
11662
- invalidateBoardCache(projectDir);
11663
- res.json({ closed: true });
11664
- } catch (error2) {
11665
- res.status(500).json({
11666
- error: error2 instanceof Error ? error2.message : String(error2)
11667
- });
11668
- }
11917
+ }
11918
+ function createProjectBoardRoutes() {
11919
+ return createBoardRouteHandlers({
11920
+ getConfig: (req) => req.projectConfig,
11921
+ getProjectDir: (req) => req.projectDir,
11922
+ pathPrefix: "board/"
11669
11923
  });
11670
- return router;
11671
11924
  }
11672
11925
  init_dist();
11673
11926
  function validateConfigChanges(changes) {
@@ -11683,6 +11936,9 @@ function validateConfigChanges(changes) {
11683
11936
  if (changes.reviewerEnabled !== void 0 && typeof changes.reviewerEnabled !== "boolean") {
11684
11937
  return "reviewerEnabled must be a boolean";
11685
11938
  }
11939
+ if (changes.executorEnabled !== void 0 && typeof changes.executorEnabled !== "boolean") {
11940
+ return "executorEnabled must be a boolean";
11941
+ }
11686
11942
  if (changes.maxRuntime !== void 0 && (typeof changes.maxRuntime !== "number" || changes.maxRuntime < 60)) {
11687
11943
  return "maxRuntime must be a number >= 60";
11688
11944
  }
@@ -11755,6 +12011,82 @@ function validateConfigChanges(changes) {
11755
12011
  }
11756
12012
  }
11757
12013
  }
12014
+ if (changes.prdDir !== void 0 && (typeof changes.prdDir !== "string" || changes.prdDir.trim().length === 0)) {
12015
+ return "prdDir must be a non-empty string";
12016
+ }
12017
+ if (changes.cronScheduleOffset !== void 0 && (typeof changes.cronScheduleOffset !== "number" || changes.cronScheduleOffset < 0 || changes.cronScheduleOffset > 59)) {
12018
+ return "cronScheduleOffset must be a number between 0 and 59";
12019
+ }
12020
+ if (changes.fallbackOnRateLimit !== void 0 && typeof changes.fallbackOnRateLimit !== "boolean") {
12021
+ return "fallbackOnRateLimit must be a boolean";
12022
+ }
12023
+ if (changes.claudeModel !== void 0 && !VALID_CLAUDE_MODELS.includes(changes.claudeModel)) {
12024
+ return `Invalid claudeModel. Must be one of: ${VALID_CLAUDE_MODELS.join(", ")}`;
12025
+ }
12026
+ if (changes.qa !== void 0) {
12027
+ if (typeof changes.qa !== "object" || changes.qa === null) {
12028
+ return "qa must be an object";
12029
+ }
12030
+ const qa = changes.qa;
12031
+ if (qa.enabled !== void 0 && typeof qa.enabled !== "boolean") {
12032
+ return "qa.enabled must be a boolean";
12033
+ }
12034
+ if (qa.schedule !== void 0 && (typeof qa.schedule !== "string" || qa.schedule.trim().length === 0)) {
12035
+ return "qa.schedule must be a non-empty string";
12036
+ }
12037
+ if (qa.maxRuntime !== void 0 && (typeof qa.maxRuntime !== "number" || qa.maxRuntime < 60)) {
12038
+ return "qa.maxRuntime must be a number >= 60";
12039
+ }
12040
+ if (qa.branchPatterns !== void 0) {
12041
+ if (!Array.isArray(qa.branchPatterns) || !qa.branchPatterns.every((p) => typeof p === "string")) {
12042
+ return "qa.branchPatterns must be an array of strings";
12043
+ }
12044
+ }
12045
+ if (qa.artifacts !== void 0) {
12046
+ const validArtifacts = ["screenshot", "video", "both"];
12047
+ if (!validArtifacts.includes(qa.artifacts)) {
12048
+ return `Invalid qa.artifacts. Must be one of: ${validArtifacts.join(", ")}`;
12049
+ }
12050
+ }
12051
+ if (qa.skipLabel !== void 0 && typeof qa.skipLabel !== "string") {
12052
+ return "qa.skipLabel must be a string";
12053
+ }
12054
+ if (qa.autoInstallPlaywright !== void 0 && typeof qa.autoInstallPlaywright !== "boolean") {
12055
+ return "qa.autoInstallPlaywright must be a boolean";
12056
+ }
12057
+ }
12058
+ if (changes.audit !== void 0) {
12059
+ if (typeof changes.audit !== "object" || changes.audit === null) {
12060
+ return "audit must be an object";
12061
+ }
12062
+ const audit = changes.audit;
12063
+ if (audit.enabled !== void 0 && typeof audit.enabled !== "boolean") {
12064
+ return "audit.enabled must be a boolean";
12065
+ }
12066
+ if (audit.schedule !== void 0 && (typeof audit.schedule !== "string" || audit.schedule.trim().length === 0)) {
12067
+ return "audit.schedule must be a non-empty string";
12068
+ }
12069
+ if (audit.maxRuntime !== void 0 && (typeof audit.maxRuntime !== "number" || audit.maxRuntime < 60)) {
12070
+ return "audit.maxRuntime must be a number >= 60";
12071
+ }
12072
+ }
12073
+ if (changes.roadmapScanner !== void 0) {
12074
+ const rs = changes.roadmapScanner;
12075
+ if (rs.slicerSchedule !== void 0 && (typeof rs.slicerSchedule !== "string" || rs.slicerSchedule.trim().length === 0)) {
12076
+ return "roadmapScanner.slicerSchedule must be a non-empty string";
12077
+ }
12078
+ if (rs.slicerMaxRuntime !== void 0 && (typeof rs.slicerMaxRuntime !== "number" || rs.slicerMaxRuntime < 60)) {
12079
+ return "roadmapScanner.slicerMaxRuntime must be a number >= 60";
12080
+ }
12081
+ }
12082
+ if (changes.boardProvider !== void 0) {
12083
+ if (typeof changes.boardProvider !== "object" || changes.boardProvider === null) {
12084
+ return "boardProvider must be an object";
12085
+ }
12086
+ if (changes.boardProvider.enabled !== void 0 && typeof changes.boardProvider.enabled !== "boolean") {
12087
+ return "boardProvider.enabled must be a boolean";
12088
+ }
12089
+ }
11758
12090
  return null;
11759
12091
  }
11760
12092
  function createConfigRoutes(deps) {
@@ -11822,7 +12154,7 @@ init_dist();
11822
12154
  function runDoctorChecks(projectDir, config) {
11823
12155
  const checks = [];
11824
12156
  try {
11825
- execSync6("git rev-parse --is-inside-work-tree", {
12157
+ execSync5("git rev-parse --is-inside-work-tree", {
11826
12158
  cwd: projectDir,
11827
12159
  stdio: "pipe"
11828
12160
  });
@@ -11831,7 +12163,7 @@ function runDoctorChecks(projectDir, config) {
11831
12163
  checks.push({ name: "git", status: "fail", detail: "Not a git repository" });
11832
12164
  }
11833
12165
  try {
11834
- execSync6(`which ${config.provider}`, { stdio: "pipe" });
12166
+ execSync5(`which ${config.provider}`, { stdio: "pipe" });
11835
12167
  checks.push({
11836
12168
  name: "provider",
11837
12169
  status: "pass",
@@ -11927,7 +12259,7 @@ function createLogRoutes(deps) {
11927
12259
  router.get("/:name", (req, res) => {
11928
12260
  try {
11929
12261
  const { name } = req.params;
11930
- const validNames = ["executor", "reviewer", "qa"];
12262
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
11931
12263
  if (!validNames.includes(name)) {
11932
12264
  res.status(400).json({
11933
12265
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -11953,7 +12285,7 @@ function createProjectLogRoutes() {
11953
12285
  try {
11954
12286
  const projectDir = req.projectDir;
11955
12287
  const { name } = req.params;
11956
- const validNames = ["executor", "reviewer", "qa"];
12288
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
11957
12289
  if (!validNames.includes(name)) {
11958
12290
  res.status(400).json({
11959
12291
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -11994,12 +12326,13 @@ function createProjectPrdRoutes() {
11994
12326
  return router;
11995
12327
  }
11996
12328
  init_dist();
11997
- function createRoadmapRoutes(deps) {
11998
- const { projectDir, getConfig, reloadConfig } = deps;
11999
- const router = Router8();
12000
- router.get("/", (_req, res) => {
12329
+ function createRoadmapRouteHandlers(ctx) {
12330
+ const router = Router8({ mergeParams: true });
12331
+ const p = ctx.pathPrefix;
12332
+ router.get(`/${p}`, (req, res) => {
12001
12333
  try {
12002
- const config = getConfig();
12334
+ const config = ctx.getConfig(req);
12335
+ const projectDir = ctx.getProjectDir(req);
12003
12336
  const status = getRoadmapStatus(projectDir, config);
12004
12337
  const prdDir = path27.join(projectDir, config.prdDir);
12005
12338
  const state = loadRoadmapState(prdDir);
@@ -12012,9 +12345,10 @@ function createRoadmapRoutes(deps) {
12012
12345
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12013
12346
  }
12014
12347
  });
12015
- router.post("/scan", async (_req, res) => {
12348
+ router.post(`/${p}scan`, async (req, res) => {
12016
12349
  try {
12017
- const config = getConfig();
12350
+ const config = ctx.getConfig(req);
12351
+ const projectDir = ctx.getProjectDir(req);
12018
12352
  if (!config.roadmapScanner.enabled) {
12019
12353
  res.status(409).json({ error: "Roadmap scanner is disabled" });
12020
12354
  return;
@@ -12027,14 +12361,15 @@ function createRoadmapRoutes(deps) {
12027
12361
  });
12028
12362
  }
12029
12363
  });
12030
- router.put("/toggle", (req, res) => {
12364
+ router.put(`/${p}toggle`, (req, res) => {
12031
12365
  try {
12032
12366
  const { enabled } = req.body;
12033
12367
  if (typeof enabled !== "boolean") {
12034
12368
  res.status(400).json({ error: "enabled must be a boolean" });
12035
12369
  return;
12036
12370
  }
12037
- const currentConfig = getConfig();
12371
+ const projectDir = ctx.getProjectDir(req);
12372
+ const currentConfig = ctx.getConfig(req);
12038
12373
  const result = saveConfig(projectDir, {
12039
12374
  roadmapScanner: {
12040
12375
  ...currentConfig.roadmapScanner,
@@ -12045,71 +12380,30 @@ function createRoadmapRoutes(deps) {
12045
12380
  res.status(500).json({ error: result.error });
12046
12381
  return;
12047
12382
  }
12048
- reloadConfig();
12049
- res.json(getConfig());
12383
+ ctx.afterToggle(req);
12384
+ res.json(loadConfig(projectDir));
12050
12385
  } catch (error2) {
12051
12386
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12052
12387
  }
12053
12388
  });
12054
12389
  return router;
12055
12390
  }
12056
- function createProjectRoadmapRoutes() {
12057
- const router = Router8({ mergeParams: true });
12058
- router.get("/roadmap", (req, res) => {
12059
- try {
12060
- const config = req.projectConfig;
12061
- const projectDir = req.projectDir;
12062
- const status = getRoadmapStatus(projectDir, config);
12063
- const prdDir = path27.join(projectDir, config.prdDir);
12064
- const state = loadRoadmapState(prdDir);
12065
- res.json({
12066
- ...status,
12067
- lastScan: state.lastScan || null,
12068
- autoScanInterval: config.roadmapScanner.autoScanInterval
12069
- });
12070
- } catch (error2) {
12071
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12072
- }
12073
- });
12074
- router.post("/roadmap/scan", async (req, res) => {
12075
- try {
12076
- const config = req.projectConfig;
12077
- const projectDir = req.projectDir;
12078
- if (!config.roadmapScanner.enabled) {
12079
- res.status(409).json({ error: "Roadmap scanner is disabled" });
12080
- return;
12081
- }
12082
- const result = await scanRoadmap(projectDir, config);
12083
- res.json(result);
12084
- } catch (error2) {
12085
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12086
- }
12391
+ function createRoadmapRoutes(deps) {
12392
+ return createRoadmapRouteHandlers({
12393
+ getConfig: () => deps.getConfig(),
12394
+ getProjectDir: () => deps.projectDir,
12395
+ afterToggle: () => deps.reloadConfig(),
12396
+ pathPrefix: ""
12087
12397
  });
12088
- router.put("/roadmap/toggle", (req, res) => {
12089
- const projectDir = req.projectDir;
12090
- try {
12091
- const { enabled } = req.body;
12092
- if (typeof enabled !== "boolean") {
12093
- res.status(400).json({ error: "enabled must be a boolean" });
12094
- return;
12095
- }
12096
- const currentConfig = req.projectConfig;
12097
- const result = saveConfig(projectDir, {
12098
- roadmapScanner: {
12099
- ...currentConfig.roadmapScanner,
12100
- enabled
12101
- }
12102
- });
12103
- if (!result.success) {
12104
- res.status(500).json({ error: result.error });
12105
- return;
12106
- }
12107
- res.json(loadConfig(projectDir));
12108
- } catch (error2) {
12109
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12110
- }
12398
+ }
12399
+ function createProjectRoadmapRoutes() {
12400
+ return createRoadmapRouteHandlers({
12401
+ getConfig: (req) => req.projectConfig,
12402
+ getProjectDir: (req) => req.projectDir,
12403
+ afterToggle: () => {
12404
+ },
12405
+ pathPrefix: "roadmap/"
12111
12406
  });
12112
- return router;
12113
12407
  }
12114
12408
  init_dist();
12115
12409
  function createStatusRoutes(deps) {
@@ -12121,21 +12415,20 @@ function createStatusRoutes(deps) {
12121
12415
  res.setHeader("Connection", "keep-alive");
12122
12416
  res.flushHeaders();
12123
12417
  sseClients.add(res);
12124
- try {
12125
- const snapshot = fetchStatusSnapshot(projectDir, getConfig());
12418
+ fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
12126
12419
  res.write(`event: status_changed
12127
12420
  data: ${JSON.stringify(snapshot)}
12128
12421
 
12129
12422
  `);
12130
- } catch {
12131
- }
12423
+ }).catch(() => {
12424
+ });
12132
12425
  req.on("close", () => {
12133
12426
  sseClients.delete(res);
12134
12427
  });
12135
12428
  });
12136
- router.get("/", (_req, res) => {
12429
+ router.get("/", async (_req, res) => {
12137
12430
  try {
12138
- const snapshot = fetchStatusSnapshot(projectDir, getConfig());
12431
+ const snapshot = await fetchStatusSnapshot(projectDir, getConfig());
12139
12432
  res.json(snapshot);
12140
12433
  } catch (error2) {
12141
12434
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
@@ -12151,30 +12444,49 @@ function computeNextRun(cronExpr) {
12151
12444
  return null;
12152
12445
  }
12153
12446
  }
12447
+ function hasScheduledCommand(entries, command) {
12448
+ const commandPattern = new RegExp(`\\s${command}\\s+>>`);
12449
+ return entries.some((entry) => commandPattern.test(entry));
12450
+ }
12154
12451
  function createScheduleInfoRoutes(deps) {
12155
12452
  const { projectDir, getConfig } = deps;
12156
12453
  const router = Router9();
12157
- router.get("/", (_req, res) => {
12454
+ router.get("/", async (_req, res) => {
12158
12455
  try {
12159
12456
  const config = getConfig();
12160
- const snapshot = fetchStatusSnapshot(projectDir, config);
12457
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
12161
12458
  const installed = snapshot.crontab.installed;
12162
12459
  const entries = snapshot.crontab.entries;
12460
+ const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
12461
+ const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
12462
+ const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
12463
+ const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
12464
+ const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
12163
12465
  res.json({
12164
12466
  executor: {
12165
12467
  schedule: config.cronSchedule,
12166
- installed,
12167
- nextRun: installed ? computeNextRun(config.cronSchedule) : null
12468
+ installed: executorInstalled,
12469
+ nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
12168
12470
  },
12169
12471
  reviewer: {
12170
12472
  schedule: config.reviewerSchedule,
12171
- installed: installed && config.reviewerEnabled,
12172
- nextRun: installed && config.reviewerEnabled ? computeNextRun(config.reviewerSchedule) : null
12473
+ installed: reviewerInstalled,
12474
+ nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
12173
12475
  },
12174
12476
  qa: {
12175
12477
  schedule: config.qa.schedule,
12176
- installed: installed && config.qa.enabled,
12177
- nextRun: installed && config.qa.enabled ? computeNextRun(config.qa.schedule) : null
12478
+ installed: qaInstalled,
12479
+ nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
12480
+ },
12481
+ audit: {
12482
+ schedule: config.audit.schedule,
12483
+ installed: auditInstalled,
12484
+ nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
12485
+ },
12486
+ planner: {
12487
+ schedule: config.roadmapScanner.slicerSchedule,
12488
+ installed: plannerInstalled,
12489
+ nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
12178
12490
  },
12179
12491
  paused: !installed,
12180
12492
  entries
@@ -12190,13 +12502,12 @@ function createProjectSseRoutes(deps) {
12190
12502
  const router = Router9({ mergeParams: true });
12191
12503
  router.get("/status/events", (req, res) => {
12192
12504
  const projectDir = req.projectDir;
12193
- const config = req.projectConfig;
12194
12505
  if (!projectSseClients.has(projectDir)) {
12195
12506
  projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
12196
12507
  }
12197
12508
  const clients = projectSseClients.get(projectDir);
12198
12509
  if (!projectSseWatchers.has(projectDir)) {
12199
- const watcher = startSseStatusWatcher(clients, projectDir, () => req.projectConfig);
12510
+ const watcher = startSseStatusWatcher(clients, projectDir, () => loadConfig(projectDir));
12200
12511
  projectSseWatchers.set(projectDir, watcher);
12201
12512
  }
12202
12513
  res.setHeader("Content-Type", "text/event-stream");
@@ -12204,48 +12515,69 @@ function createProjectSseRoutes(deps) {
12204
12515
  res.setHeader("Connection", "keep-alive");
12205
12516
  res.flushHeaders();
12206
12517
  clients.add(res);
12207
- try {
12208
- const snapshot = fetchStatusSnapshot(projectDir, config);
12518
+ fetchStatusSnapshot(projectDir, loadConfig(projectDir)).then((snapshot) => {
12209
12519
  res.write(`event: status_changed
12210
12520
  data: ${JSON.stringify(snapshot)}
12211
12521
 
12212
12522
  `);
12213
- } catch {
12214
- }
12523
+ }).catch(() => {
12524
+ });
12215
12525
  req.on("close", () => {
12216
12526
  clients.delete(res);
12527
+ if (clients.size === 0) {
12528
+ const watcher = projectSseWatchers.get(projectDir);
12529
+ if (watcher !== void 0) {
12530
+ clearInterval(watcher);
12531
+ projectSseWatchers.delete(projectDir);
12532
+ }
12533
+ }
12217
12534
  });
12218
12535
  });
12219
- router.get("/status", (req, res) => {
12536
+ router.get("/status", async (req, res) => {
12220
12537
  try {
12221
- const snapshot = fetchStatusSnapshot(req.projectDir, req.projectConfig);
12538
+ const snapshot = await fetchStatusSnapshot(req.projectDir, req.projectConfig);
12222
12539
  res.json(snapshot);
12223
12540
  } catch (error2) {
12224
12541
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12225
12542
  }
12226
12543
  });
12227
- router.get("/schedule-info", (req, res) => {
12544
+ router.get("/schedule-info", async (req, res) => {
12228
12545
  try {
12229
12546
  const config = req.projectConfig;
12230
12547
  const projectDir = req.projectDir;
12231
- const snapshot = fetchStatusSnapshot(projectDir, config);
12548
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
12232
12549
  const installed = snapshot.crontab.installed;
12233
12550
  const entries = snapshot.crontab.entries;
12551
+ const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
12552
+ const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
12553
+ const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
12554
+ const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
12555
+ const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
12234
12556
  res.json({
12235
12557
  executor: {
12236
12558
  schedule: config.cronSchedule,
12237
- installed,
12238
- nextRun: installed ? computeNextRun(config.cronSchedule) : null
12559
+ installed: executorInstalled,
12560
+ nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
12239
12561
  },
12240
12562
  reviewer: {
12241
12563
  schedule: config.reviewerSchedule,
12242
- installed: installed && config.reviewerEnabled,
12243
- nextRun: installed && config.reviewerEnabled ? computeNextRun(config.reviewerSchedule) : null
12564
+ installed: reviewerInstalled,
12565
+ nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
12244
12566
  },
12245
12567
  qa: {
12246
12568
  schedule: config.qa.schedule,
12247
- installed: installed && config.qa.enabled,
12248
- nextRun: installed && config.qa.enabled ? computeNextRun(config.qa.schedule) : null
12569
+ installed: qaInstalled,
12570
+ nextRun: qaInstalled ? computeNextRun(config.qa.schedule) : null
12571
+ },
12572
+ audit: {
12573
+ schedule: config.audit.schedule,
12574
+ installed: auditInstalled,
12575
+ nextRun: auditInstalled ? computeNextRun(config.audit.schedule) : null
12576
+ },
12577
+ planner: {
12578
+ schedule: config.roadmapScanner.slicerSchedule,
12579
+ installed: plannerInstalled,
12580
+ nextRun: plannerInstalled ? computeNextRun(config.roadmapScanner.slicerSchedule) : null
12249
12581
  },
12250
12582
  paused: !installed,
12251
12583
  entries
@@ -12320,9 +12652,9 @@ function createApp(projectDir) {
12320
12652
  app.use("/api/roadmap", createRoadmapRoutes({ projectDir, getConfig: () => config, reloadConfig }));
12321
12653
  app.use("/api/logs", createLogRoutes({ projectDir }));
12322
12654
  app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
12323
- app.get("/api/prs", (_req, res) => {
12655
+ app.get("/api/prs", async (_req, res) => {
12324
12656
  try {
12325
- res.json(collectPrInfo(projectDir, config.branchPatterns));
12657
+ res.json(await collectPrInfo(projectDir, config.branchPatterns));
12326
12658
  } catch (error2) {
12327
12659
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12328
12660
  }
@@ -12366,9 +12698,9 @@ function createProjectRouter() {
12366
12698
  router.use("/agents", createAgentRoutes());
12367
12699
  router.use(createProjectActionRoutes({ projectSseClients }));
12368
12700
  router.use(createProjectRoadmapRoutes());
12369
- router.get("/prs", (req, res) => {
12701
+ router.get("/prs", async (req, res) => {
12370
12702
  try {
12371
- res.json(collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
12703
+ res.json(await collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
12372
12704
  } catch (error2) {
12373
12705
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12374
12706
  }
@@ -12758,7 +13090,7 @@ function prsCommand(program2) {
12758
13090
  }
12759
13091
  const projectDir = process.cwd();
12760
13092
  const config = loadConfig(projectDir);
12761
- const prs = collectPrInfo(projectDir, config.branchPatterns);
13093
+ const prs = await collectPrInfo(projectDir, config.branchPatterns);
12762
13094
  if (options.json) {
12763
13095
  const output = {
12764
13096
  prs,
@@ -12805,7 +13137,7 @@ function prsCommand(program2) {
12805
13137
  init_dist();
12806
13138
  function getOpenPrBranches(projectDir) {
12807
13139
  try {
12808
- execSync7("git rev-parse --git-dir", {
13140
+ execSync6("git rev-parse --git-dir", {
12809
13141
  cwd: projectDir,
12810
13142
  encoding: "utf-8",
12811
13143
  stdio: ["pipe", "pipe", "pipe"]
@@ -12814,12 +13146,12 @@ function getOpenPrBranches(projectDir) {
12814
13146
  return /* @__PURE__ */ new Set();
12815
13147
  }
12816
13148
  try {
12817
- execSync7("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13149
+ execSync6("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12818
13150
  } catch {
12819
13151
  return /* @__PURE__ */ new Set();
12820
13152
  }
12821
13153
  try {
12822
- const output = execSync7("gh pr list --state open --json headRefName", {
13154
+ const output = execSync6("gh pr list --state open --json headRefName", {
12823
13155
  cwd: projectDir,
12824
13156
  encoding: "utf-8",
12825
13157
  stdio: ["pipe", "pipe", "pipe"]
@@ -13120,6 +13452,36 @@ function cancelCommand(program2) {
13120
13452
  });
13121
13453
  }
13122
13454
  init_dist();
13455
+ function getTelegramStatusWebhooks3(config) {
13456
+ return (config.notifications?.webhooks ?? []).filter((wh) => wh.type === "telegram" && typeof wh.botToken === "string" && wh.botToken.trim().length > 0 && typeof wh.chatId === "string" && wh.chatId.trim().length > 0).map((wh) => ({ botToken: wh.botToken, chatId: wh.chatId }));
13457
+ }
13458
+ function plannerLockPath2(projectDir) {
13459
+ return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
13460
+ }
13461
+ function acquirePlannerLock(projectDir) {
13462
+ const lockFile = plannerLockPath2(projectDir);
13463
+ if (fs31.existsSync(lockFile)) {
13464
+ const pidRaw = fs31.readFileSync(lockFile, "utf-8").trim();
13465
+ const pid = parseInt(pidRaw, 10);
13466
+ if (!Number.isNaN(pid) && isProcessRunning(pid)) {
13467
+ return { acquired: false, lockFile, pid };
13468
+ }
13469
+ try {
13470
+ fs31.unlinkSync(lockFile);
13471
+ } catch {
13472
+ }
13473
+ }
13474
+ fs31.writeFileSync(lockFile, String(process.pid));
13475
+ return { acquired: true, lockFile };
13476
+ }
13477
+ function releasePlannerLock(lockFile) {
13478
+ try {
13479
+ if (fs31.existsSync(lockFile)) {
13480
+ fs31.unlinkSync(lockFile);
13481
+ }
13482
+ } catch {
13483
+ }
13484
+ }
13123
13485
  function buildEnvVars5(config, options) {
13124
13486
  const env = {};
13125
13487
  const slicerProvider = resolveJobProvider(config, "slicer");
@@ -13130,6 +13492,12 @@ function buildEnvVars5(config, options) {
13130
13492
  if (config.providerEnv) {
13131
13493
  Object.assign(env, config.providerEnv);
13132
13494
  }
13495
+ const telegramWebhooks = getTelegramStatusWebhooks3(config);
13496
+ if (telegramWebhooks.length > 0) {
13497
+ env.NW_TELEGRAM_STATUS_WEBHOOKS = JSON.stringify(telegramWebhooks);
13498
+ env.NW_TELEGRAM_BOT_TOKEN = telegramWebhooks[0].botToken;
13499
+ env.NW_TELEGRAM_CHAT_ID = telegramWebhooks[0].chatId;
13500
+ }
13133
13501
  if (options.dryRun) {
13134
13502
  env.NW_DRY_RUN = "1";
13135
13503
  }
@@ -13153,13 +13521,20 @@ function applyCliOverrides4(config, options) {
13153
13521
  return overridden;
13154
13522
  }
13155
13523
  function sliceCommand(program2) {
13156
- program2.command("slice").description("Run roadmap slicer to create PRD from next roadmap item").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds for slicer").option("--provider <string>", "AI provider to use (claude or codex)").action(async (options) => {
13524
+ program2.command("slice").alias("planner").description("Run Planner (roadmap slicer) to create a PRD from the next roadmap item").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds for slicer").option("--provider <string>", "AI provider to use (claude or codex)").action(async (options) => {
13157
13525
  const projectDir = process.cwd();
13526
+ const lockResult = acquirePlannerLock(projectDir);
13527
+ if (!lockResult.acquired) {
13528
+ info(`Planner is already running${lockResult.pid ? ` (PID ${lockResult.pid})` : ""}`);
13529
+ process.exit(0);
13530
+ }
13531
+ const cleanupLock = () => releasePlannerLock(lockResult.lockFile);
13532
+ process.on("exit", cleanupLock);
13158
13533
  let config = loadConfig(projectDir);
13159
13534
  config = applyCliOverrides4(config, options);
13160
13535
  const envVars = buildEnvVars5(config, options);
13161
13536
  if (options.dryRun) {
13162
- header("Dry Run: Roadmap Slicer");
13537
+ header("Dry Run: Planner");
13163
13538
  const slicerProvider = resolveJobProvider(config, "slicer");
13164
13539
  header("Configuration");
13165
13540
  const configTable = createTable({ head: ["Setting", "Value"] });
@@ -13168,10 +13543,10 @@ function sliceCommand(program2) {
13168
13543
  configTable.push(["PRD Directory", config.prdDir]);
13169
13544
  configTable.push(["Roadmap Path", config.roadmapScanner.roadmapPath]);
13170
13545
  configTable.push([
13171
- "Slicer Max Runtime",
13546
+ "Planner Max Runtime",
13172
13547
  `${config.roadmapScanner.slicerMaxRuntime}s (${Math.floor(config.roadmapScanner.slicerMaxRuntime / 60)}min)`
13173
13548
  ]);
13174
- configTable.push(["Slicer Schedule", config.roadmapScanner.slicerSchedule]);
13549
+ configTable.push(["Planner Schedule", config.roadmapScanner.slicerSchedule]);
13175
13550
  configTable.push(["Scanner Enabled", config.roadmapScanner.enabled ? "Yes" : "No"]);
13176
13551
  console.log(configTable.toString());
13177
13552
  header("Roadmap Status");
@@ -13215,38 +13590,51 @@ function sliceCommand(program2) {
13215
13590
  process.exit(0);
13216
13591
  }
13217
13592
  if (!config.roadmapScanner.enabled) {
13218
- error("Roadmap scanner is disabled. Enable it in night-watch.config.json to use the slicer.");
13219
- process.exit(1);
13593
+ info("Planner is disabled in config; skipping run.");
13594
+ process.exit(0);
13220
13595
  }
13221
- const spinner = createSpinner("Running roadmap slicer...");
13596
+ const spinner = createSpinner("Running Planner...");
13222
13597
  spinner.start();
13223
13598
  try {
13599
+ if (!options.dryRun) {
13600
+ await sendNotifications(config, {
13601
+ event: "run_started",
13602
+ projectName: path31.basename(projectDir),
13603
+ exitCode: 0,
13604
+ provider: config.provider
13605
+ });
13606
+ }
13224
13607
  const result = await sliceNextItem(projectDir, config);
13225
13608
  if (result.sliced) {
13226
- spinner.succeed(`Slicer completed successfully: Created ${result.file}`);
13609
+ spinner.succeed(`Planner completed successfully: Created ${result.file}`);
13227
13610
  } else if (result.error) {
13228
13611
  if (result.error === "No pending items to process") {
13229
13612
  spinner.succeed("No pending items to process");
13230
13613
  } else {
13231
- spinner.fail(`Slicer failed: ${result.error}`);
13614
+ spinner.fail(`Planner failed: ${result.error}`);
13232
13615
  }
13233
13616
  }
13234
13617
  const nothingPending = result.error === "No pending items to process";
13235
13618
  const exitCode = result.sliced || nothingPending ? 0 : 1;
13236
13619
  if (!options.dryRun && result.sliced) {
13237
- const event = "run_succeeded";
13238
- const _sliceCtx = {
13239
- event,
13620
+ await sendNotifications(config, {
13621
+ event: "run_succeeded",
13240
13622
  projectName: path31.basename(projectDir),
13241
13623
  exitCode,
13242
13624
  provider: config.provider,
13243
13625
  prTitle: result.item?.title
13244
- };
13245
- await sendNotifications(config, _sliceCtx);
13626
+ });
13627
+ } else if (!options.dryRun && !nothingPending) {
13628
+ await sendNotifications(config, {
13629
+ event: "run_failed",
13630
+ projectName: path31.basename(projectDir),
13631
+ exitCode,
13632
+ provider: config.provider
13633
+ });
13246
13634
  }
13247
13635
  process.exit(exitCode);
13248
13636
  } catch (err) {
13249
- spinner.fail("Failed to execute slice command");
13637
+ spinner.fail("Failed to execute planner command");
13250
13638
  error(`${err instanceof Error ? err.message : String(err)}`);
13251
13639
  process.exit(1);
13252
13640
  }
@@ -13351,7 +13739,7 @@ async function confirmPrompt(question) {
13351
13739
  }
13352
13740
  async function createGitHubLabel(label2, cwd) {
13353
13741
  try {
13354
- execFileSync5("gh", ["label", "create", label2.name, "--description", label2.description, "--color", label2.color], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13742
+ execFileSync3("gh", ["label", "create", label2.name, "--description", label2.description, "--color", label2.color], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13355
13743
  return { created: true, skipped: false };
13356
13744
  } catch (err) {
13357
13745
  const output = err instanceof Error ? err.message : String(err);
@@ -13491,11 +13879,11 @@ function boardCommand(program2) {
13491
13879
  let body = options.body ?? "";
13492
13880
  if (options.bodyFile) {
13493
13881
  const filePath = options.bodyFile;
13494
- if (!fs31.existsSync(filePath)) {
13882
+ if (!fs32.existsSync(filePath)) {
13495
13883
  console.error(`File not found: ${filePath}`);
13496
13884
  process.exit(1);
13497
13885
  }
13498
- body = fs31.readFileSync(filePath, "utf-8");
13886
+ body = fs32.readFileSync(filePath, "utf-8");
13499
13887
  }
13500
13888
  const labels = [];
13501
13889
  if (options.label) {
@@ -13704,11 +14092,11 @@ function boardCommand(program2) {
13704
14092
  const provider = getProvider(config, cwd);
13705
14093
  await ensureBoardConfigured(config, cwd, provider);
13706
14094
  const roadmapPath = options.roadmap ?? path33.join(cwd, "ROADMAP.md");
13707
- if (!fs31.existsSync(roadmapPath)) {
14095
+ if (!fs32.existsSync(roadmapPath)) {
13708
14096
  console.error(`Roadmap file not found: ${roadmapPath}`);
13709
14097
  process.exit(1);
13710
14098
  }
13711
- const roadmapContent = fs31.readFileSync(roadmapPath, "utf-8");
14099
+ const roadmapContent = fs32.readFileSync(roadmapPath, "utf-8");
13712
14100
  const items = parseRoadmap(roadmapContent);
13713
14101
  const uncheckedItems = getUncheckedItems(items);
13714
14102
  if (uncheckedItems.length === 0) {
@@ -13802,7 +14190,7 @@ function boardCommand(program2) {
13802
14190
  try {
13803
14191
  const labelsToAdd = [category, horizon].filter((l) => !issue.labels.includes(l));
13804
14192
  if (labelsToAdd.length > 0) {
13805
- execFileSync5("gh", ["issue", "edit", String(issue.number), "--add-label", labelsToAdd.join(",")], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
14193
+ execFileSync3("gh", ["issue", "edit", String(issue.number), "--add-label", labelsToAdd.join(",")], { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13806
14194
  }
13807
14195
  updated++;
13808
14196
  success(`Updated labels on #${issue.number}: ${item.title}`);
@@ -13826,14 +14214,14 @@ var __dirname4 = dirname8(__filename3);
13826
14214
  function findPackageRoot(dir) {
13827
14215
  let d = dir;
13828
14216
  for (let i = 0; i < 5; i++) {
13829
- if (existsSync25(join30(d, "package.json")))
14217
+ if (existsSync26(join30(d, "package.json")))
13830
14218
  return d;
13831
14219
  d = dirname8(d);
13832
14220
  }
13833
14221
  return dir;
13834
14222
  }
13835
14223
  var packageRoot = findPackageRoot(__dirname4);
13836
- var packageJson = JSON.parse(readFileSync15(join30(packageRoot, "package.json"), "utf-8"));
14224
+ var packageJson = JSON.parse(readFileSync16(join30(packageRoot, "package.json"), "utf-8"));
13837
14225
  var program = new Command2();
13838
14226
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
13839
14227
  initCommand(program);