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

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