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