@jonit-dev/night-watch-cli 1.7.46 → 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.
Files changed (48) hide show
  1. package/dist/cli.js +1097 -705
  2. package/dist/commands/audit.d.ts.map +1 -1
  3. package/dist/commands/audit.js +21 -1
  4. package/dist/commands/audit.js.map +1 -1
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +14 -37
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/install.d.ts.map +1 -1
  9. package/dist/commands/install.js +20 -12
  10. package/dist/commands/install.js.map +1 -1
  11. package/dist/commands/logs.d.ts.map +1 -1
  12. package/dist/commands/logs.js +37 -5
  13. package/dist/commands/logs.js.map +1 -1
  14. package/dist/commands/prd.d.ts.map +1 -1
  15. package/dist/commands/prd.js +2 -6
  16. package/dist/commands/prd.js.map +1 -1
  17. package/dist/commands/prds.d.ts.map +1 -1
  18. package/dist/commands/prds.js +1 -1
  19. package/dist/commands/prds.js.map +1 -1
  20. package/dist/commands/qa.d.ts.map +1 -1
  21. package/dist/commands/qa.js +20 -0
  22. package/dist/commands/qa.js.map +1 -1
  23. package/dist/commands/review.d.ts +10 -0
  24. package/dist/commands/review.d.ts.map +1 -1
  25. package/dist/commands/review.js +38 -0
  26. package/dist/commands/review.js.map +1 -1
  27. package/dist/commands/run.d.ts.map +1 -1
  28. package/dist/commands/run.js +7 -2
  29. package/dist/commands/run.js.map +1 -1
  30. package/dist/commands/slice.d.ts.map +1 -1
  31. package/dist/commands/slice.js +88 -17
  32. package/dist/commands/slice.js.map +1 -1
  33. package/dist/commands/status.d.ts.map +1 -1
  34. package/dist/commands/status.js +75 -0
  35. package/dist/commands/status.js.map +1 -1
  36. package/dist/scripts/night-watch-audit-cron.sh +19 -0
  37. package/dist/scripts/night-watch-cron.sh +2 -2
  38. package/dist/scripts/night-watch-helpers.sh +47 -1
  39. package/dist/scripts/night-watch-pr-reviewer-cron.sh +168 -35
  40. package/dist/scripts/night-watch-qa-cron.sh +19 -0
  41. package/dist/scripts/night-watch-slicer-cron.sh +10 -1
  42. package/dist/templates/night-watch-slicer.md +18 -3
  43. package/dist/templates/night-watch.config.json +1 -0
  44. package/dist/templates/night-watch.md +10 -37
  45. package/dist/web/assets/index-Ba-4YvTQ.js +365 -0
  46. package/dist/web/assets/index-DpVirMEe.css +1 -0
  47. package/dist/web/index.html +2 -2
  48. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -5,13 +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
8
  import * as fs from "fs";
16
9
  import * as path from "path";
17
10
  import { fileURLToPath } from "url";
@@ -34,8 +27,10 @@ import * as path2 from "path";
34
27
  import Database7 from "better-sqlite3";
35
28
  import "reflect-metadata";
36
29
  import { container } from "tsyringe";
37
- import { execFileSync } from "child_process";
38
- 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";
39
34
  import * as fs3 from "fs";
40
35
  import * as path3 from "path";
41
36
  import { execSync } from "child_process";
@@ -43,18 +38,19 @@ import * as fs4 from "fs";
43
38
  import * as os2 from "os";
44
39
  import * as path4 from "path";
45
40
  import { createHash } from "crypto";
46
- import { execSync as execSync2 } from "child_process";
41
+ import { exec } from "child_process";
42
+ import { promisify as promisify3 } from "util";
47
43
  import * as fs5 from "fs";
48
44
  import * as path5 from "path";
49
45
  import * as fs6 from "fs";
50
46
  import * as fs7 from "fs";
51
47
  import * as path6 from "path";
52
- import { execSync as execSync3 } from "child_process";
48
+ import { execSync as execSync2 } from "child_process";
53
49
  import * as fs8 from "fs";
54
50
  import * as path7 from "path";
55
51
  import * as os3 from "os";
56
52
  import * as path8 from "path";
57
- import { execFileSync as execFileSync3 } from "child_process";
53
+ import { execFileSync } from "child_process";
58
54
  import chalk from "chalk";
59
55
  import ora from "ora";
60
56
  import Table from "cli-table3";
@@ -70,26 +66,27 @@ import * as path11 from "path";
70
66
  import * as fs13 from "fs";
71
67
  import * as path12 from "path";
72
68
  import { spawn } from "child_process";
69
+ import { createHash as createHash3 } from "crypto";
73
70
  import { spawn as spawn2 } from "child_process";
74
71
  import "reflect-metadata";
75
72
  import { Command as Command2 } from "commander";
76
- import { existsSync as existsSync25, readFileSync as readFileSync15 } from "fs";
73
+ import { existsSync as existsSync26, readFileSync as readFileSync16 } from "fs";
77
74
  import { fileURLToPath as fileURLToPath4 } from "url";
78
75
  import { dirname as dirname8, join as join30 } from "path";
79
76
  import fs14 from "fs";
80
77
  import path13 from "path";
81
- import { execSync as execSync4 } from "child_process";
78
+ import { execSync as execSync3 } from "child_process";
82
79
  import { fileURLToPath as fileURLToPath2 } from "url";
83
80
  import { dirname as dirname4, join as join13 } from "path";
84
81
  import * as readline from "readline";
85
82
  import * as fs15 from "fs";
86
83
  import * as path14 from "path";
87
- import { execFileSync as execFileSync4 } from "child_process";
84
+ import { execFileSync as execFileSync2 } from "child_process";
88
85
  import * as path15 from "path";
89
86
  import * as path16 from "path";
90
87
  import * as fs16 from "fs";
91
88
  import * as path17 from "path";
92
- import { execSync as execSync5 } from "child_process";
89
+ import { execSync as execSync4 } from "child_process";
93
90
  import * as path18 from "path";
94
91
  import * as fs17 from "fs";
95
92
  import * as path19 from "path";
@@ -130,7 +127,7 @@ import { Router as Router3 } from "express";
130
127
  import { Router as Router4 } from "express";
131
128
  import * as fs25 from "fs";
132
129
  import * as path25 from "path";
133
- import { execSync as execSync6 } from "child_process";
130
+ import { execSync as execSync5 } from "child_process";
134
131
  import { Router as Router5 } from "express";
135
132
  import * as path26 from "path";
136
133
  import { Router as Router6 } from "express";
@@ -146,16 +143,17 @@ import * as fs29 from "fs";
146
143
  import * as path30 from "path";
147
144
  import chalk3 from "chalk";
148
145
  import chalk4 from "chalk";
149
- import { execSync as execSync7 } from "child_process";
146
+ import { execSync as execSync6 } from "child_process";
150
147
  import * as fs30 from "fs";
151
148
  import * as readline3 from "readline";
149
+ import * as fs31 from "fs";
152
150
  import * as path31 from "path";
153
151
  import * as os5 from "os";
154
152
  import * as path32 from "path";
155
153
  import chalk5 from "chalk";
156
154
  import { Command } from "commander";
157
- import { execFileSync as execFileSync5 } from "child_process";
158
- import * as fs31 from "fs";
155
+ import { execFileSync as execFileSync3 } from "child_process";
156
+ import * as fs32 from "fs";
159
157
  import * as path33 from "path";
160
158
  import * as readline4 from "readline";
161
159
  import chalk6 from "chalk";
@@ -181,11 +179,14 @@ var DEFAULT_CRON_SCHEDULE;
181
179
  var DEFAULT_REVIEWER_SCHEDULE;
182
180
  var DEFAULT_CRON_SCHEDULE_OFFSET;
183
181
  var DEFAULT_MAX_RETRIES;
182
+ var DEFAULT_REVIEWER_MAX_RETRIES;
183
+ var DEFAULT_REVIEWER_RETRY_DELAY;
184
184
  var DEFAULT_BRANCH_PREFIX;
185
185
  var DEFAULT_BRANCH_PATTERNS;
186
186
  var DEFAULT_MIN_REVIEW_SCORE;
187
187
  var DEFAULT_MAX_LOG_SIZE;
188
188
  var DEFAULT_PROVIDER;
189
+ var DEFAULT_EXECUTOR_ENABLED;
189
190
  var DEFAULT_REVIEWER_ENABLED;
190
191
  var DEFAULT_PROVIDER_ENV;
191
192
  var DEFAULT_FALLBACK_ON_RATE_LIMIT;
@@ -216,6 +217,7 @@ var DEFAULT_AUDIT_SCHEDULE;
216
217
  var DEFAULT_AUDIT_MAX_RUNTIME;
217
218
  var DEFAULT_AUDIT;
218
219
  var AUDIT_LOG_NAME;
220
+ var PLANNER_LOG_NAME;
219
221
  var VALID_PROVIDERS;
220
222
  var VALID_JOB_TYPES;
221
223
  var DEFAULT_JOB_PROVIDERS;
@@ -239,18 +241,21 @@ var init_constants = __esm({
239
241
  "../core/dist/constants.js"() {
240
242
  "use strict";
241
243
  DEFAULT_DEFAULT_BRANCH = "";
242
- DEFAULT_PRD_DIR = "docs/PRDs/night-watch";
244
+ DEFAULT_PRD_DIR = "docs/prds";
243
245
  DEFAULT_MAX_RUNTIME = 7200;
244
246
  DEFAULT_REVIEWER_MAX_RUNTIME = 3600;
245
247
  DEFAULT_CRON_SCHEDULE = "0 0-21 * * *";
246
248
  DEFAULT_REVIEWER_SCHEDULE = "0 0,3,6,9,12,15,18,21 * * *";
247
249
  DEFAULT_CRON_SCHEDULE_OFFSET = 0;
248
250
  DEFAULT_MAX_RETRIES = 3;
251
+ DEFAULT_REVIEWER_MAX_RETRIES = 2;
252
+ DEFAULT_REVIEWER_RETRY_DELAY = 30;
249
253
  DEFAULT_BRANCH_PREFIX = "night-watch";
250
254
  DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
251
255
  DEFAULT_MIN_REVIEW_SCORE = 80;
252
256
  DEFAULT_MAX_LOG_SIZE = 524288;
253
257
  DEFAULT_PROVIDER = "claude";
258
+ DEFAULT_EXECUTOR_ENABLED = true;
254
259
  DEFAULT_REVIEWER_ENABLED = true;
255
260
  DEFAULT_PROVIDER_ENV = {};
256
261
  DEFAULT_FALLBACK_ON_RATE_LIMIT = false;
@@ -265,7 +270,7 @@ var init_constants = __esm({
265
270
  DEFAULT_SLICER_SCHEDULE = "0 */6 * * *";
266
271
  DEFAULT_SLICER_MAX_RUNTIME = 600;
267
272
  DEFAULT_ROADMAP_SCANNER = {
268
- enabled: false,
273
+ enabled: true,
269
274
  roadmapPath: "ROADMAP.md",
270
275
  autoScanInterval: 300,
271
276
  slicerSchedule: DEFAULT_SLICER_SCHEDULE,
@@ -305,6 +310,7 @@ var init_constants = __esm({
305
310
  maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
306
311
  };
307
312
  AUDIT_LOG_NAME = "audit";
313
+ PLANNER_LOG_NAME = "slicer";
308
314
  VALID_PROVIDERS = ["claude", "codex"];
309
315
  VALID_JOB_TYPES = ["executor", "reviewer", "qa", "audit", "slicer"];
310
316
  DEFAULT_JOB_PROVIDERS = {};
@@ -323,7 +329,9 @@ var init_constants = __esm({
323
329
  LOG_FILE_NAMES = {
324
330
  executor: EXECUTOR_LOG_NAME,
325
331
  reviewer: REVIEWER_LOG_NAME,
326
- qa: QA_LOG_NAME
332
+ qa: QA_LOG_NAME,
333
+ audit: AUDIT_LOG_NAME,
334
+ planner: PLANNER_LOG_NAME
327
335
  };
328
336
  GLOBAL_CONFIG_DIR = ".night-watch";
329
337
  REGISTRY_FILE_NAME = "projects.json";
@@ -349,8 +357,12 @@ function getDefaultConfig() {
349
357
  reviewerSchedule: DEFAULT_REVIEWER_SCHEDULE,
350
358
  cronScheduleOffset: DEFAULT_CRON_SCHEDULE_OFFSET,
351
359
  maxRetries: DEFAULT_MAX_RETRIES,
360
+ // Reviewer retry configuration
361
+ reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
362
+ reviewerRetryDelay: DEFAULT_REVIEWER_RETRY_DELAY,
352
363
  // Provider configuration
353
364
  provider: DEFAULT_PROVIDER,
365
+ executorEnabled: DEFAULT_EXECUTOR_ENABLED,
354
366
  reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
355
367
  providerEnv: { ...DEFAULT_PROVIDER_ENV },
356
368
  // Notification configuration
@@ -412,7 +424,10 @@ function normalizeConfig(rawConfig) {
412
424
  normalized.reviewerSchedule = readString(rawConfig.reviewerSchedule) ?? readString(cron?.reviewerSchedule);
413
425
  normalized.cronScheduleOffset = readNumber(rawConfig.cronScheduleOffset);
414
426
  normalized.maxRetries = readNumber(rawConfig.maxRetries);
427
+ normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
428
+ normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
415
429
  normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
430
+ normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
416
431
  normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
417
432
  const rawProviderEnv = readObject(rawConfig.providerEnv);
418
433
  if (rawProviderEnv) {
@@ -552,6 +567,28 @@ function sanitizeMaxRetries(value, fallback) {
552
567
  const normalized = Math.floor(value);
553
568
  return normalized >= 1 ? normalized : fallback;
554
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
+ }
555
592
  function mergeConfigs(base, fileConfig, envConfig) {
556
593
  const merged = { ...base };
557
594
  if (fileConfig) {
@@ -579,8 +616,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
579
616
  merged.cronScheduleOffset = fileConfig.cronScheduleOffset;
580
617
  if (fileConfig.maxRetries !== void 0)
581
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;
582
623
  if (fileConfig.provider !== void 0)
583
624
  merged.provider = fileConfig.provider;
625
+ if (fileConfig.executorEnabled !== void 0)
626
+ merged.executorEnabled = fileConfig.executorEnabled;
584
627
  if (fileConfig.reviewerEnabled !== void 0)
585
628
  merged.reviewerEnabled = fileConfig.reviewerEnabled;
586
629
  if (fileConfig.providerEnv !== void 0)
@@ -605,6 +648,8 @@ function mergeConfigs(base, fileConfig, envConfig) {
605
648
  merged.claudeModel = fileConfig.claudeModel;
606
649
  if (fileConfig.qa !== void 0)
607
650
  merged.qa = { ...merged.qa, ...fileConfig.qa };
651
+ if (fileConfig.audit !== void 0)
652
+ merged.audit = { ...merged.audit, ...fileConfig.audit };
608
653
  if (fileConfig.jobProviders !== void 0)
609
654
  merged.jobProviders = { ...fileConfig.jobProviders };
610
655
  }
@@ -632,8 +677,14 @@ function mergeConfigs(base, fileConfig, envConfig) {
632
677
  merged.cronScheduleOffset = envConfig.cronScheduleOffset;
633
678
  if (envConfig.maxRetries !== void 0)
634
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;
635
684
  if (envConfig.provider !== void 0)
636
685
  merged.provider = envConfig.provider;
686
+ if (envConfig.executorEnabled !== void 0)
687
+ merged.executorEnabled = envConfig.executorEnabled;
637
688
  if (envConfig.reviewerEnabled !== void 0)
638
689
  merged.reviewerEnabled = envConfig.reviewerEnabled;
639
690
  if (envConfig.providerEnv !== void 0)
@@ -658,9 +709,13 @@ function mergeConfigs(base, fileConfig, envConfig) {
658
709
  merged.claudeModel = envConfig.claudeModel;
659
710
  if (envConfig.qa !== void 0)
660
711
  merged.qa = { ...merged.qa, ...envConfig.qa };
712
+ if (envConfig.audit !== void 0)
713
+ merged.audit = { ...merged.audit, ...envConfig.audit };
661
714
  if (envConfig.jobProviders !== void 0)
662
715
  merged.jobProviders = { ...envConfig.jobProviders };
663
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);
664
719
  return merged;
665
720
  }
666
721
  function loadConfig(projectDir) {
@@ -726,6 +781,18 @@ function loadConfig(projectDir) {
726
781
  envConfig.maxRetries = retries;
727
782
  }
728
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
+ }
729
796
  if (process.env.NW_PROVIDER) {
730
797
  const provider = validateProvider(process.env.NW_PROVIDER);
731
798
  if (provider !== null) {
@@ -738,6 +805,12 @@ function loadConfig(projectDir) {
738
805
  envConfig.reviewerEnabled = reviewerEnabled;
739
806
  }
740
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
+ }
741
814
  if (process.env.NW_NOTIFICATIONS) {
742
815
  try {
743
816
  const parsed = JSON.parse(process.env.NW_NOTIFICATIONS);
@@ -2361,7 +2434,7 @@ var init_container = __esm({
2361
2434
  DATABASE_TOKEN = "Database";
2362
2435
  }
2363
2436
  });
2364
- function graphql(query, variables, cwd) {
2437
+ async function graphql(query, variables, cwd) {
2365
2438
  const args = ["api", "graphql", "-f", `query=${query}`];
2366
2439
  for (const [key, value] of Object.entries(variables)) {
2367
2440
  if (typeof value === "number") {
@@ -2370,10 +2443,9 @@ function graphql(query, variables, cwd) {
2370
2443
  args.push("-f", `${key}=${String(value)}`);
2371
2444
  }
2372
2445
  }
2373
- const output = execFileSync("gh", args, {
2446
+ const { stdout: output } = await execFileAsync("gh", args, {
2374
2447
  cwd,
2375
- encoding: "utf-8",
2376
- stdio: ["pipe", "pipe", "pipe"]
2448
+ encoding: "utf-8"
2377
2449
  });
2378
2450
  const parsed = JSON.parse(output);
2379
2451
  if (parsed.errors?.length) {
@@ -2381,25 +2453,29 @@ function graphql(query, variables, cwd) {
2381
2453
  }
2382
2454
  return parsed.data;
2383
2455
  }
2384
- function getRepoNwo(cwd) {
2385
- 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" });
2386
2458
  return output.trim();
2387
2459
  }
2388
- function getViewerLogin(cwd) {
2389
- const result = graphql(`query { viewer { login } }`, {}, cwd);
2460
+ async function getViewerLogin(cwd) {
2461
+ const result = await graphql(`query { viewer { login } }`, {}, cwd);
2390
2462
  return result.viewer.login;
2391
2463
  }
2464
+ var execFileAsync;
2392
2465
  var init_github_graphql = __esm({
2393
2466
  "../core/dist/board/providers/github-graphql.js"() {
2394
2467
  "use strict";
2468
+ execFileAsync = promisify(execFile);
2395
2469
  }
2396
2470
  });
2471
+ var execFileAsync2;
2397
2472
  var GitHubProjectsProvider;
2398
2473
  var init_github_projects = __esm({
2399
2474
  "../core/dist/board/providers/github-projects.js"() {
2400
2475
  "use strict";
2401
2476
  init_types2();
2402
2477
  init_github_graphql();
2478
+ execFileAsync2 = promisify2(execFile2);
2403
2479
  GitHubProjectsProvider = class {
2404
2480
  config;
2405
2481
  cwd;
@@ -2415,26 +2491,26 @@ var init_github_projects = __esm({
2415
2491
  // -------------------------------------------------------------------------
2416
2492
  // Helpers
2417
2493
  // -------------------------------------------------------------------------
2418
- getRepo() {
2494
+ async getRepo() {
2419
2495
  return this.config.repo ?? getRepoNwo(this.cwd);
2420
2496
  }
2421
- getRepoParts() {
2422
- const repo = this.getRepo();
2497
+ async getRepoParts() {
2498
+ const repo = await this.getRepo();
2423
2499
  const [owner, name] = repo.split("/");
2424
2500
  if (!owner || !name) {
2425
2501
  throw new Error(`Invalid repository slug: "${repo}". Expected "owner/repo".`);
2426
2502
  }
2427
2503
  return { owner, name };
2428
2504
  }
2429
- getRepoOwnerLogin() {
2430
- return this.getRepoParts().owner;
2505
+ async getRepoOwnerLogin() {
2506
+ return (await this.getRepoParts()).owner;
2431
2507
  }
2432
- getRepoOwner() {
2508
+ async getRepoOwner() {
2433
2509
  if (this.cachedOwner && this.cachedRepositoryId) {
2434
2510
  return this.cachedOwner;
2435
2511
  }
2436
- const { owner, name } = this.getRepoParts();
2437
- 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!) {
2438
2514
  repository(owner: $owner, name: $name) {
2439
2515
  id
2440
2516
  owner {
@@ -2459,20 +2535,20 @@ var init_github_projects = __esm({
2459
2535
  };
2460
2536
  return this.cachedOwner;
2461
2537
  }
2462
- getRepositoryNodeId() {
2538
+ async getRepositoryNodeId() {
2463
2539
  if (this.cachedRepositoryId) {
2464
2540
  return this.cachedRepositoryId;
2465
2541
  }
2466
- this.getRepoOwner();
2542
+ await this.getRepoOwner();
2467
2543
  if (!this.cachedRepositoryId) {
2468
- throw new Error(`Failed to resolve repository ID for ${this.getRepo()}.`);
2544
+ throw new Error(`Failed to resolve repository ID for ${await this.getRepo()}.`);
2469
2545
  }
2470
2546
  return this.cachedRepositoryId;
2471
2547
  }
2472
- linkProjectToRepository(projectId) {
2473
- const repositoryId = this.getRepositoryNodeId();
2548
+ async linkProjectToRepository(projectId) {
2549
+ const repositoryId = await this.getRepositoryNodeId();
2474
2550
  try {
2475
- graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
2551
+ await graphql(`mutation LinkProjectToRepository($projectId: ID!, $repositoryId: ID!) {
2476
2552
  linkProjectV2ToRepository(input: { projectId: $projectId, repositoryId: $repositoryId }) {
2477
2553
  repository {
2478
2554
  id
@@ -2488,8 +2564,8 @@ var init_github_projects = __esm({
2488
2564
  throw err;
2489
2565
  }
2490
2566
  }
2491
- fetchStatusField(projectId) {
2492
- const fieldData = graphql(`query GetStatusField($projectId: ID!) {
2567
+ async fetchStatusField(projectId) {
2568
+ const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
2493
2569
  node(id: $projectId) {
2494
2570
  ... on ProjectV2 {
2495
2571
  field(name: "Status") {
@@ -2526,7 +2602,7 @@ var init_github_projects = __esm({
2526
2602
  };
2527
2603
  }
2528
2604
  if (this.cachedProjectId !== null) {
2529
- const statusField2 = this.fetchStatusField(this.cachedProjectId);
2605
+ const statusField2 = await this.fetchStatusField(this.cachedProjectId);
2530
2606
  this.cachedFieldId = statusField2.fieldId;
2531
2607
  this.cachedOptionIds = statusField2.optionIds;
2532
2608
  return {
@@ -2539,23 +2615,23 @@ var init_github_projects = __esm({
2539
2615
  if (!projectNumber) {
2540
2616
  throw new Error("No projectNumber configured. Run `night-watch board setup` first.");
2541
2617
  }
2542
- const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
2618
+ const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
2543
2619
  try {
2544
- ownerLogins.add(getViewerLogin(this.cwd));
2620
+ ownerLogins.add(await getViewerLogin(this.cwd));
2545
2621
  } catch {
2546
2622
  }
2547
2623
  let projectNode = null;
2548
2624
  for (const login of ownerLogins) {
2549
- projectNode = this.fetchProjectNode(login, projectNumber);
2625
+ projectNode = await this.fetchProjectNode(login, projectNumber);
2550
2626
  if (projectNode) {
2551
2627
  break;
2552
2628
  }
2553
2629
  }
2554
2630
  if (!projectNode) {
2555
- 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()}".`);
2556
2632
  }
2557
2633
  this.cachedProjectId = projectNode.id;
2558
- const statusField = this.fetchStatusField(projectNode.id);
2634
+ const statusField = await this.fetchStatusField(projectNode.id);
2559
2635
  this.cachedFieldId = statusField.fieldId;
2560
2636
  this.cachedOptionIds = statusField.optionIds;
2561
2637
  return {
@@ -2565,9 +2641,9 @@ var init_github_projects = __esm({
2565
2641
  };
2566
2642
  }
2567
2643
  /** Try user query first, fall back to org query. */
2568
- fetchProjectNode(login, projectNumber) {
2644
+ async fetchProjectNode(login, projectNumber) {
2569
2645
  try {
2570
- const userData = graphql(`query GetProject($login: String!, $number: Int!) {
2646
+ const userData = await graphql(`query GetProject($login: String!, $number: Int!) {
2571
2647
  user(login: $login) {
2572
2648
  projectV2(number: $number) {
2573
2649
  id
@@ -2583,7 +2659,7 @@ var init_github_projects = __esm({
2583
2659
  } catch {
2584
2660
  }
2585
2661
  try {
2586
- const orgData = graphql(`query GetOrgProject($login: String!, $number: Int!) {
2662
+ const orgData = await graphql(`query GetOrgProject($login: String!, $number: Int!) {
2587
2663
  organization(login: $login) {
2588
2664
  projectV2(number: $number) {
2589
2665
  id
@@ -2629,6 +2705,117 @@ var init_github_projects = __esm({
2629
2705
  assignees: content.assignees?.nodes.map((a) => a.login) ?? []
2630
2706
  };
2631
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
+ }
2632
2819
  // -------------------------------------------------------------------------
2633
2820
  // IBoardProvider implementation
2634
2821
  // -------------------------------------------------------------------------
@@ -2636,10 +2823,10 @@ var init_github_projects = __esm({
2636
2823
  * Find an existing project by title among the repository owner's first 50 projects.
2637
2824
  * Returns null if not found.
2638
2825
  */
2639
- findExistingProject(owner, title) {
2826
+ async findExistingProject(owner, title) {
2640
2827
  try {
2641
2828
  if (owner.type === "User") {
2642
- const data2 = graphql(`query ListUserProjects($login: String!) {
2829
+ const data2 = await graphql(`query ListUserProjects($login: String!) {
2643
2830
  user(login: $login) {
2644
2831
  projectsV2(first: 50) {
2645
2832
  nodes { id number title url }
@@ -2648,7 +2835,7 @@ var init_github_projects = __esm({
2648
2835
  }`, { login: owner.login }, this.cwd);
2649
2836
  return data2.user?.projectsV2.nodes.find((p) => p.title === title) ?? null;
2650
2837
  }
2651
- const data = graphql(`query ListOrgProjects($login: String!) {
2838
+ const data = await graphql(`query ListOrgProjects($login: String!) {
2652
2839
  organization(login: $login) {
2653
2840
  projectsV2(first: 50) {
2654
2841
  nodes { id number title url }
@@ -2664,8 +2851,8 @@ var init_github_projects = __esm({
2664
2851
  * Ensure the Status field on an existing project has all five Night Watch
2665
2852
  * lifecycle columns, updating it via GraphQL if any are missing.
2666
2853
  */
2667
- ensureStatusColumns(projectId) {
2668
- const fieldData = graphql(`query GetStatusField($projectId: ID!) {
2854
+ async ensureStatusColumns(projectId) {
2855
+ const fieldData = await graphql(`query GetStatusField($projectId: ID!) {
2669
2856
  node(id: $projectId) {
2670
2857
  ... on ProjectV2 {
2671
2858
  field(name: "Status") {
@@ -2697,7 +2884,7 @@ var init_github_projects = __esm({
2697
2884
  color: colorMap[name],
2698
2885
  description: ""
2699
2886
  }));
2700
- graphql(`mutation UpdateField($fieldId: ID!) {
2887
+ await graphql(`mutation UpdateField($fieldId: ID!) {
2701
2888
  updateProjectV2Field(input: {
2702
2889
  fieldId: $fieldId,
2703
2890
  singleSelectOptions: [
@@ -2718,15 +2905,15 @@ var init_github_projects = __esm({
2718
2905
  }`, { fieldId: field.id, allOptions }, this.cwd);
2719
2906
  }
2720
2907
  async setupBoard(title) {
2721
- const owner = this.getRepoOwner();
2722
- const existing = this.findExistingProject(owner, title);
2908
+ const owner = await this.getRepoOwner();
2909
+ const existing = await this.findExistingProject(owner, title);
2723
2910
  if (existing) {
2724
2911
  this.cachedProjectId = existing.id;
2725
- this.linkProjectToRepository(existing.id);
2726
- this.ensureStatusColumns(existing.id);
2912
+ await this.linkProjectToRepository(existing.id);
2913
+ await this.ensureStatusColumns(existing.id);
2727
2914
  return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
2728
2915
  }
2729
- const createData = graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
2916
+ const createData = await graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
2730
2917
  createProjectV2(input: { ownerId: $ownerId, title: $title }) {
2731
2918
  projectV2 {
2732
2919
  id
@@ -2738,13 +2925,13 @@ var init_github_projects = __esm({
2738
2925
  }`, { ownerId: owner.id, title }, this.cwd);
2739
2926
  const project = createData.createProjectV2.projectV2;
2740
2927
  this.cachedProjectId = project.id;
2741
- this.linkProjectToRepository(project.id);
2928
+ await this.linkProjectToRepository(project.id);
2742
2929
  try {
2743
- const statusField = this.fetchStatusField(project.id);
2930
+ const statusField = await this.fetchStatusField(project.id);
2744
2931
  this.cachedFieldId = statusField.fieldId;
2745
2932
  this.cachedOptionIds = statusField.optionIds;
2746
- this.ensureStatusColumns(project.id);
2747
- const refreshed = this.fetchStatusField(project.id);
2933
+ await this.ensureStatusColumns(project.id);
2934
+ const refreshed = await this.fetchStatusField(project.id);
2748
2935
  this.cachedFieldId = refreshed.fieldId;
2749
2936
  this.cachedOptionIds = refreshed.optionIds;
2750
2937
  } catch (err) {
@@ -2752,7 +2939,7 @@ var init_github_projects = __esm({
2752
2939
  if (!message.includes("Status field not found")) {
2753
2940
  throw err;
2754
2941
  }
2755
- const createFieldData = graphql(`mutation CreateStatusField($projectId: ID!) {
2942
+ const createFieldData = await graphql(`mutation CreateStatusField($projectId: ID!) {
2756
2943
  createProjectV2Field(input: {
2757
2944
  projectId: $projectId,
2758
2945
  dataType: SINGLE_SELECT,
@@ -2785,14 +2972,14 @@ var init_github_projects = __esm({
2785
2972
  return null;
2786
2973
  }
2787
2974
  try {
2788
- const ownerLogins = /* @__PURE__ */ new Set([this.getRepoOwnerLogin()]);
2975
+ const ownerLogins = /* @__PURE__ */ new Set([await this.getRepoOwnerLogin()]);
2789
2976
  try {
2790
- ownerLogins.add(getViewerLogin(this.cwd));
2977
+ ownerLogins.add(await getViewerLogin(this.cwd));
2791
2978
  } catch {
2792
2979
  }
2793
2980
  let node = null;
2794
2981
  for (const login of ownerLogins) {
2795
- node = this.fetchProjectNode(login, projectNumber);
2982
+ node = await this.fetchProjectNode(login, projectNumber);
2796
2983
  if (node) {
2797
2984
  break;
2798
2985
  }
@@ -2813,7 +3000,7 @@ var init_github_projects = __esm({
2813
3000
  }));
2814
3001
  }
2815
3002
  async createIssue(input) {
2816
- const repo = this.getRepo();
3003
+ const repo = await this.getRepo();
2817
3004
  const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2818
3005
  const issueArgs = [
2819
3006
  "issue",
@@ -2828,19 +3015,20 @@ var init_github_projects = __esm({
2828
3015
  if (input.labels && input.labels.length > 0) {
2829
3016
  issueArgs.push("--label", input.labels.join(","));
2830
3017
  }
2831
- const issueUrl = execFileSync2("gh", issueArgs, {
3018
+ const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
2832
3019
  cwd: this.cwd,
2833
- encoding: "utf-8",
2834
- stdio: ["pipe", "pipe", "pipe"]
2835
- }).trim();
3020
+ encoding: "utf-8"
3021
+ });
3022
+ const issueUrl = issueUrlRaw.trim();
2836
3023
  const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
2837
3024
  if (!issueNumber) {
2838
3025
  throw new Error(`Failed to parse issue number from URL: ${issueUrl}`);
2839
3026
  }
2840
3027
  const [owner, repoName] = repo.split("/");
2841
- 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();
2842
3030
  const issueJson = { number: issueNumber, id: nodeIdOutput, url: issueUrl };
2843
- const addData = graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
3031
+ const addData = await graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2844
3032
  addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2845
3033
  item {
2846
3034
  id
@@ -2851,7 +3039,7 @@ var init_github_projects = __esm({
2851
3039
  const targetColumn = input.column ?? "Draft";
2852
3040
  const optionId = optionIds.get(targetColumn);
2853
3041
  if (optionId) {
2854
- graphql(`mutation UpdateItemField(
3042
+ await graphql(`mutation UpdateItemField(
2855
3043
  $projectId: ID!,
2856
3044
  $itemId: ID!,
2857
3045
  $fieldId: ID!,
@@ -2885,10 +3073,10 @@ var init_github_projects = __esm({
2885
3073
  };
2886
3074
  }
2887
3075
  async getIssue(issueNumber) {
2888
- const repo = this.getRepo();
3076
+ const repo = await this.getRepo();
2889
3077
  let rawIssue;
2890
3078
  try {
2891
- const output = execFileSync2("gh", [
3079
+ const { stdout: output } = await execFileAsync2("gh", [
2892
3080
  "issue",
2893
3081
  "view",
2894
3082
  String(issueNumber),
@@ -2896,7 +3084,7 @@ var init_github_projects = __esm({
2896
3084
  repo,
2897
3085
  "--json",
2898
3086
  "number,title,body,url,id,labels,assignees"
2899
- ], { cwd: this.cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
3087
+ ], { cwd: this.cwd, encoding: "utf-8" });
2900
3088
  rawIssue = JSON.parse(output);
2901
3089
  } catch {
2902
3090
  return null;
@@ -2927,42 +3115,9 @@ var init_github_projects = __esm({
2927
3115
  }
2928
3116
  async getAllIssues() {
2929
3117
  const { projectId } = await this.ensureProjectCache();
2930
- const data = graphql(`query GetProjectItems($projectId: ID!) {
2931
- node(id: $projectId) {
2932
- ... on ProjectV2 {
2933
- items(first: 100) {
2934
- nodes {
2935
- id
2936
- content {
2937
- ... on Issue {
2938
- number
2939
- title
2940
- body
2941
- url
2942
- id
2943
- labels(first: 10) { nodes { name } }
2944
- assignees(first: 10) { nodes { login } }
2945
- }
2946
- }
2947
- fieldValues(first: 10) {
2948
- nodes {
2949
- ... on ProjectV2ItemFieldSingleSelectValue {
2950
- name
2951
- field {
2952
- ... on ProjectV2SingleSelectField {
2953
- name
2954
- }
2955
- }
2956
- }
2957
- }
2958
- }
2959
- }
2960
- }
2961
- }
2962
- }
2963
- }`, { projectId }, this.cwd);
3118
+ const allNodes = await this.fetchAllProjectItems(projectId);
2964
3119
  const results = [];
2965
- for (const item of data.node.items.nodes) {
3120
+ for (const item of allNodes) {
2966
3121
  const parsed = this.parseItem(item);
2967
3122
  if (parsed) {
2968
3123
  results.push(parsed);
@@ -2972,35 +3127,8 @@ var init_github_projects = __esm({
2972
3127
  }
2973
3128
  async moveIssue(issueNumber, targetColumn) {
2974
3129
  const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2975
- const data = graphql(`query GetProjectItems($projectId: ID!) {
2976
- node(id: $projectId) {
2977
- ... on ProjectV2 {
2978
- items(first: 100) {
2979
- nodes {
2980
- id
2981
- content {
2982
- ... on Issue {
2983
- number
2984
- }
2985
- }
2986
- fieldValues(first: 10) {
2987
- nodes {
2988
- ... on ProjectV2ItemFieldSingleSelectValue {
2989
- name
2990
- field {
2991
- ... on ProjectV2SingleSelectField {
2992
- name
2993
- }
2994
- }
2995
- }
2996
- }
2997
- }
2998
- }
2999
- }
3000
- }
3001
- }
3002
- }`, { projectId }, this.cwd);
3003
- 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);
3004
3132
  if (!itemNode) {
3005
3133
  throw new Error(`Issue #${issueNumber} not found on the project board.`);
3006
3134
  }
@@ -3008,7 +3136,7 @@ var init_github_projects = __esm({
3008
3136
  if (!optionId) {
3009
3137
  throw new Error(`Column "${targetColumn}" not found on the project board.`);
3010
3138
  }
3011
- graphql(`mutation UpdateItemField(
3139
+ await graphql(`mutation UpdateItemField(
3012
3140
  $projectId: ID!,
3013
3141
  $itemId: ID!,
3014
3142
  $fieldId: ID!,
@@ -3027,12 +3155,12 @@ var init_github_projects = __esm({
3027
3155
  }`, { projectId, itemId: itemNode.id, fieldId, optionId }, this.cwd);
3028
3156
  }
3029
3157
  async closeIssue(issueNumber) {
3030
- const repo = this.getRepo();
3031
- 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" });
3032
3160
  }
3033
3161
  async commentOnIssue(issueNumber, body) {
3034
- const repo = this.getRepo();
3035
- 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" });
3036
3164
  }
3037
3165
  };
3038
3166
  }
@@ -3493,7 +3621,7 @@ function collectPrdDirs(projectPaths) {
3493
3621
  const prdDirs = [];
3494
3622
  for (const projectPath of projectPaths) {
3495
3623
  const configPath = path3.join(projectPath, CONFIG_FILE_NAME);
3496
- let prdDir = "docs/PRDs/night-watch";
3624
+ let prdDir = "docs/prds";
3497
3625
  if (fs3.existsSync(configPath)) {
3498
3626
  try {
3499
3627
  const config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
@@ -4123,9 +4251,15 @@ function executorLockPath(projectDir) {
4123
4251
  function reviewerLockPath(projectDir) {
4124
4252
  return `${LOCK_FILE_PREFIX}pr-reviewer-${projectRuntimeKey(projectDir)}.lock`;
4125
4253
  }
4254
+ function qaLockPath(projectDir) {
4255
+ return `${LOCK_FILE_PREFIX}qa-${projectRuntimeKey(projectDir)}.lock`;
4256
+ }
4126
4257
  function auditLockPath(projectDir) {
4127
4258
  return `${LOCK_FILE_PREFIX}audit-${projectRuntimeKey(projectDir)}.lock`;
4128
4259
  }
4260
+ function plannerLockPath(projectDir) {
4261
+ return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
4262
+ }
4129
4263
  function isProcessRunning(pid) {
4130
4264
  try {
4131
4265
  process.kill(pid, 0);
@@ -4302,24 +4436,26 @@ function collectPrdInfo(projectDir, prdDir, maxRuntime) {
4302
4436
  }
4303
4437
  return prds;
4304
4438
  }
4305
- function countOpenPRs(projectDir, branchPatterns) {
4439
+ async function countOpenPRs(projectDir, branchPatterns) {
4306
4440
  try {
4307
- execSync2("git rev-parse --git-dir", {
4441
+ await execAsync("git rev-parse --git-dir", {
4308
4442
  cwd: projectDir,
4309
- encoding: "utf-8",
4310
- stdio: ["pipe", "pipe", "pipe"]
4443
+ encoding: "utf-8"
4311
4444
  });
4312
4445
  try {
4313
- execSync2("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4446
+ await execAsync("which gh", { encoding: "utf-8" });
4314
4447
  } catch {
4315
4448
  return 0;
4316
4449
  }
4317
- 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", {
4318
4451
  cwd: projectDir,
4319
- encoding: "utf-8",
4320
- stdio: ["pipe", "pipe", "pipe"]
4452
+ encoding: "utf-8"
4321
4453
  });
4322
- const prs = JSON.parse(output);
4454
+ const trimmed = output.trim();
4455
+ if (!trimmed || trimmed === "[]") {
4456
+ return 0;
4457
+ }
4458
+ const prs = JSON.parse(trimmed);
4323
4459
  const matchingPRs = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
4324
4460
  return matchingPRs.length;
4325
4461
  } catch {
@@ -4385,27 +4521,29 @@ function deriveReviewScore(reviewDecision) {
4385
4521
  return null;
4386
4522
  }
4387
4523
  }
4388
- function collectPrInfo(projectDir, branchPatterns) {
4524
+ async function collectPrInfo(projectDir, branchPatterns) {
4389
4525
  try {
4390
- execSync2("git rev-parse --git-dir", {
4526
+ await execAsync("git rev-parse --git-dir", {
4391
4527
  cwd: projectDir,
4392
- encoding: "utf-8",
4393
- stdio: ["pipe", "pipe", "pipe"]
4528
+ encoding: "utf-8"
4394
4529
  });
4395
4530
  try {
4396
- execSync2("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
4531
+ await execAsync("which gh", { encoding: "utf-8" });
4397
4532
  } catch {
4398
4533
  return [];
4399
4534
  }
4400
- 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", {
4401
4536
  cwd: projectDir,
4402
- encoding: "utf-8",
4403
- stdio: ["pipe", "pipe", "pipe"]
4537
+ encoding: "utf-8"
4404
4538
  });
4405
4539
  if (process.env.DEBUG_PR_DATA === "1") {
4406
4540
  console.error("[DEBUG] Raw gh pr list output:", output);
4407
4541
  }
4408
- const prs = JSON.parse(output);
4542
+ const trimmed = output.trim();
4543
+ if (!trimmed || trimmed === "[]") {
4544
+ return [];
4545
+ }
4546
+ const prs = JSON.parse(trimmed);
4409
4547
  return prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern))).map((pr) => {
4410
4548
  if (process.env.DEBUG_PR_DATA === "1") {
4411
4549
  console.error(`[DEBUG] PR #${pr.number}:`);
@@ -4450,7 +4588,9 @@ function collectLogInfo(projectDir) {
4450
4588
  const logEntries = [
4451
4589
  { name: "executor", fileName: "executor.log" },
4452
4590
  { name: "reviewer", fileName: "reviewer.log" },
4453
- { 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` }
4454
4594
  ];
4455
4595
  return logEntries.map(({ name, fileName }) => {
4456
4596
  const logPath = path5.join(projectDir, LOG_DIR, fileName);
@@ -4472,16 +4612,22 @@ function getCrontabInfo(projectName, projectDir) {
4472
4612
  entries: crontabEntries
4473
4613
  };
4474
4614
  }
4475
- function fetchStatusSnapshot(projectDir, config) {
4615
+ async function fetchStatusSnapshot(projectDir, config) {
4476
4616
  const projectName = getProjectName(projectDir);
4477
4617
  const executorLock = checkLockFile(executorLockPath(projectDir));
4478
4618
  const reviewerLock = checkLockFile(reviewerLockPath(projectDir));
4619
+ const qaLock = checkLockFile(qaLockPath(projectDir));
4620
+ const auditLock = checkLockFile(auditLockPath(projectDir));
4621
+ const plannerLock = checkLockFile(plannerLockPath(projectDir));
4479
4622
  const processes = [
4480
4623
  { name: "executor", running: executorLock.running, pid: executorLock.pid },
4481
- { 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 }
4482
4628
  ];
4483
4629
  const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
4484
- const prs = collectPrInfo(projectDir, config.branchPatterns);
4630
+ const prs = await collectPrInfo(projectDir, config.branchPatterns);
4485
4631
  const logs = collectLogInfo(projectDir);
4486
4632
  const crontab = getCrontabInfo(projectName, projectDir);
4487
4633
  const activePrd = prds.find((p) => p.status === "in-progress")?.name ?? null;
@@ -4498,12 +4644,14 @@ function fetchStatusSnapshot(projectDir, config) {
4498
4644
  timestamp: /* @__PURE__ */ new Date()
4499
4645
  };
4500
4646
  }
4647
+ var execAsync;
4501
4648
  var init_status_data = __esm({
4502
4649
  "../core/dist/utils/status-data.js"() {
4503
4650
  "use strict";
4504
4651
  init_constants();
4505
4652
  init_prd_states();
4506
4653
  init_crontab();
4654
+ execAsync = promisify3(exec);
4507
4655
  }
4508
4656
  });
4509
4657
  function getLockFilePaths(projectDir) {
@@ -4622,7 +4770,7 @@ function checkGitRepo(cwd) {
4622
4770
  }
4623
4771
  function checkGhCli() {
4624
4772
  try {
4625
- execSync3("gh auth status", {
4773
+ execSync2("gh auth status", {
4626
4774
  encoding: "utf-8",
4627
4775
  stdio: ["pipe", "pipe", "pipe"]
4628
4776
  });
@@ -4641,7 +4789,7 @@ function checkGhCli() {
4641
4789
  }
4642
4790
  function checkProviderCli(provider) {
4643
4791
  try {
4644
- execSync3(`which ${provider}`, {
4792
+ execSync2(`which ${provider}`, {
4645
4793
  encoding: "utf-8",
4646
4794
  stdio: ["pipe", "pipe", "pipe"]
4647
4795
  });
@@ -4662,7 +4810,7 @@ function detectProviders() {
4662
4810
  const providers = [];
4663
4811
  for (const provider of VALID_PROVIDERS) {
4664
4812
  try {
4665
- execSync3(`which ${provider}`, {
4813
+ execSync2(`which ${provider}`, {
4666
4814
  encoding: "utf-8",
4667
4815
  stdio: ["pipe", "pipe", "pipe"]
4668
4816
  });
@@ -4756,7 +4904,7 @@ function checkLogsDirectory(projectDir) {
4756
4904
  }
4757
4905
  function checkCrontabAccess() {
4758
4906
  try {
4759
- execSync3("crontab -l", {
4907
+ execSync2("crontab -l", {
4760
4908
  encoding: "utf-8",
4761
4909
  stdio: ["pipe", "pipe", "pipe"]
4762
4910
  });
@@ -4906,7 +5054,7 @@ function parsePrDetails(raw) {
4906
5054
  }
4907
5055
  function fetchPrBySelector(selector, cwd) {
4908
5056
  try {
4909
- 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"] });
4910
5058
  return parsePrDetails(output);
4911
5059
  } catch {
4912
5060
  return null;
@@ -4920,7 +5068,7 @@ function fetchPrDetailsByNumber(prNumber, cwd) {
4920
5068
  }
4921
5069
  function fetchPrDetails(branchPrefix, cwd) {
4922
5070
  try {
4923
- 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"] });
4924
5072
  const prs = JSON.parse(listOutput);
4925
5073
  const matching = prs.filter((pr) => pr.headRefName.startsWith(branchPrefix + "/"));
4926
5074
  if (matching.length === 0) {
@@ -4934,7 +5082,7 @@ function fetchPrDetails(branchPrefix, cwd) {
4934
5082
  }
4935
5083
  function fetchReviewedPrDetails(branchPatterns, cwd) {
4936
5084
  try {
4937
- 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"] });
4938
5086
  const prs = JSON.parse(listOutput);
4939
5087
  const matching = prs.filter((pr) => branchPatterns.some((pattern) => pr.headRefName.startsWith(pattern)));
4940
5088
  if (matching.length === 0) {
@@ -5134,6 +5282,14 @@ function buildDescription(ctx) {
5134
5282
  if (ctx.duration !== void 0) {
5135
5283
  lines.push(`Duration: ${ctx.duration}s`);
5136
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
+ }
5137
5293
  return lines.join("\n");
5138
5294
  }
5139
5295
  function escapeMarkdownV2(text) {
@@ -5188,7 +5344,7 @@ function formatDiscordPayload(ctx) {
5188
5344
  }
5189
5345
  function formatTelegramPayload(ctx) {
5190
5346
  const emoji = getEventEmoji(ctx.event);
5191
- const title = getEventTitle(ctx.event);
5347
+ const title = ctx.event === "run_succeeded" ? "PR Opened" : getEventTitle(ctx.event);
5192
5348
  if (ctx.prUrl && ctx.prTitle) {
5193
5349
  const lines = [];
5194
5350
  lines.push(`*${escapeMarkdownV2(emoji + " " + title)}*`);
@@ -5215,6 +5371,14 @@ function formatTelegramPayload(ctx) {
5215
5371
  }
5216
5372
  lines.push(escapeMarkdownV2(stats.join(" | ")));
5217
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
+ }
5218
5382
  lines.push("");
5219
5383
  lines.push(escapeMarkdownV2(`\u2699\uFE0F Project: ${ctx.projectName} | Provider: ${ctx.provider}`));
5220
5384
  return {
@@ -5692,6 +5856,8 @@ The PRD directory is: \`{{PRD_DIR}}\`
5692
5856
 
5693
5857
  ## Your Task
5694
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
+
5695
5861
  1. **Explore the Codebase** - Read relevant existing files to understand the project structure, patterns, and conventions.
5696
5862
 
5697
5863
  2. **Assess Complexity** - Score the complexity using the rubric and determine whether this is LOW, MEDIUM, or HIGH complexity.
@@ -5751,6 +5917,116 @@ DO NOT forget to write the file.
5751
5917
  cachedTemplate = null;
5752
5918
  }
5753
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
+ }
5754
6030
  function getRoadmapStatus(projectDir, config) {
5755
6031
  const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
5756
6032
  const scannerEnabled = config.roadmapScanner.enabled;
@@ -5916,15 +6192,16 @@ async function sliceNextItem(projectDir, config) {
5916
6192
  };
5917
6193
  }
5918
6194
  const roadmapPath = path12.join(projectDir, config.roadmapScanner.roadmapPath);
5919
- if (!fs13.existsSync(roadmapPath)) {
6195
+ const auditItems = collectAuditPlannerItems(projectDir);
6196
+ const roadmapExists = fs13.existsSync(roadmapPath);
6197
+ if (!roadmapExists && auditItems.length === 0) {
5920
6198
  return {
5921
6199
  sliced: false,
5922
6200
  error: "ROADMAP.md not found"
5923
6201
  };
5924
6202
  }
5925
- const content = fs13.readFileSync(roadmapPath, "utf-8");
5926
- const items = parseRoadmap(content);
5927
- if (items.length === 0) {
6203
+ const roadmapItems = roadmapExists ? parseRoadmap(fs13.readFileSync(roadmapPath, "utf-8")) : [];
6204
+ if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
5928
6205
  return {
5929
6206
  sliced: false,
5930
6207
  error: "No items in roadmap"
@@ -5933,21 +6210,23 @@ async function sliceNextItem(projectDir, config) {
5933
6210
  const prdDir = path12.join(projectDir, config.prdDir);
5934
6211
  const state = loadRoadmapState(prdDir);
5935
6212
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5936
- let targetItem;
5937
- for (const item of items) {
5938
- if (item.checked) {
5939
- continue;
5940
- }
5941
- if (isItemProcessed(state, item.hash)) {
5942
- continue;
5943
- }
5944
- const itemSlug = slugify(item.title);
5945
- if (existingPrdSlugs.has(itemSlug)) {
5946
- 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;
5947
6226
  }
5948
- targetItem = item;
5949
- break;
5950
- }
6227
+ return void 0;
6228
+ };
6229
+ const targetItem = pickEligibleItem(auditItems) ?? pickEligibleItem(roadmapItems);
5951
6230
  if (!targetItem) {
5952
6231
  return {
5953
6232
  sliced: false,
@@ -6046,11 +6325,11 @@ var init_script_result = __esm({
6046
6325
  RESULT_PREFIX = "NIGHT_WATCH_RESULT:";
6047
6326
  }
6048
6327
  });
6049
- async function executeScript(scriptPath, args = [], env = {}) {
6050
- const result = await executeScriptWithOutput(scriptPath, args, env);
6328
+ async function executeScript(scriptPath, args = [], env = {}, options = {}) {
6329
+ const result = await executeScriptWithOutput(scriptPath, args, env, options);
6051
6330
  return result.exitCode;
6052
6331
  }
6053
- async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
6332
+ async function executeScriptWithOutput(scriptPath, args = [], env = {}, options = {}) {
6054
6333
  return new Promise((resolve9, reject) => {
6055
6334
  const childEnv = {
6056
6335
  ...process.env,
@@ -6060,6 +6339,7 @@ async function executeScriptWithOutput(scriptPath, args = [], env = {}) {
6060
6339
  const stderrChunks = [];
6061
6340
  const child = spawn2("bash", [scriptPath, ...args], {
6062
6341
  env: childEnv,
6342
+ cwd: options.cwd,
6063
6343
  stdio: ["inherit", "pipe", "pipe"]
6064
6344
  });
6065
6345
  child.stdout?.on("data", (data) => {
@@ -6338,6 +6618,7 @@ __export(dist_exports, {
6338
6618
  DEFAULT_CRON_SCHEDULE: () => DEFAULT_CRON_SCHEDULE,
6339
6619
  DEFAULT_CRON_SCHEDULE_OFFSET: () => DEFAULT_CRON_SCHEDULE_OFFSET,
6340
6620
  DEFAULT_DEFAULT_BRANCH: () => DEFAULT_DEFAULT_BRANCH,
6621
+ DEFAULT_EXECUTOR_ENABLED: () => DEFAULT_EXECUTOR_ENABLED,
6341
6622
  DEFAULT_FALLBACK_ON_RATE_LIMIT: () => DEFAULT_FALLBACK_ON_RATE_LIMIT,
6342
6623
  DEFAULT_JOB_PROVIDERS: () => DEFAULT_JOB_PROVIDERS,
6343
6624
  DEFAULT_LOCAL_BOARD_INFO: () => DEFAULT_LOCAL_BOARD_INFO,
@@ -6358,7 +6639,9 @@ __export(dist_exports, {
6358
6639
  DEFAULT_QA_SCHEDULE: () => DEFAULT_QA_SCHEDULE,
6359
6640
  DEFAULT_QA_SKIP_LABEL: () => DEFAULT_QA_SKIP_LABEL,
6360
6641
  DEFAULT_REVIEWER_ENABLED: () => DEFAULT_REVIEWER_ENABLED,
6642
+ DEFAULT_REVIEWER_MAX_RETRIES: () => DEFAULT_REVIEWER_MAX_RETRIES,
6361
6643
  DEFAULT_REVIEWER_MAX_RUNTIME: () => DEFAULT_REVIEWER_MAX_RUNTIME,
6644
+ DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
6362
6645
  DEFAULT_REVIEWER_SCHEDULE: () => DEFAULT_REVIEWER_SCHEDULE,
6363
6646
  DEFAULT_ROADMAP_SCANNER: () => DEFAULT_ROADMAP_SCANNER,
6364
6647
  DEFAULT_SLICER_MAX_RUNTIME: () => DEFAULT_SLICER_MAX_RUNTIME,
@@ -6377,6 +6660,7 @@ __export(dist_exports, {
6377
6660
  Logger: () => Logger,
6378
6661
  MAX_HISTORY_RECORDS_PER_PRD: () => MAX_HISTORY_RECORDS_PER_PRD,
6379
6662
  NIGHT_WATCH_LABELS: () => NIGHT_WATCH_LABELS,
6663
+ PLANNER_LOG_NAME: () => PLANNER_LOG_NAME,
6380
6664
  PRD_STATES_FILE_NAME: () => PRD_STATES_FILE_NAME,
6381
6665
  PRD_TEMPLATE: () => PRD_TEMPLATE,
6382
6666
  PRIORITY_LABELS: () => PRIORITY_LABELS,
@@ -6505,7 +6789,9 @@ __export(dist_exports, {
6505
6789
  parseRoadmap: () => parseRoadmap,
6506
6790
  parseScriptResult: () => parseScriptResult,
6507
6791
  performCancel: () => performCancel,
6792
+ plannerLockPath: () => plannerLockPath,
6508
6793
  projectRuntimeKey: () => projectRuntimeKey,
6794
+ qaLockPath: () => qaLockPath,
6509
6795
  readCrontab: () => readCrontab,
6510
6796
  readPrdStates: () => readPrdStates,
6511
6797
  recordExecution: () => recordExecution,
@@ -6620,7 +6906,7 @@ function detectPlaywright(cwd) {
6620
6906
  return true;
6621
6907
  }
6622
6908
  try {
6623
- execSync4("playwright --version", {
6909
+ execSync3("playwright --version", {
6624
6910
  cwd,
6625
6911
  encoding: "utf-8",
6626
6912
  stdio: ["pipe", "pipe", "pipe"],
@@ -6664,12 +6950,12 @@ function promptYesNo(question, defaultNo = true) {
6664
6950
  function installPlaywrightForQa(cwd) {
6665
6951
  try {
6666
6952
  const installCmd = resolvePlaywrightInstallCommand(cwd);
6667
- execSync4(installCmd, {
6953
+ execSync3(installCmd, {
6668
6954
  cwd,
6669
6955
  encoding: "utf-8",
6670
6956
  stdio: ["pipe", "pipe", "pipe"]
6671
6957
  });
6672
- execSync4("npx playwright install chromium", {
6958
+ execSync3("npx playwright install chromium", {
6673
6959
  cwd,
6674
6960
  encoding: "utf-8",
6675
6961
  stdio: ["pipe", "pipe", "pipe"]
@@ -6682,7 +6968,7 @@ function installPlaywrightForQa(cwd) {
6682
6968
  function getDefaultBranch(cwd) {
6683
6969
  const getRefTimestamp = (ref) => {
6684
6970
  try {
6685
- const timestamp = execSync4(`git log -1 --format=%ct ${ref}`, {
6971
+ const timestamp = execSync3(`git log -1 --format=%ct ${ref}`, {
6686
6972
  encoding: "utf-8",
6687
6973
  cwd,
6688
6974
  stdio: ["pipe", "pipe", "pipe"]
@@ -6716,7 +7002,7 @@ function getDefaultBranch(cwd) {
6716
7002
  if (masterTimestamp !== null) {
6717
7003
  return "master";
6718
7004
  }
6719
- 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 ""', {
6720
7006
  encoding: "utf-8",
6721
7007
  cwd,
6722
7008
  stdio: ["pipe", "pipe", "pipe"]
@@ -6972,7 +7258,7 @@ function initCommand(program2) {
6972
7258
  } else {
6973
7259
  let hasGitHubRemote = false;
6974
7260
  try {
6975
- const remoteUrl = execSync4("git remote get-url origin", {
7261
+ const remoteUrl = execSync3("git remote get-url origin", {
6976
7262
  cwd,
6977
7263
  encoding: "utf-8",
6978
7264
  stdio: ["pipe", "pipe", "pipe"]
@@ -7034,7 +7320,7 @@ function initCommand(program2) {
7034
7320
  label("Reviewer", reviewerEnabled ? "Enabled" : "Disabled");
7035
7321
  console.log();
7036
7322
  header("Next Steps");
7037
- info("1. Add your PRD files to docs/PRDs/night-watch/");
7323
+ info(`1. Add your PRD files to ${prdDir}/`);
7038
7324
  info("2. Run `night-watch install` to set up cron jobs");
7039
7325
  info("3. Or run `night-watch run` to execute PRDs manually");
7040
7326
  console.log();
@@ -7054,10 +7340,10 @@ function resolveRunNotificationEvent(exitCode, scriptStatus) {
7054
7340
  return null;
7055
7341
  }
7056
7342
  function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7057
- if (options.dryRun) {
7343
+ if (options.crossProjectFallback !== true) {
7058
7344
  return false;
7059
7345
  }
7060
- if (options.crossProjectFallback === false) {
7346
+ if (options.dryRun) {
7061
7347
  return false;
7062
7348
  }
7063
7349
  if (process.env.NW_CROSS_PROJECT_FALLBACK_ACTIVE === "1") {
@@ -7132,7 +7418,7 @@ async function runCrossProjectFallback(currentProjectDir, options) {
7132
7418
  const envVars = buildEnvVars(candidateConfig, options);
7133
7419
  envVars.NW_CROSS_PROJECT_FALLBACK_ACTIVE = "1";
7134
7420
  try {
7135
- const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars);
7421
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [candidate.path], envVars, { cwd: candidate.path });
7136
7422
  const scriptResult = parseScriptResult(`${stdout}
7137
7423
  ${stderr}`);
7138
7424
  if (!options.dryRun) {
@@ -7251,10 +7537,14 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7251
7537
  return { pending, completed };
7252
7538
  }
7253
7539
  function runCommand(program2) {
7254
- 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) => {
7255
7541
  const projectDir = process.cwd();
7256
7542
  let config = loadConfig(projectDir);
7257
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
+ }
7258
7548
  const envVars = buildEnvVars(config, options);
7259
7549
  const scriptPath = getScriptPath("night-watch-cron.sh");
7260
7550
  if (options.dryRun) {
@@ -7344,7 +7634,7 @@ function runCommand(program2) {
7344
7634
  const spinner = createSpinner("Running PRD executor...");
7345
7635
  spinner.start();
7346
7636
  try {
7347
- const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars);
7637
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptPath, [projectDir], envVars, { cwd: projectDir });
7348
7638
  const scriptResult = parseScriptResult(`${stdout}
7349
7639
  ${stderr}`);
7350
7640
  if (exitCode === 0) {
@@ -7388,6 +7678,23 @@ function parseAutoMergedPrNumbers(raw) {
7388
7678
  }
7389
7679
  return raw.split(",").map((token) => parseInt(token.trim().replace(/^#/, ""), 10)).filter((value) => !Number.isNaN(value));
7390
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
+ }
7391
7698
  function buildEnvVars2(config, options) {
7392
7699
  const env = {};
7393
7700
  const reviewerProvider = resolveJobProvider(config, "reviewer");
@@ -7396,6 +7703,8 @@ function buildEnvVars2(config, options) {
7396
7703
  env.NW_DEFAULT_BRANCH = config.defaultBranch;
7397
7704
  }
7398
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);
7399
7708
  env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
7400
7709
  env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
7401
7710
  if (config.providerEnv) {
@@ -7408,10 +7717,6 @@ function buildEnvVars2(config, options) {
7408
7717
  if (options.dryRun) {
7409
7718
  env.NW_DRY_RUN = "1";
7410
7719
  }
7411
- if (config.autoMerge) {
7412
- env.NW_AUTO_MERGE = "1";
7413
- }
7414
- env.NW_AUTO_MERGE_METHOD = config.autoMergeMethod;
7415
7720
  env.NW_EXECUTION_CONTEXT = "agent";
7416
7721
  return env;
7417
7722
  }
@@ -7437,7 +7742,7 @@ function getOpenPrsNeedingWork(branchPatterns) {
7437
7742
  for (const pattern of branchPatterns) {
7438
7743
  args.push("--head", pattern);
7439
7744
  }
7440
- const result = execFileSync4("gh", args, {
7745
+ const result = execFileSync2("gh", args, {
7441
7746
  encoding: "utf-8",
7442
7747
  stdio: ["pipe", "pipe", "pipe"]
7443
7748
  });
@@ -7456,6 +7761,10 @@ function reviewCommand(program2) {
7456
7761
  const projectDir = process.cwd();
7457
7762
  let config = loadConfig(projectDir);
7458
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
+ }
7459
7768
  const envVars = buildEnvVars2(config, options);
7460
7769
  const scriptPath = getScriptPath("night-watch-pr-reviewer-cron.sh");
7461
7770
  if (options.dryRun) {
@@ -7475,6 +7784,8 @@ function reviewCommand(program2) {
7475
7784
  "Auto-merge",
7476
7785
  config.autoMerge ? `Enabled (${config.autoMergeMethod})` : "Disabled"
7477
7786
  ]);
7787
+ configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
7788
+ configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
7478
7789
  console.log(configTable.toString());
7479
7790
  header("Open PRs Needing Work");
7480
7791
  const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
@@ -7534,6 +7845,8 @@ ${stderr}`);
7534
7845
  }
7535
7846
  }
7536
7847
  if (!skipNotification) {
7848
+ const attempts = parseRetryAttempts(scriptResult?.data.attempts);
7849
+ const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
7537
7850
  const _reviewCtx = {
7538
7851
  event: "review_completed",
7539
7852
  projectName: path15.basename(projectDir),
@@ -7545,7 +7858,9 @@ ${stderr}`);
7545
7858
  prNumber: prDetails?.number,
7546
7859
  filesChanged: prDetails?.changedFiles,
7547
7860
  additions: prDetails?.additions,
7548
- deletions: prDetails?.deletions
7861
+ deletions: prDetails?.deletions,
7862
+ attempts,
7863
+ finalScore
7549
7864
  };
7550
7865
  await sendNotifications(config, _reviewCtx);
7551
7866
  }
@@ -7599,6 +7914,9 @@ function parseQaPrNumbers(prsRaw) {
7599
7914
  }
7600
7915
  return numbers;
7601
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
+ }
7602
7920
  function buildEnvVars3(config, options) {
7603
7921
  const env = {};
7604
7922
  const qaProvider = resolveJobProvider(config, "qa");
@@ -7615,6 +7933,12 @@ function buildEnvVars3(config, options) {
7615
7933
  if (config.providerEnv) {
7616
7934
  Object.assign(env, config.providerEnv);
7617
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
+ }
7618
7942
  if (options.dryRun) {
7619
7943
  env.NW_DRY_RUN = "1";
7620
7944
  }
@@ -7639,6 +7963,10 @@ function qaCommand(program2) {
7639
7963
  const projectDir = process.cwd();
7640
7964
  let config = loadConfig(projectDir);
7641
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
+ }
7642
7970
  const envVars = buildEnvVars3(config, options);
7643
7971
  const scriptPath = getScriptPath("night-watch-qa-cron.sh");
7644
7972
  if (options.dryRun) {
@@ -7721,6 +8049,9 @@ ${stderr}`);
7721
8049
  });
7722
8050
  }
7723
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
+ }
7724
8055
  function buildEnvVars4(config, options) {
7725
8056
  const env = {};
7726
8057
  const auditProvider = resolveJobProvider(config, "audit");
@@ -7732,6 +8063,12 @@ function buildEnvVars4(config, options) {
7732
8063
  if (config.providerEnv) {
7733
8064
  Object.assign(env, config.providerEnv);
7734
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
+ }
7735
8072
  if (options.dryRun) {
7736
8073
  env.NW_DRY_RUN = "1";
7737
8074
  }
@@ -7754,6 +8091,10 @@ function auditCommand(program2) {
7754
8091
  _cliProviderOverride: options.provider
7755
8092
  };
7756
8093
  }
8094
+ if (!config.audit.enabled && !options.dryRun) {
8095
+ info("Audit is disabled in config; skipping run.");
8096
+ process.exit(0);
8097
+ }
7757
8098
  const envVars = buildEnvVars4(config, options);
7758
8099
  const scriptPath = getScriptPath("night-watch-audit-cron.sh");
7759
8100
  if (options.dryRun) {
@@ -7823,7 +8164,7 @@ function shellQuote(value) {
7823
8164
  }
7824
8165
  function getNightWatchBinPath() {
7825
8166
  try {
7826
- const npmBin = execSync5("npm bin -g", { encoding: "utf-8" }).trim();
8167
+ const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
7827
8168
  const binPath = path18.join(npmBin, "night-watch");
7828
8169
  if (fs17.existsSync(binPath)) {
7829
8170
  return binPath;
@@ -7831,14 +8172,14 @@ function getNightWatchBinPath() {
7831
8172
  } catch {
7832
8173
  }
7833
8174
  try {
7834
- return execSync5("which night-watch", { encoding: "utf-8" }).trim();
8175
+ return execSync4("which night-watch", { encoding: "utf-8" }).trim();
7835
8176
  } catch {
7836
8177
  return "night-watch";
7837
8178
  }
7838
8179
  }
7839
8180
  function getNodeBinDir() {
7840
8181
  try {
7841
- const nodePath = execSync5("which node", { encoding: "utf-8" }).trim();
8182
+ const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
7842
8183
  return path18.dirname(nodePath);
7843
8184
  } catch {
7844
8185
  return "";
@@ -7897,8 +8238,11 @@ function performInstall(projectDir, config, options) {
7897
8238
  const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
7898
8239
  providerEnvPrefix = exports + " && ";
7899
8240
  }
7900
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
7901
- 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
+ }
7902
8246
  const installReviewer = options?.noReviewer === true ? false : config.reviewerEnabled;
7903
8247
  if (installReviewer) {
7904
8248
  const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
@@ -7908,7 +8252,7 @@ function performInstall(projectDir, config, options) {
7908
8252
  if (installSlicer) {
7909
8253
  const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
7910
8254
  const slicerLog = path18.join(logDir, "slicer.log");
7911
- 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}`;
7912
8256
  entries.push(slicerEntry);
7913
8257
  }
7914
8258
  const disableQa = options?.noQa === true || options?.qa === false;
@@ -7975,8 +8319,11 @@ function installCommand(program2) {
7975
8319
  const exports = Object.entries(config.providerEnv).map(([key, value]) => `export ${key}=${shellQuote(value)}`).join(" && ");
7976
8320
  providerEnvPrefix = exports + " && ";
7977
8321
  }
7978
- const executorEntry = `${executorSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} run >> ${shellQuote(executorLog)} 2>&1 ${marker}`;
7979
- 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
+ }
7980
8327
  const installReviewer = options.noReviewer === true ? false : config.reviewerEnabled;
7981
8328
  if (installReviewer) {
7982
8329
  const reviewerEntry = `${reviewerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} review >> ${shellQuote(reviewerLog)} 2>&1 ${marker}`;
@@ -7987,7 +8334,7 @@ function installCommand(program2) {
7987
8334
  if (installSlicer) {
7988
8335
  slicerLog = path18.join(logDir, "slicer.log");
7989
8336
  const slicerSchedule = applyScheduleOffset(config.roadmapScanner.slicerSchedule, offset);
7990
- 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}`;
7991
8338
  entries.push(slicerEntry);
7992
8339
  }
7993
8340
  const disableQa = options.noQa === true || options.qa === false;
@@ -8017,12 +8364,14 @@ function installCommand(program2) {
8017
8364
  entries.forEach((entry) => dim(` ${entry}`));
8018
8365
  console.log();
8019
8366
  header("Log Files");
8020
- dim(` Executor: ${executorLog}`);
8367
+ if (installExecutor) {
8368
+ dim(` Executor: ${executorLog}`);
8369
+ }
8021
8370
  if (installReviewer) {
8022
8371
  dim(` Reviewer: ${reviewerLog}`);
8023
8372
  }
8024
8373
  if (installSlicer && slicerLog) {
8025
- dim(` Slicer: ${slicerLog}`);
8374
+ dim(` Planner: ${slicerLog}`);
8026
8375
  }
8027
8376
  if (installQa && qaLog) {
8028
8377
  dim(` QA: ${qaLog}`);
@@ -8141,11 +8490,17 @@ function statusCommand(program2) {
8141
8490
  try {
8142
8491
  const projectDir = process.cwd();
8143
8492
  const config = loadConfig(projectDir);
8144
- const snapshot = fetchStatusSnapshot(projectDir, config);
8493
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
8145
8494
  const executorProc = snapshot.processes.find((p) => p.name === "executor");
8146
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");
8147
8499
  const executorLog = snapshot.logs.find((l) => l.name === "executor");
8148
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");
8149
8504
  const pendingPrds = snapshot.prds.filter((p) => p.status === "ready" || p.status === "blocked").length;
8150
8505
  const claimedPrds = snapshot.prds.filter((p) => p.status === "in-progress").length;
8151
8506
  const donePrds = snapshot.prds.filter((p) => p.status === "done").length;
@@ -8158,6 +8513,9 @@ function statusCommand(program2) {
8158
8513
  autoMergeMethod: config.autoMergeMethod,
8159
8514
  executor: { running: executorProc?.running ?? false, pid: executorProc?.pid ?? null },
8160
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 },
8161
8519
  prds: { pending: pendingPrds, claimed: claimedPrds, done: donePrds },
8162
8520
  prs: { open: snapshot.prs.length },
8163
8521
  crontab: snapshot.crontab,
@@ -8173,6 +8531,24 @@ function statusCommand(program2) {
8173
8531
  lastLines: reviewerLog.lastLines,
8174
8532
  exists: reviewerLog.exists,
8175
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
8176
8552
  } : void 0
8177
8553
  }
8178
8554
  };
@@ -8204,6 +8580,12 @@ function statusCommand(program2) {
8204
8580
  "Reviewer",
8205
8581
  formatRunningStatus(status.reviewer.running, status.reviewer.pid)
8206
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
+ ]);
8207
8589
  console.log(processTable.toString());
8208
8590
  header("PRD Status");
8209
8591
  const prdTable = createTable({ head: ["Status", "Count"] });
@@ -8239,6 +8621,27 @@ function statusCommand(program2) {
8239
8621
  status.logs.reviewer.exists ? "Exists" : "Not found"
8240
8622
  ]);
8241
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
+ }
8242
8645
  console.log(logTable.toString());
8243
8646
  if (options.verbose) {
8244
8647
  if (status.logs.executor?.exists && status.logs.executor.lastLines.length > 0) {
@@ -8249,12 +8652,27 @@ function statusCommand(program2) {
8249
8652
  dim(" Reviewer last 5 lines:");
8250
8653
  status.logs.reviewer.lastLines.forEach((line) => dim(` ${line}`));
8251
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
+ }
8252
8667
  }
8253
8668
  header("Commands");
8254
8669
  dim(" night-watch install - Install crontab entries");
8255
8670
  dim(" night-watch logs - View logs");
8256
8671
  dim(" night-watch run - Run executor now");
8257
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");
8258
8676
  console.log();
8259
8677
  } catch (error2) {
8260
8678
  console.error(`Error getting status: ${error2 instanceof Error ? error2.message : String(error2)}`);
@@ -8293,22 +8711,36 @@ function followLog(filePath) {
8293
8711
  });
8294
8712
  }
8295
8713
  function logsCommand(program2) {
8296
- 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) => {
8297
8715
  try {
8298
8716
  const projectDir = process.cwd();
8299
8717
  const logDir = path20.join(projectDir, LOG_DIR);
8300
8718
  const lineCount = parseInt(options.lines || "50", 10);
8301
8719
  const executorLog = path20.join(logDir, EXECUTOR_LOG_FILE);
8302
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`);
8303
8724
  const logType = options.type?.toLowerCase() || "all";
8304
8725
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
8305
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";
8306
8730
  if (options.follow) {
8307
8731
  if (logType === "all") {
8308
8732
  dim("Note: Following all logs is not supported. Showing executor log.");
8309
- dim("Use --type review to follow reviewer log.\n");
8310
- }
8311
- 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;
8312
8744
  followLog(targetLog);
8313
8745
  return;
8314
8746
  }
@@ -8325,10 +8757,28 @@ function logsCommand(program2) {
8325
8757
  console.log();
8326
8758
  console.log(getLastLines(reviewerLog, lineCount));
8327
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
+ }
8328
8778
  console.log();
8329
8779
  dim("---");
8330
8780
  dim("Tip: Use -f to follow logs in real-time");
8331
- dim(" Use --type run or --type review to view specific logs");
8781
+ dim(" Use --type executor|reviewer|qa|audit|planner to view specific logs");
8332
8782
  } catch (err) {
8333
8783
  console.error(`Error reading logs: ${err instanceof Error ? err.message : String(err)}`);
8334
8784
  process.exit(1);
@@ -9841,10 +10291,14 @@ function createSchedulesTab() {
9841
10291
  ctx.showMessage(`Saved but cron install failed: ${installResult.error}`, "error");
9842
10292
  }
9843
10293
  ctx.config = newConfig;
9844
- const snap = ctx.refreshSnapshot();
9845
- ctx.snapshot = snap;
9846
- renderCrontab(ctx);
9847
- 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
+ });
9848
10302
  }
9849
10303
  function showCustomCronInput(ctx, field, label2) {
9850
10304
  const currentValue = ctx.config[field];
@@ -9942,10 +10396,13 @@ function createSchedulesTab() {
9942
10396
  } else {
9943
10397
  ctx.showMessage(`Install failed: ${result.error}`, "error");
9944
10398
  }
9945
- const snap = ctx.refreshSnapshot();
9946
- ctx.snapshot = snap;
9947
- renderCrontab(ctx);
9948
- 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
+ });
9949
10406
  }
9950
10407
  ],
9951
10408
  [
@@ -9957,10 +10414,13 @@ function createSchedulesTab() {
9957
10414
  } else {
9958
10415
  ctx.showMessage(`Uninstall failed: ${result.error}`, "error");
9959
10416
  }
9960
- const snap = ctx.refreshSnapshot();
9961
- ctx.snapshot = snap;
9962
- renderCrontab(ctx);
9963
- 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
+ });
9964
10424
  }
9965
10425
  ],
9966
10426
  [
@@ -9973,10 +10433,13 @@ function createSchedulesTab() {
9973
10433
  } else {
9974
10434
  ctx.showMessage(`Reinstall failed: ${result.error}`, "error");
9975
10435
  }
9976
- const snap = ctx.refreshSnapshot();
9977
- ctx.snapshot = snap;
9978
- renderCrontab(ctx);
9979
- 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
+ });
9980
10443
  }
9981
10444
  ]
9982
10445
  ];
@@ -10104,9 +10567,12 @@ ${result.entries.map((e) => ` ${e}`).join("\n")}`);
10104
10567
  outputBox.setContent(`{red-fg}Install failed: ${result.error}{/red-fg}`);
10105
10568
  ctx.showMessage("Install failed", "error");
10106
10569
  }
10107
- const snap = ctx.refreshSnapshot();
10108
- ctx.snapshot = snap;
10109
- ctx.screen.render();
10570
+ ctx.refreshSnapshot().then((snap) => {
10571
+ ctx.snapshot = snap;
10572
+ ctx.screen.render();
10573
+ }).catch(() => {
10574
+ ctx.screen.render();
10575
+ });
10110
10576
  }
10111
10577
  },
10112
10578
  {
@@ -10122,9 +10588,12 @@ Removed ${result.removedCount} entries.`);
10122
10588
  outputBox.setContent(`{red-fg}Uninstall failed: ${result.error}{/red-fg}`);
10123
10589
  ctx.showMessage("Uninstall failed", "error");
10124
10590
  }
10125
- const snap = ctx.refreshSnapshot();
10126
- ctx.snapshot = snap;
10127
- ctx.screen.render();
10591
+ ctx.refreshSnapshot().then((snap) => {
10592
+ ctx.snapshot = snap;
10593
+ ctx.screen.render();
10594
+ }).catch(() => {
10595
+ ctx.screen.render();
10596
+ });
10128
10597
  }
10129
10598
  },
10130
10599
  {
@@ -10517,7 +10986,7 @@ function dashboardCommand(program2) {
10517
10986
  }
10518
10987
  let activeTabIndex = 0;
10519
10988
  let isEditing = false;
10520
- let snapshot = fetchStatusSnapshot(projectDir, config);
10989
+ let snapshot = await fetchStatusSnapshot(projectDir, config);
10521
10990
  const ctx = {
10522
10991
  screen,
10523
10992
  projectDir,
@@ -10528,8 +10997,8 @@ function dashboardCommand(program2) {
10528
10997
  ctx.config = config;
10529
10998
  return config;
10530
10999
  },
10531
- refreshSnapshot: () => {
10532
- snapshot = fetchStatusSnapshot(projectDir, config);
11000
+ refreshSnapshot: async () => {
11001
+ snapshot = await fetchStatusSnapshot(projectDir, config);
10533
11002
  ctx.snapshot = snapshot;
10534
11003
  return snapshot;
10535
11004
  },
@@ -10573,10 +11042,10 @@ function dashboardCommand(program2) {
10573
11042
  function updateHeader() {
10574
11043
  headerBox.setContent(`{center}Night Watch: ${snapshot.projectName} | Provider: ${config.provider} | Last: ${snapshot.timestamp.toLocaleTimeString()} | Next: ${countdown}s{/center}`);
10575
11044
  }
10576
- function refreshData() {
11045
+ async function refreshData() {
10577
11046
  config = loadConfig(projectDir);
10578
11047
  ctx.config = config;
10579
- snapshot = fetchStatusSnapshot(projectDir, config);
11048
+ snapshot = await fetchStatusSnapshot(projectDir, config);
10580
11049
  ctx.snapshot = snapshot;
10581
11050
  countdown = intervalSeconds;
10582
11051
  updateHeader();
@@ -10588,7 +11057,8 @@ function dashboardCommand(program2) {
10588
11057
  updateHeader();
10589
11058
  screen.render();
10590
11059
  if (countdown <= 0) {
10591
- refreshData();
11060
+ refreshData().catch(() => {
11061
+ });
10592
11062
  }
10593
11063
  }, 1e3);
10594
11064
  screen.key(["q", "escape"], () => {
@@ -10604,7 +11074,8 @@ function dashboardCommand(program2) {
10604
11074
  screen.key(["r"], () => {
10605
11075
  if (isEditing)
10606
11076
  return;
10607
- refreshData();
11077
+ refreshData().catch(() => {
11078
+ });
10608
11079
  });
10609
11080
  for (let i = 0; i < tabs.length; i++) {
10610
11081
  const idx = i;
@@ -10903,8 +11374,7 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
10903
11374
  return setInterval(() => {
10904
11375
  if (clients.size === 0)
10905
11376
  return;
10906
- try {
10907
- const snapshot = fetchStatusSnapshot(projectDir, getConfig());
11377
+ fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
10908
11378
  const hash = JSON.stringify({
10909
11379
  processes: snapshot.processes,
10910
11380
  prds: snapshot.prds.map((p) => ({ n: p.name, s: p.status }))
@@ -10913,8 +11383,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
10913
11383
  lastSnapshotHash = hash;
10914
11384
  broadcastSSE(clients, "status_changed", snapshot);
10915
11385
  }
10916
- } catch {
10917
- }
11386
+ }).catch(() => {
11387
+ });
10918
11388
  }, 2e3);
10919
11389
  }
10920
11390
  init_dist();
@@ -10985,11 +11455,17 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
10985
11455
  lockPath = executorLockPath(projectDir);
10986
11456
  } else if (command[0] === "review") {
10987
11457
  lockPath = reviewerLockPath(projectDir);
11458
+ } else if (command[0] === "planner") {
11459
+ lockPath = plannerLockPath(projectDir);
10988
11460
  }
10989
11461
  if (lockPath) {
10990
11462
  const lock = checkLockFile(lockPath);
10991
11463
  if (lock.running) {
10992
- 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";
10993
11469
  res.status(409).json({
10994
11470
  error: `${processType} is already running (PID ${lock.pid})`,
10995
11471
  pid: lock.pid
@@ -11033,25 +11509,36 @@ function spawnAction2(projectDir, command, req, res, onSpawned) {
11033
11509
  });
11034
11510
  }
11035
11511
  }
11036
- function createActionRoutes(deps) {
11037
- const { projectDir, getConfig, sseClients } = deps;
11038
- const router = Router();
11039
- 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);
11040
11517
  spawnAction2(projectDir, ["run"], req, res, (pid) => {
11041
- broadcastSSE(sseClients, "executor_started", { pid });
11518
+ broadcastSSE(ctx.getSseClients(req), "executor_started", { pid });
11042
11519
  });
11043
11520
  });
11044
- router.post("/review", (req, res) => {
11045
- spawnAction2(projectDir, ["review"], req, res);
11521
+ router.post(`/${p}review`, (req, res) => {
11522
+ spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
11046
11523
  });
11047
- router.post("/install-cron", (req, res) => {
11048
- spawnAction2(projectDir, ["install"], req, res);
11524
+ router.post(`/${p}qa`, (req, res) => {
11525
+ spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
11049
11526
  });
11050
- router.post("/uninstall-cron", (req, res) => {
11051
- spawnAction2(projectDir, ["uninstall"], req, res);
11527
+ router.post(`/${p}audit`, (req, res) => {
11528
+ spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
11052
11529
  });
11053
- router.post("/cancel", async (req, res) => {
11530
+ router.post(`/${p}planner`, (req, res) => {
11531
+ spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
11532
+ });
11533
+ router.post(`/${p}install-cron`, (req, res) => {
11534
+ spawnAction2(ctx.getProjectDir(req), ["install"], req, res);
11535
+ });
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) => {
11054
11540
  try {
11541
+ const projectDir = ctx.getProjectDir(req);
11055
11542
  const { type = "all" } = req.body;
11056
11543
  const validTypes = ["run", "review", "all"];
11057
11544
  if (!validTypes.includes(type)) {
@@ -11072,9 +11559,10 @@ function createActionRoutes(deps) {
11072
11559
  });
11073
11560
  }
11074
11561
  });
11075
- router.post("/retry", (req, res) => {
11562
+ router.post(`/${p}retry`, (req, res) => {
11076
11563
  try {
11077
- const config = getConfig();
11564
+ const projectDir = ctx.getProjectDir(req);
11565
+ const config = ctx.getConfig(req);
11078
11566
  const { prdName } = req.body;
11079
11567
  if (!prdName || typeof prdName !== "string") {
11080
11568
  res.status(400).json({ error: "prdName is required" });
@@ -11104,9 +11592,10 @@ function createActionRoutes(deps) {
11104
11592
  });
11105
11593
  }
11106
11594
  });
11107
- router.post("/clear-lock", (req, res) => {
11595
+ router.post(`/${p}clear-lock`, async (req, res) => {
11108
11596
  try {
11109
- const config = getConfig();
11597
+ const projectDir = ctx.getProjectDir(req);
11598
+ const config = ctx.getConfig(req);
11110
11599
  const lockPath = executorLockPath(projectDir);
11111
11600
  const lock = checkLockFile(lockPath);
11112
11601
  if (lock.running) {
@@ -11120,7 +11609,7 @@ function createActionRoutes(deps) {
11120
11609
  if (fs24.existsSync(prdDir)) {
11121
11610
  cleanOrphanedClaims(prdDir);
11122
11611
  }
11123
- broadcastSSE(sseClients, "status_changed", fetchStatusSnapshot(projectDir, config));
11612
+ broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
11124
11613
  res.json({ cleared: true });
11125
11614
  } catch (error2) {
11126
11615
  res.status(500).json({
@@ -11130,110 +11619,28 @@ function createActionRoutes(deps) {
11130
11619
  });
11131
11620
  return router;
11132
11621
  }
11622
+ function createActionRoutes(deps) {
11623
+ return createActionRouteHandlers({
11624
+ getConfig: () => deps.getConfig(),
11625
+ getProjectDir: () => deps.projectDir,
11626
+ getSseClients: () => deps.sseClients,
11627
+ pathPrefix: ""
11628
+ });
11629
+ }
11133
11630
  function createProjectActionRoutes(deps) {
11134
11631
  const { projectSseClients } = deps;
11135
- const router = Router({ mergeParams: true });
11136
- router.post("/actions/run", (req, res) => {
11137
- const projectDir = req.projectDir;
11138
- spawnAction2(projectDir, ["run"], req, res, (pid) => {
11139
- const clients = projectSseClients.get(projectDir);
11140
- if (clients) {
11141
- broadcastSSE(clients, "executor_started", { pid });
11142
- }
11143
- });
11144
- });
11145
- router.post("/actions/review", (req, res) => {
11146
- spawnAction2(req.projectDir, ["review"], req, res);
11147
- });
11148
- router.post("/actions/install-cron", (req, res) => {
11149
- spawnAction2(req.projectDir, ["install"], req, res);
11150
- });
11151
- router.post("/actions/uninstall-cron", (req, res) => {
11152
- spawnAction2(req.projectDir, ["uninstall"], req, res);
11153
- });
11154
- router.post("/actions/cancel", async (req, res) => {
11155
- try {
11632
+ return createActionRouteHandlers({
11633
+ getConfig: (req) => req.projectConfig,
11634
+ getProjectDir: (req) => req.projectDir,
11635
+ getSseClients: (req) => {
11156
11636
  const projectDir = req.projectDir;
11157
- const { type = "all" } = req.body;
11158
- const validTypes = ["run", "review", "all"];
11159
- if (!validTypes.includes(type)) {
11160
- res.status(400).json({
11161
- error: `Invalid type. Must be one of: ${validTypes.join(", ")}`
11162
- });
11163
- return;
11637
+ if (!projectSseClients.has(projectDir)) {
11638
+ projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
11164
11639
  }
11165
- const results = await performCancel(projectDir, {
11166
- type,
11167
- force: true
11168
- });
11169
- const hasFailure = results.some((r) => !r.success);
11170
- res.status(hasFailure ? 500 : 200).json({ results });
11171
- } catch (error2) {
11172
- res.status(500).json({
11173
- error: error2 instanceof Error ? error2.message : String(error2)
11174
- });
11175
- }
11176
- });
11177
- router.post("/actions/retry", (req, res) => {
11178
- try {
11179
- const projectDir = req.projectDir;
11180
- const config = req.projectConfig;
11181
- const { prdName } = req.body;
11182
- if (!prdName || typeof prdName !== "string") {
11183
- res.status(400).json({ error: "prdName is required" });
11184
- return;
11185
- }
11186
- if (!validatePrdName(prdName)) {
11187
- res.status(400).json({ error: "Invalid PRD name" });
11188
- return;
11189
- }
11190
- const prdDir = path24.join(projectDir, config.prdDir);
11191
- const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
11192
- const pendingPath = path24.join(prdDir, normalized);
11193
- const donePath = path24.join(prdDir, "done", normalized);
11194
- if (fs24.existsSync(pendingPath)) {
11195
- res.json({ message: `"${normalized}" is already pending` });
11196
- return;
11197
- }
11198
- if (!fs24.existsSync(donePath)) {
11199
- res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
11200
- return;
11201
- }
11202
- fs24.renameSync(donePath, pendingPath);
11203
- res.json({ message: `Moved "${normalized}" back to pending` });
11204
- } catch (error2) {
11205
- res.status(500).json({
11206
- error: error2 instanceof Error ? error2.message : String(error2)
11207
- });
11208
- }
11209
- });
11210
- router.post("/actions/clear-lock", (req, res) => {
11211
- try {
11212
- const projectDir = req.projectDir;
11213
- const config = req.projectConfig;
11214
- const lockPath = executorLockPath(projectDir);
11215
- const lock = checkLockFile(lockPath);
11216
- if (lock.running) {
11217
- res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
11218
- return;
11219
- }
11220
- if (fs24.existsSync(lockPath)) {
11221
- fs24.unlinkSync(lockPath);
11222
- }
11223
- const prdDir = path24.join(projectDir, config.prdDir);
11224
- if (fs24.existsSync(prdDir)) {
11225
- cleanOrphanedClaims(prdDir);
11226
- }
11227
- const clients = projectSseClients.get(projectDir) ?? /* @__PURE__ */ new Set();
11228
- broadcastSSE(clients, "status_changed", fetchStatusSnapshot(projectDir, config));
11229
- res.json({ cleared: true });
11230
- } catch (error2) {
11231
- res.status(500).json({
11232
- error: error2 instanceof Error ? error2.message : String(error2)
11233
- });
11234
- }
11640
+ return projectSseClients.get(projectDir);
11641
+ },
11642
+ pathPrefix: "actions/"
11235
11643
  });
11236
- return router;
11237
11644
  }
11238
11645
  init_dist();
11239
11646
  function createAgentRoutes() {
@@ -11336,12 +11743,13 @@ function createAgentRoutes() {
11336
11743
  return router;
11337
11744
  }
11338
11745
  init_dist();
11339
- function createBoardRoutes(deps) {
11340
- const { projectDir, getConfig } = deps;
11341
- const router = Router3();
11342
- 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) => {
11343
11750
  try {
11344
- const config = getConfig();
11751
+ const config = ctx.getConfig(req);
11752
+ const projectDir = ctx.getProjectDir(req);
11345
11753
  const provider = getBoardProvider(config, projectDir);
11346
11754
  if (!provider) {
11347
11755
  res.status(404).json({ error: "Board not configured" });
@@ -11371,9 +11779,10 @@ function createBoardRoutes(deps) {
11371
11779
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
11372
11780
  }
11373
11781
  });
11374
- router.get("/issues", async (_req, res) => {
11782
+ router.get(`/${p}issues`, async (req, res) => {
11375
11783
  try {
11376
- const config = getConfig();
11784
+ const config = ctx.getConfig(req);
11785
+ const projectDir = ctx.getProjectDir(req);
11377
11786
  const provider = getBoardProvider(config, projectDir);
11378
11787
  if (!provider) {
11379
11788
  res.status(404).json({ error: "Board not configured" });
@@ -11385,9 +11794,10 @@ function createBoardRoutes(deps) {
11385
11794
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
11386
11795
  }
11387
11796
  });
11388
- router.post("/issues", async (req, res) => {
11797
+ router.post(`/${p}issues`, async (req, res) => {
11389
11798
  try {
11390
- const config = getConfig();
11799
+ const config = ctx.getConfig(req);
11800
+ const projectDir = ctx.getProjectDir(req);
11391
11801
  const provider = getBoardProvider(config, projectDir);
11392
11802
  if (!provider) {
11393
11803
  res.status(404).json({ error: "Board not configured" });
@@ -11415,9 +11825,10 @@ function createBoardRoutes(deps) {
11415
11825
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
11416
11826
  }
11417
11827
  });
11418
- router.patch("/issues/:number/move", async (req, res) => {
11828
+ router.patch(`/${p}issues/:number/move`, async (req, res) => {
11419
11829
  try {
11420
- const config = getConfig();
11830
+ const config = ctx.getConfig(req);
11831
+ const projectDir = ctx.getProjectDir(req);
11421
11832
  const provider = getBoardProvider(config, projectDir);
11422
11833
  if (!provider) {
11423
11834
  res.status(404).json({ error: "Board not configured" });
@@ -11444,9 +11855,10 @@ function createBoardRoutes(deps) {
11444
11855
  });
11445
11856
  }
11446
11857
  });
11447
- router.post("/issues/:number/comment", async (req, res) => {
11858
+ router.post(`/${p}issues/:number/comment`, async (req, res) => {
11448
11859
  try {
11449
- const config = getConfig();
11860
+ const config = ctx.getConfig(req);
11861
+ const projectDir = ctx.getProjectDir(req);
11450
11862
  const provider = getBoardProvider(config, projectDir);
11451
11863
  if (!provider) {
11452
11864
  res.status(404).json({ error: "Board not configured" });
@@ -11471,9 +11883,10 @@ function createBoardRoutes(deps) {
11471
11883
  });
11472
11884
  }
11473
11885
  });
11474
- router.delete("/issues/:number", async (req, res) => {
11886
+ router.delete(`/${p}issues/:number`, async (req, res) => {
11475
11887
  try {
11476
- const config = getConfig();
11888
+ const config = ctx.getConfig(req);
11889
+ const projectDir = ctx.getProjectDir(req);
11477
11890
  const provider = getBoardProvider(config, projectDir);
11478
11891
  if (!provider) {
11479
11892
  res.status(404).json({ error: "Board not configured" });
@@ -11495,175 +11908,19 @@ function createBoardRoutes(deps) {
11495
11908
  });
11496
11909
  return router;
11497
11910
  }
11498
- function createProjectBoardRoutes() {
11499
- const router = Router3({ mergeParams: true });
11500
- router.get("/board/status", async (req, res) => {
11501
- try {
11502
- const config = req.projectConfig;
11503
- const projectDir = req.projectDir;
11504
- const provider = getBoardProvider(config, projectDir);
11505
- if (!provider) {
11506
- res.status(404).json({ error: "Board not configured" });
11507
- return;
11508
- }
11509
- const cached = getCachedBoardData(projectDir);
11510
- if (cached) {
11511
- res.json(cached);
11512
- return;
11513
- }
11514
- const issues = await provider.getAllIssues();
11515
- const columns = {
11516
- Draft: [],
11517
- Ready: [],
11518
- "In Progress": [],
11519
- Review: [],
11520
- Done: []
11521
- };
11522
- for (const issue of issues) {
11523
- const col = issue.column ?? "Draft";
11524
- columns[col].push(issue);
11525
- }
11526
- const result = { enabled: true, columns };
11527
- setCachedBoardData(projectDir, result);
11528
- res.json(result);
11529
- } catch (error2) {
11530
- res.status(500).json({
11531
- error: error2 instanceof Error ? error2.message : String(error2)
11532
- });
11533
- }
11534
- });
11535
- router.get("/board/issues", async (_req, res) => {
11536
- try {
11537
- const config = _req.projectConfig;
11538
- const projectDir = _req.projectDir;
11539
- const provider = getBoardProvider(config, projectDir);
11540
- if (!provider) {
11541
- res.status(404).json({ error: "Board not configured" });
11542
- return;
11543
- }
11544
- const issues = await provider.getAllIssues();
11545
- res.json(issues);
11546
- } catch (error2) {
11547
- res.status(500).json({
11548
- error: error2 instanceof Error ? error2.message : String(error2)
11549
- });
11550
- }
11551
- });
11552
- router.post("/board/issues", async (req, res) => {
11553
- try {
11554
- const config = req.projectConfig;
11555
- const projectDir = req.projectDir;
11556
- const provider = getBoardProvider(config, projectDir);
11557
- if (!provider) {
11558
- res.status(404).json({ error: "Board not configured" });
11559
- return;
11560
- }
11561
- const { title, body, column } = req.body;
11562
- if (!title || typeof title !== "string" || title.trim().length === 0) {
11563
- res.status(400).json({ error: "title is required" });
11564
- return;
11565
- }
11566
- if (column && !BOARD_COLUMNS.includes(column)) {
11567
- res.status(400).json({
11568
- error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
11569
- });
11570
- return;
11571
- }
11572
- const issue = await provider.createIssue({
11573
- title: title.trim(),
11574
- body: body ?? "",
11575
- column
11576
- });
11577
- invalidateBoardCache(projectDir);
11578
- res.status(201).json(issue);
11579
- } catch (error2) {
11580
- res.status(500).json({
11581
- error: error2 instanceof Error ? error2.message : String(error2)
11582
- });
11583
- }
11584
- });
11585
- router.patch("/board/issues/:number/move", async (req, res) => {
11586
- try {
11587
- const config = req.projectConfig;
11588
- const projectDir = req.projectDir;
11589
- const provider = getBoardProvider(config, projectDir);
11590
- if (!provider) {
11591
- res.status(404).json({ error: "Board not configured" });
11592
- return;
11593
- }
11594
- const issueNumber = parseInt(req.params.number, 10);
11595
- if (isNaN(issueNumber)) {
11596
- res.status(400).json({ error: "Invalid issue number" });
11597
- return;
11598
- }
11599
- const { column } = req.body;
11600
- if (!column || !BOARD_COLUMNS.includes(column)) {
11601
- res.status(400).json({
11602
- error: `Invalid column. Must be one of: ${BOARD_COLUMNS.join(", ")}`
11603
- });
11604
- return;
11605
- }
11606
- await provider.moveIssue(issueNumber, column);
11607
- invalidateBoardCache(projectDir);
11608
- res.json({ moved: true });
11609
- } catch (error2) {
11610
- res.status(500).json({
11611
- error: error2 instanceof Error ? error2.message : String(error2)
11612
- });
11613
- }
11614
- });
11615
- router.post("/board/issues/:number/comment", async (req, res) => {
11616
- try {
11617
- const config = req.projectConfig;
11618
- const projectDir = req.projectDir;
11619
- const provider = getBoardProvider(config, projectDir);
11620
- if (!provider) {
11621
- res.status(404).json({ error: "Board not configured" });
11622
- return;
11623
- }
11624
- const issueNumber = parseInt(req.params.number, 10);
11625
- if (isNaN(issueNumber)) {
11626
- res.status(400).json({ error: "Invalid issue number" });
11627
- return;
11628
- }
11629
- const { body } = req.body;
11630
- if (!body || typeof body !== "string" || body.trim().length === 0) {
11631
- res.status(400).json({ error: "body is required" });
11632
- return;
11633
- }
11634
- await provider.commentOnIssue(issueNumber, body);
11635
- invalidateBoardCache(projectDir);
11636
- res.json({ commented: true });
11637
- } catch (error2) {
11638
- res.status(500).json({
11639
- error: error2 instanceof Error ? error2.message : String(error2)
11640
- });
11641
- }
11911
+ function createBoardRoutes(deps) {
11912
+ return createBoardRouteHandlers({
11913
+ getConfig: () => deps.getConfig(),
11914
+ getProjectDir: () => deps.projectDir,
11915
+ pathPrefix: ""
11642
11916
  });
11643
- router.delete("/board/issues/:number", async (req, res) => {
11644
- try {
11645
- const config = req.projectConfig;
11646
- const projectDir = req.projectDir;
11647
- const provider = getBoardProvider(config, projectDir);
11648
- if (!provider) {
11649
- res.status(404).json({ error: "Board not configured" });
11650
- return;
11651
- }
11652
- const issueNumber = parseInt(req.params.number, 10);
11653
- if (isNaN(issueNumber)) {
11654
- res.status(400).json({ error: "Invalid issue number" });
11655
- return;
11656
- }
11657
- await provider.closeIssue(issueNumber);
11658
- invalidateBoardCache(projectDir);
11659
- res.json({ closed: true });
11660
- } catch (error2) {
11661
- res.status(500).json({
11662
- error: error2 instanceof Error ? error2.message : String(error2)
11663
- });
11664
- }
11917
+ }
11918
+ function createProjectBoardRoutes() {
11919
+ return createBoardRouteHandlers({
11920
+ getConfig: (req) => req.projectConfig,
11921
+ getProjectDir: (req) => req.projectDir,
11922
+ pathPrefix: "board/"
11665
11923
  });
11666
- return router;
11667
11924
  }
11668
11925
  init_dist();
11669
11926
  function validateConfigChanges(changes) {
@@ -11679,6 +11936,9 @@ function validateConfigChanges(changes) {
11679
11936
  if (changes.reviewerEnabled !== void 0 && typeof changes.reviewerEnabled !== "boolean") {
11680
11937
  return "reviewerEnabled must be a boolean";
11681
11938
  }
11939
+ if (changes.executorEnabled !== void 0 && typeof changes.executorEnabled !== "boolean") {
11940
+ return "executorEnabled must be a boolean";
11941
+ }
11682
11942
  if (changes.maxRuntime !== void 0 && (typeof changes.maxRuntime !== "number" || changes.maxRuntime < 60)) {
11683
11943
  return "maxRuntime must be a number >= 60";
11684
11944
  }
@@ -11751,6 +12011,82 @@ function validateConfigChanges(changes) {
11751
12011
  }
11752
12012
  }
11753
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
+ }
11754
12090
  return null;
11755
12091
  }
11756
12092
  function createConfigRoutes(deps) {
@@ -11818,7 +12154,7 @@ init_dist();
11818
12154
  function runDoctorChecks(projectDir, config) {
11819
12155
  const checks = [];
11820
12156
  try {
11821
- execSync6("git rev-parse --is-inside-work-tree", {
12157
+ execSync5("git rev-parse --is-inside-work-tree", {
11822
12158
  cwd: projectDir,
11823
12159
  stdio: "pipe"
11824
12160
  });
@@ -11827,7 +12163,7 @@ function runDoctorChecks(projectDir, config) {
11827
12163
  checks.push({ name: "git", status: "fail", detail: "Not a git repository" });
11828
12164
  }
11829
12165
  try {
11830
- execSync6(`which ${config.provider}`, { stdio: "pipe" });
12166
+ execSync5(`which ${config.provider}`, { stdio: "pipe" });
11831
12167
  checks.push({
11832
12168
  name: "provider",
11833
12169
  status: "pass",
@@ -11923,7 +12259,7 @@ function createLogRoutes(deps) {
11923
12259
  router.get("/:name", (req, res) => {
11924
12260
  try {
11925
12261
  const { name } = req.params;
11926
- const validNames = ["executor", "reviewer", "qa"];
12262
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
11927
12263
  if (!validNames.includes(name)) {
11928
12264
  res.status(400).json({
11929
12265
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -11949,7 +12285,7 @@ function createProjectLogRoutes() {
11949
12285
  try {
11950
12286
  const projectDir = req.projectDir;
11951
12287
  const { name } = req.params;
11952
- const validNames = ["executor", "reviewer", "qa"];
12288
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
11953
12289
  if (!validNames.includes(name)) {
11954
12290
  res.status(400).json({
11955
12291
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -11990,12 +12326,13 @@ function createProjectPrdRoutes() {
11990
12326
  return router;
11991
12327
  }
11992
12328
  init_dist();
11993
- function createRoadmapRoutes(deps) {
11994
- const { projectDir, getConfig, reloadConfig } = deps;
11995
- const router = Router8();
11996
- 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) => {
11997
12333
  try {
11998
- const config = getConfig();
12334
+ const config = ctx.getConfig(req);
12335
+ const projectDir = ctx.getProjectDir(req);
11999
12336
  const status = getRoadmapStatus(projectDir, config);
12000
12337
  const prdDir = path27.join(projectDir, config.prdDir);
12001
12338
  const state = loadRoadmapState(prdDir);
@@ -12008,9 +12345,10 @@ function createRoadmapRoutes(deps) {
12008
12345
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12009
12346
  }
12010
12347
  });
12011
- router.post("/scan", async (_req, res) => {
12348
+ router.post(`/${p}scan`, async (req, res) => {
12012
12349
  try {
12013
- const config = getConfig();
12350
+ const config = ctx.getConfig(req);
12351
+ const projectDir = ctx.getProjectDir(req);
12014
12352
  if (!config.roadmapScanner.enabled) {
12015
12353
  res.status(409).json({ error: "Roadmap scanner is disabled" });
12016
12354
  return;
@@ -12023,14 +12361,15 @@ function createRoadmapRoutes(deps) {
12023
12361
  });
12024
12362
  }
12025
12363
  });
12026
- router.put("/toggle", (req, res) => {
12364
+ router.put(`/${p}toggle`, (req, res) => {
12027
12365
  try {
12028
12366
  const { enabled } = req.body;
12029
12367
  if (typeof enabled !== "boolean") {
12030
12368
  res.status(400).json({ error: "enabled must be a boolean" });
12031
12369
  return;
12032
12370
  }
12033
- const currentConfig = getConfig();
12371
+ const projectDir = ctx.getProjectDir(req);
12372
+ const currentConfig = ctx.getConfig(req);
12034
12373
  const result = saveConfig(projectDir, {
12035
12374
  roadmapScanner: {
12036
12375
  ...currentConfig.roadmapScanner,
@@ -12041,71 +12380,30 @@ function createRoadmapRoutes(deps) {
12041
12380
  res.status(500).json({ error: result.error });
12042
12381
  return;
12043
12382
  }
12044
- reloadConfig();
12045
- res.json(getConfig());
12383
+ ctx.afterToggle(req);
12384
+ res.json(loadConfig(projectDir));
12046
12385
  } catch (error2) {
12047
12386
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12048
12387
  }
12049
12388
  });
12050
12389
  return router;
12051
12390
  }
12052
- function createProjectRoadmapRoutes() {
12053
- const router = Router8({ mergeParams: true });
12054
- router.get("/roadmap", (req, res) => {
12055
- try {
12056
- const config = req.projectConfig;
12057
- const projectDir = req.projectDir;
12058
- const status = getRoadmapStatus(projectDir, config);
12059
- const prdDir = path27.join(projectDir, config.prdDir);
12060
- const state = loadRoadmapState(prdDir);
12061
- res.json({
12062
- ...status,
12063
- lastScan: state.lastScan || null,
12064
- autoScanInterval: config.roadmapScanner.autoScanInterval
12065
- });
12066
- } catch (error2) {
12067
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12068
- }
12069
- });
12070
- router.post("/roadmap/scan", async (req, res) => {
12071
- try {
12072
- const config = req.projectConfig;
12073
- const projectDir = req.projectDir;
12074
- if (!config.roadmapScanner.enabled) {
12075
- res.status(409).json({ error: "Roadmap scanner is disabled" });
12076
- return;
12077
- }
12078
- const result = await scanRoadmap(projectDir, config);
12079
- res.json(result);
12080
- } catch (error2) {
12081
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12082
- }
12391
+ function createRoadmapRoutes(deps) {
12392
+ return createRoadmapRouteHandlers({
12393
+ getConfig: () => deps.getConfig(),
12394
+ getProjectDir: () => deps.projectDir,
12395
+ afterToggle: () => deps.reloadConfig(),
12396
+ pathPrefix: ""
12083
12397
  });
12084
- router.put("/roadmap/toggle", (req, res) => {
12085
- const projectDir = req.projectDir;
12086
- try {
12087
- const { enabled } = req.body;
12088
- if (typeof enabled !== "boolean") {
12089
- res.status(400).json({ error: "enabled must be a boolean" });
12090
- return;
12091
- }
12092
- const currentConfig = req.projectConfig;
12093
- const result = saveConfig(projectDir, {
12094
- roadmapScanner: {
12095
- ...currentConfig.roadmapScanner,
12096
- enabled
12097
- }
12098
- });
12099
- if (!result.success) {
12100
- res.status(500).json({ error: result.error });
12101
- return;
12102
- }
12103
- res.json(loadConfig(projectDir));
12104
- } catch (error2) {
12105
- res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12106
- }
12398
+ }
12399
+ function createProjectRoadmapRoutes() {
12400
+ return createRoadmapRouteHandlers({
12401
+ getConfig: (req) => req.projectConfig,
12402
+ getProjectDir: (req) => req.projectDir,
12403
+ afterToggle: () => {
12404
+ },
12405
+ pathPrefix: "roadmap/"
12107
12406
  });
12108
- return router;
12109
12407
  }
12110
12408
  init_dist();
12111
12409
  function createStatusRoutes(deps) {
@@ -12117,21 +12415,20 @@ function createStatusRoutes(deps) {
12117
12415
  res.setHeader("Connection", "keep-alive");
12118
12416
  res.flushHeaders();
12119
12417
  sseClients.add(res);
12120
- try {
12121
- const snapshot = fetchStatusSnapshot(projectDir, getConfig());
12418
+ fetchStatusSnapshot(projectDir, getConfig()).then((snapshot) => {
12122
12419
  res.write(`event: status_changed
12123
12420
  data: ${JSON.stringify(snapshot)}
12124
12421
 
12125
12422
  `);
12126
- } catch {
12127
- }
12423
+ }).catch(() => {
12424
+ });
12128
12425
  req.on("close", () => {
12129
12426
  sseClients.delete(res);
12130
12427
  });
12131
12428
  });
12132
- router.get("/", (_req, res) => {
12429
+ router.get("/", async (_req, res) => {
12133
12430
  try {
12134
- const snapshot = fetchStatusSnapshot(projectDir, getConfig());
12431
+ const snapshot = await fetchStatusSnapshot(projectDir, getConfig());
12135
12432
  res.json(snapshot);
12136
12433
  } catch (error2) {
12137
12434
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
@@ -12147,30 +12444,49 @@ function computeNextRun(cronExpr) {
12147
12444
  return null;
12148
12445
  }
12149
12446
  }
12447
+ function hasScheduledCommand(entries, command) {
12448
+ const commandPattern = new RegExp(`\\s${command}\\s+>>`);
12449
+ return entries.some((entry) => commandPattern.test(entry));
12450
+ }
12150
12451
  function createScheduleInfoRoutes(deps) {
12151
12452
  const { projectDir, getConfig } = deps;
12152
12453
  const router = Router9();
12153
- router.get("/", (_req, res) => {
12454
+ router.get("/", async (_req, res) => {
12154
12455
  try {
12155
12456
  const config = getConfig();
12156
- const snapshot = fetchStatusSnapshot(projectDir, config);
12457
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
12157
12458
  const installed = snapshot.crontab.installed;
12158
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"));
12159
12465
  res.json({
12160
12466
  executor: {
12161
12467
  schedule: config.cronSchedule,
12162
- installed,
12163
- nextRun: installed ? computeNextRun(config.cronSchedule) : null
12468
+ installed: executorInstalled,
12469
+ nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
12164
12470
  },
12165
12471
  reviewer: {
12166
12472
  schedule: config.reviewerSchedule,
12167
- installed: installed && config.reviewerEnabled,
12168
- nextRun: installed && config.reviewerEnabled ? computeNextRun(config.reviewerSchedule) : null
12473
+ installed: reviewerInstalled,
12474
+ nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
12169
12475
  },
12170
12476
  qa: {
12171
12477
  schedule: config.qa.schedule,
12172
- installed: installed && config.qa.enabled,
12173
- 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
12174
12490
  },
12175
12491
  paused: !installed,
12176
12492
  entries
@@ -12186,13 +12502,12 @@ function createProjectSseRoutes(deps) {
12186
12502
  const router = Router9({ mergeParams: true });
12187
12503
  router.get("/status/events", (req, res) => {
12188
12504
  const projectDir = req.projectDir;
12189
- const config = req.projectConfig;
12190
12505
  if (!projectSseClients.has(projectDir)) {
12191
12506
  projectSseClients.set(projectDir, /* @__PURE__ */ new Set());
12192
12507
  }
12193
12508
  const clients = projectSseClients.get(projectDir);
12194
12509
  if (!projectSseWatchers.has(projectDir)) {
12195
- const watcher = startSseStatusWatcher(clients, projectDir, () => req.projectConfig);
12510
+ const watcher = startSseStatusWatcher(clients, projectDir, () => loadConfig(projectDir));
12196
12511
  projectSseWatchers.set(projectDir, watcher);
12197
12512
  }
12198
12513
  res.setHeader("Content-Type", "text/event-stream");
@@ -12200,48 +12515,69 @@ function createProjectSseRoutes(deps) {
12200
12515
  res.setHeader("Connection", "keep-alive");
12201
12516
  res.flushHeaders();
12202
12517
  clients.add(res);
12203
- try {
12204
- const snapshot = fetchStatusSnapshot(projectDir, config);
12518
+ fetchStatusSnapshot(projectDir, loadConfig(projectDir)).then((snapshot) => {
12205
12519
  res.write(`event: status_changed
12206
12520
  data: ${JSON.stringify(snapshot)}
12207
12521
 
12208
12522
  `);
12209
- } catch {
12210
- }
12523
+ }).catch(() => {
12524
+ });
12211
12525
  req.on("close", () => {
12212
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
+ }
12213
12534
  });
12214
12535
  });
12215
- router.get("/status", (req, res) => {
12536
+ router.get("/status", async (req, res) => {
12216
12537
  try {
12217
- const snapshot = fetchStatusSnapshot(req.projectDir, req.projectConfig);
12538
+ const snapshot = await fetchStatusSnapshot(req.projectDir, req.projectConfig);
12218
12539
  res.json(snapshot);
12219
12540
  } catch (error2) {
12220
12541
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12221
12542
  }
12222
12543
  });
12223
- router.get("/schedule-info", (req, res) => {
12544
+ router.get("/schedule-info", async (req, res) => {
12224
12545
  try {
12225
12546
  const config = req.projectConfig;
12226
12547
  const projectDir = req.projectDir;
12227
- const snapshot = fetchStatusSnapshot(projectDir, config);
12548
+ const snapshot = await fetchStatusSnapshot(projectDir, config);
12228
12549
  const installed = snapshot.crontab.installed;
12229
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"));
12230
12556
  res.json({
12231
12557
  executor: {
12232
12558
  schedule: config.cronSchedule,
12233
- installed,
12234
- nextRun: installed ? computeNextRun(config.cronSchedule) : null
12559
+ installed: executorInstalled,
12560
+ nextRun: executorInstalled ? computeNextRun(config.cronSchedule) : null
12235
12561
  },
12236
12562
  reviewer: {
12237
12563
  schedule: config.reviewerSchedule,
12238
- installed: installed && config.reviewerEnabled,
12239
- nextRun: installed && config.reviewerEnabled ? computeNextRun(config.reviewerSchedule) : null
12564
+ installed: reviewerInstalled,
12565
+ nextRun: reviewerInstalled ? computeNextRun(config.reviewerSchedule) : null
12240
12566
  },
12241
12567
  qa: {
12242
12568
  schedule: config.qa.schedule,
12243
- installed: installed && config.qa.enabled,
12244
- 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
12245
12581
  },
12246
12582
  paused: !installed,
12247
12583
  entries
@@ -12316,9 +12652,9 @@ function createApp(projectDir) {
12316
12652
  app.use("/api/roadmap", createRoadmapRoutes({ projectDir, getConfig: () => config, reloadConfig }));
12317
12653
  app.use("/api/logs", createLogRoutes({ projectDir }));
12318
12654
  app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
12319
- app.get("/api/prs", (_req, res) => {
12655
+ app.get("/api/prs", async (_req, res) => {
12320
12656
  try {
12321
- res.json(collectPrInfo(projectDir, config.branchPatterns));
12657
+ res.json(await collectPrInfo(projectDir, config.branchPatterns));
12322
12658
  } catch (error2) {
12323
12659
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12324
12660
  }
@@ -12362,9 +12698,9 @@ function createProjectRouter() {
12362
12698
  router.use("/agents", createAgentRoutes());
12363
12699
  router.use(createProjectActionRoutes({ projectSseClients }));
12364
12700
  router.use(createProjectRoadmapRoutes());
12365
- router.get("/prs", (req, res) => {
12701
+ router.get("/prs", async (req, res) => {
12366
12702
  try {
12367
- res.json(collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
12703
+ res.json(await collectPrInfo(req.projectDir, req.projectConfig.branchPatterns));
12368
12704
  } catch (error2) {
12369
12705
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
12370
12706
  }
@@ -12754,7 +13090,7 @@ function prsCommand(program2) {
12754
13090
  }
12755
13091
  const projectDir = process.cwd();
12756
13092
  const config = loadConfig(projectDir);
12757
- const prs = collectPrInfo(projectDir, config.branchPatterns);
13093
+ const prs = await collectPrInfo(projectDir, config.branchPatterns);
12758
13094
  if (options.json) {
12759
13095
  const output = {
12760
13096
  prs,
@@ -12801,7 +13137,7 @@ function prsCommand(program2) {
12801
13137
  init_dist();
12802
13138
  function getOpenPrBranches(projectDir) {
12803
13139
  try {
12804
- execSync7("git rev-parse --git-dir", {
13140
+ execSync6("git rev-parse --git-dir", {
12805
13141
  cwd: projectDir,
12806
13142
  encoding: "utf-8",
12807
13143
  stdio: ["pipe", "pipe", "pipe"]
@@ -12810,12 +13146,12 @@ function getOpenPrBranches(projectDir) {
12810
13146
  return /* @__PURE__ */ new Set();
12811
13147
  }
12812
13148
  try {
12813
- execSync7("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
13149
+ execSync6("which gh", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
12814
13150
  } catch {
12815
13151
  return /* @__PURE__ */ new Set();
12816
13152
  }
12817
13153
  try {
12818
- const output = execSync7("gh pr list --state open --json headRefName", {
13154
+ const output = execSync6("gh pr list --state open --json headRefName", {
12819
13155
  cwd: projectDir,
12820
13156
  encoding: "utf-8",
12821
13157
  stdio: ["pipe", "pipe", "pipe"]
@@ -13116,6 +13452,36 @@ function cancelCommand(program2) {
13116
13452
  });
13117
13453
  }
13118
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
+ }
13119
13485
  function buildEnvVars5(config, options) {
13120
13486
  const env = {};
13121
13487
  const slicerProvider = resolveJobProvider(config, "slicer");
@@ -13126,6 +13492,12 @@ function buildEnvVars5(config, options) {
13126
13492
  if (config.providerEnv) {
13127
13493
  Object.assign(env, config.providerEnv);
13128
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
+ }
13129
13501
  if (options.dryRun) {
13130
13502
  env.NW_DRY_RUN = "1";
13131
13503
  }
@@ -13149,13 +13521,20 @@ function applyCliOverrides4(config, options) {
13149
13521
  return overridden;
13150
13522
  }
13151
13523
  function sliceCommand(program2) {
13152
- 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) => {
13153
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);
13154
13533
  let config = loadConfig(projectDir);
13155
13534
  config = applyCliOverrides4(config, options);
13156
13535
  const envVars = buildEnvVars5(config, options);
13157
13536
  if (options.dryRun) {
13158
- header("Dry Run: Roadmap Slicer");
13537
+ header("Dry Run: Planner");
13159
13538
  const slicerProvider = resolveJobProvider(config, "slicer");
13160
13539
  header("Configuration");
13161
13540
  const configTable = createTable({ head: ["Setting", "Value"] });
@@ -13164,10 +13543,10 @@ function sliceCommand(program2) {
13164
13543
  configTable.push(["PRD Directory", config.prdDir]);
13165
13544
  configTable.push(["Roadmap Path", config.roadmapScanner.roadmapPath]);
13166
13545
  configTable.push([
13167
- "Slicer Max Runtime",
13546
+ "Planner Max Runtime",
13168
13547
  `${config.roadmapScanner.slicerMaxRuntime}s (${Math.floor(config.roadmapScanner.slicerMaxRuntime / 60)}min)`
13169
13548
  ]);
13170
- configTable.push(["Slicer Schedule", config.roadmapScanner.slicerSchedule]);
13549
+ configTable.push(["Planner Schedule", config.roadmapScanner.slicerSchedule]);
13171
13550
  configTable.push(["Scanner Enabled", config.roadmapScanner.enabled ? "Yes" : "No"]);
13172
13551
  console.log(configTable.toString());
13173
13552
  header("Roadmap Status");
@@ -13211,38 +13590,51 @@ function sliceCommand(program2) {
13211
13590
  process.exit(0);
13212
13591
  }
13213
13592
  if (!config.roadmapScanner.enabled) {
13214
- error("Roadmap scanner is disabled. Enable it in night-watch.config.json to use the slicer.");
13215
- process.exit(1);
13593
+ info("Planner is disabled in config; skipping run.");
13594
+ process.exit(0);
13216
13595
  }
13217
- const spinner = createSpinner("Running roadmap slicer...");
13596
+ const spinner = createSpinner("Running Planner...");
13218
13597
  spinner.start();
13219
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
+ }
13220
13607
  const result = await sliceNextItem(projectDir, config);
13221
13608
  if (result.sliced) {
13222
- spinner.succeed(`Slicer completed successfully: Created ${result.file}`);
13609
+ spinner.succeed(`Planner completed successfully: Created ${result.file}`);
13223
13610
  } else if (result.error) {
13224
13611
  if (result.error === "No pending items to process") {
13225
13612
  spinner.succeed("No pending items to process");
13226
13613
  } else {
13227
- spinner.fail(`Slicer failed: ${result.error}`);
13614
+ spinner.fail(`Planner failed: ${result.error}`);
13228
13615
  }
13229
13616
  }
13230
13617
  const nothingPending = result.error === "No pending items to process";
13231
13618
  const exitCode = result.sliced || nothingPending ? 0 : 1;
13232
13619
  if (!options.dryRun && result.sliced) {
13233
- const event = "run_succeeded";
13234
- const _sliceCtx = {
13235
- event,
13620
+ await sendNotifications(config, {
13621
+ event: "run_succeeded",
13236
13622
  projectName: path31.basename(projectDir),
13237
13623
  exitCode,
13238
13624
  provider: config.provider,
13239
13625
  prTitle: result.item?.title
13240
- };
13241
- 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
+ });
13242
13634
  }
13243
13635
  process.exit(exitCode);
13244
13636
  } catch (err) {
13245
- spinner.fail("Failed to execute slice command");
13637
+ spinner.fail("Failed to execute planner command");
13246
13638
  error(`${err instanceof Error ? err.message : String(err)}`);
13247
13639
  process.exit(1);
13248
13640
  }
@@ -13347,7 +13739,7 @@ async function confirmPrompt(question) {
13347
13739
  }
13348
13740
  async function createGitHubLabel(label2, cwd) {
13349
13741
  try {
13350
- 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"] });
13351
13743
  return { created: true, skipped: false };
13352
13744
  } catch (err) {
13353
13745
  const output = err instanceof Error ? err.message : String(err);
@@ -13487,11 +13879,11 @@ function boardCommand(program2) {
13487
13879
  let body = options.body ?? "";
13488
13880
  if (options.bodyFile) {
13489
13881
  const filePath = options.bodyFile;
13490
- if (!fs31.existsSync(filePath)) {
13882
+ if (!fs32.existsSync(filePath)) {
13491
13883
  console.error(`File not found: ${filePath}`);
13492
13884
  process.exit(1);
13493
13885
  }
13494
- body = fs31.readFileSync(filePath, "utf-8");
13886
+ body = fs32.readFileSync(filePath, "utf-8");
13495
13887
  }
13496
13888
  const labels = [];
13497
13889
  if (options.label) {
@@ -13700,11 +14092,11 @@ function boardCommand(program2) {
13700
14092
  const provider = getProvider(config, cwd);
13701
14093
  await ensureBoardConfigured(config, cwd, provider);
13702
14094
  const roadmapPath = options.roadmap ?? path33.join(cwd, "ROADMAP.md");
13703
- if (!fs31.existsSync(roadmapPath)) {
14095
+ if (!fs32.existsSync(roadmapPath)) {
13704
14096
  console.error(`Roadmap file not found: ${roadmapPath}`);
13705
14097
  process.exit(1);
13706
14098
  }
13707
- const roadmapContent = fs31.readFileSync(roadmapPath, "utf-8");
14099
+ const roadmapContent = fs32.readFileSync(roadmapPath, "utf-8");
13708
14100
  const items = parseRoadmap(roadmapContent);
13709
14101
  const uncheckedItems = getUncheckedItems(items);
13710
14102
  if (uncheckedItems.length === 0) {
@@ -13798,7 +14190,7 @@ function boardCommand(program2) {
13798
14190
  try {
13799
14191
  const labelsToAdd = [category, horizon].filter((l) => !issue.labels.includes(l));
13800
14192
  if (labelsToAdd.length > 0) {
13801
- 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"] });
13802
14194
  }
13803
14195
  updated++;
13804
14196
  success(`Updated labels on #${issue.number}: ${item.title}`);
@@ -13822,14 +14214,14 @@ var __dirname4 = dirname8(__filename3);
13822
14214
  function findPackageRoot(dir) {
13823
14215
  let d = dir;
13824
14216
  for (let i = 0; i < 5; i++) {
13825
- if (existsSync25(join30(d, "package.json")))
14217
+ if (existsSync26(join30(d, "package.json")))
13826
14218
  return d;
13827
14219
  d = dirname8(d);
13828
14220
  }
13829
14221
  return dir;
13830
14222
  }
13831
14223
  var packageRoot = findPackageRoot(__dirname4);
13832
- var packageJson = JSON.parse(readFileSync15(join30(packageRoot, "package.json"), "utf-8"));
14224
+ var packageJson = JSON.parse(readFileSync16(join30(packageRoot, "package.json"), "utf-8"));
13833
14225
  var program = new Command2();
13834
14226
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
13835
14227
  initCommand(program);