@jonit-dev/night-watch-cli 1.8.3 → 1.8.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -17,6 +17,283 @@ var init_types = __esm({
17
17
  }
18
18
  });
19
19
 
20
+ // ../core/dist/jobs/job-registry.js
21
+ function getJobDef(id) {
22
+ return JOB_MAP.get(id);
23
+ }
24
+ function getAllJobDefs() {
25
+ return [...JOB_REGISTRY];
26
+ }
27
+ function getJobDefByCommand(command) {
28
+ return JOB_REGISTRY.find((job) => job.cliCommand === command);
29
+ }
30
+ function getJobDefByLogName(logName) {
31
+ return JOB_REGISTRY.find((job) => job.logName === logName);
32
+ }
33
+ function getValidJobTypes() {
34
+ return JOB_REGISTRY.map((job) => job.id);
35
+ }
36
+ function getDefaultQueuePriority() {
37
+ const result = {};
38
+ for (const job of JOB_REGISTRY) {
39
+ result[job.id] = job.queuePriority;
40
+ }
41
+ return result;
42
+ }
43
+ function getLogFileNames() {
44
+ const result = {};
45
+ for (const job of JOB_REGISTRY) {
46
+ result[job.id] = job.logName;
47
+ if (job.cliCommand !== job.id) {
48
+ result[job.cliCommand] = job.logName;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ function getLockSuffix(jobId) {
54
+ return getJobDef(jobId)?.lockSuffix ?? ".lock";
55
+ }
56
+ function normalizeJobConfig(raw, jobDef) {
57
+ const readBoolean = (v) => typeof v === "boolean" ? v : void 0;
58
+ const readString = (v) => typeof v === "string" ? v : void 0;
59
+ const readNumber = (v) => typeof v === "number" && !Number.isNaN(v) ? v : void 0;
60
+ const readStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
61
+ const defaults = jobDef.defaultConfig;
62
+ const result = {
63
+ enabled: readBoolean(raw.enabled) ?? defaults.enabled,
64
+ schedule: readString(raw.schedule) ?? defaults.schedule,
65
+ maxRuntime: readNumber(raw.maxRuntime) ?? defaults.maxRuntime
66
+ };
67
+ for (const field of jobDef.extraFields ?? []) {
68
+ switch (field.type) {
69
+ case "boolean":
70
+ result[field.name] = readBoolean(raw[field.name]) ?? field.defaultValue;
71
+ break;
72
+ case "string":
73
+ result[field.name] = readString(raw[field.name]) ?? field.defaultValue;
74
+ break;
75
+ case "number":
76
+ result[field.name] = readNumber(raw[field.name]) ?? field.defaultValue;
77
+ break;
78
+ case "string[]":
79
+ result[field.name] = readStringArray(raw[field.name]) ?? field.defaultValue;
80
+ break;
81
+ case "enum": {
82
+ const val = readString(raw[field.name]);
83
+ result[field.name] = val && field.enumValues?.includes(val) ? val : field.defaultValue;
84
+ break;
85
+ }
86
+ }
87
+ }
88
+ return result;
89
+ }
90
+ function camelToUpperSnake(name) {
91
+ return name.replace(/([A-Z])/g, "_$1").toUpperCase();
92
+ }
93
+ function buildJobEnvOverrides(envPrefix, currentBase, extraFields) {
94
+ const parseBoolean2 = (value) => {
95
+ const v = value.toLowerCase().trim();
96
+ if (v === "true" || v === "1")
97
+ return true;
98
+ if (v === "false" || v === "0")
99
+ return false;
100
+ return null;
101
+ };
102
+ const result = { ...currentBase };
103
+ let changed = false;
104
+ const enabledVal = process.env[`${envPrefix}_ENABLED`];
105
+ if (enabledVal) {
106
+ const v = parseBoolean2(enabledVal);
107
+ if (v !== null) {
108
+ result.enabled = v;
109
+ changed = true;
110
+ }
111
+ }
112
+ const scheduleVal = process.env[`${envPrefix}_SCHEDULE`];
113
+ if (scheduleVal) {
114
+ result.schedule = scheduleVal;
115
+ changed = true;
116
+ }
117
+ const maxRuntimeVal = process.env[`${envPrefix}_MAX_RUNTIME`];
118
+ if (maxRuntimeVal) {
119
+ const v = parseInt(maxRuntimeVal, 10);
120
+ if (!isNaN(v) && v > 0) {
121
+ result.maxRuntime = v;
122
+ changed = true;
123
+ }
124
+ }
125
+ for (const field of extraFields ?? []) {
126
+ const envKey = `${envPrefix}_${camelToUpperSnake(field.name)}`;
127
+ const envVal = process.env[envKey];
128
+ if (!envVal)
129
+ continue;
130
+ switch (field.type) {
131
+ case "boolean": {
132
+ const v = parseBoolean2(envVal);
133
+ if (v !== null) {
134
+ result[field.name] = v;
135
+ changed = true;
136
+ }
137
+ break;
138
+ }
139
+ case "string":
140
+ result[field.name] = envVal;
141
+ changed = true;
142
+ break;
143
+ case "number": {
144
+ const v = parseInt(envVal, 10);
145
+ if (!isNaN(v) && v > 0) {
146
+ result[field.name] = v;
147
+ changed = true;
148
+ }
149
+ break;
150
+ }
151
+ case "string[]": {
152
+ const patterns = envVal.split(",").map((s) => s.trim()).filter(Boolean);
153
+ if (patterns.length > 0) {
154
+ result[field.name] = patterns;
155
+ changed = true;
156
+ }
157
+ break;
158
+ }
159
+ case "enum":
160
+ if (field.enumValues?.includes(envVal)) {
161
+ result[field.name] = envVal;
162
+ changed = true;
163
+ }
164
+ break;
165
+ }
166
+ }
167
+ return changed ? result : null;
168
+ }
169
+ var JOB_REGISTRY, JOB_MAP;
170
+ var init_job_registry = __esm({
171
+ "../core/dist/jobs/job-registry.js"() {
172
+ "use strict";
173
+ JOB_REGISTRY = [
174
+ {
175
+ id: "executor",
176
+ name: "Executor",
177
+ description: "Creates implementation PRs from PRDs",
178
+ cliCommand: "run",
179
+ logName: "executor",
180
+ lockSuffix: ".lock",
181
+ queuePriority: 50,
182
+ envPrefix: "NW_EXECUTOR",
183
+ defaultConfig: {
184
+ enabled: true,
185
+ schedule: "5 */2 * * *",
186
+ maxRuntime: 7200
187
+ }
188
+ },
189
+ {
190
+ id: "reviewer",
191
+ name: "Reviewer",
192
+ description: "Reviews and improves PRs on night-watch branches",
193
+ cliCommand: "review",
194
+ logName: "reviewer",
195
+ lockSuffix: "-r.lock",
196
+ queuePriority: 40,
197
+ envPrefix: "NW_REVIEWER",
198
+ defaultConfig: {
199
+ enabled: true,
200
+ schedule: "25 */3 * * *",
201
+ maxRuntime: 3600
202
+ }
203
+ },
204
+ {
205
+ id: "slicer",
206
+ name: "Slicer",
207
+ description: "Generates PRDs from roadmap items",
208
+ cliCommand: "planner",
209
+ logName: "slicer",
210
+ lockSuffix: "-slicer.lock",
211
+ queuePriority: 30,
212
+ envPrefix: "NW_SLICER",
213
+ defaultConfig: {
214
+ enabled: true,
215
+ schedule: "35 */6 * * *",
216
+ maxRuntime: 600
217
+ }
218
+ },
219
+ {
220
+ id: "qa",
221
+ name: "QA",
222
+ description: "Runs end-to-end tests on PRs",
223
+ cliCommand: "qa",
224
+ logName: "night-watch-qa",
225
+ lockSuffix: "-qa.lock",
226
+ queuePriority: 20,
227
+ envPrefix: "NW_QA",
228
+ extraFields: [
229
+ { name: "branchPatterns", type: "string[]", defaultValue: [] },
230
+ {
231
+ name: "artifacts",
232
+ type: "enum",
233
+ enumValues: ["screenshot", "video", "both"],
234
+ defaultValue: "both"
235
+ },
236
+ { name: "skipLabel", type: "string", defaultValue: "skip-qa" },
237
+ { name: "autoInstallPlaywright", type: "boolean", defaultValue: true }
238
+ ],
239
+ defaultConfig: {
240
+ enabled: true,
241
+ schedule: "45 2,10,18 * * *",
242
+ maxRuntime: 3600,
243
+ branchPatterns: [],
244
+ artifacts: "both",
245
+ skipLabel: "skip-qa",
246
+ autoInstallPlaywright: true
247
+ }
248
+ },
249
+ {
250
+ id: "audit",
251
+ name: "Auditor",
252
+ description: "Performs code audits and creates issues for findings",
253
+ cliCommand: "audit",
254
+ logName: "audit",
255
+ lockSuffix: "-audit.lock",
256
+ queuePriority: 10,
257
+ envPrefix: "NW_AUDIT",
258
+ defaultConfig: {
259
+ enabled: true,
260
+ schedule: "50 3 * * 1",
261
+ maxRuntime: 1800
262
+ }
263
+ },
264
+ {
265
+ id: "analytics",
266
+ name: "Analytics",
267
+ description: "Analyzes product analytics and creates issues for trends",
268
+ cliCommand: "analytics",
269
+ logName: "analytics",
270
+ lockSuffix: "-analytics.lock",
271
+ queuePriority: 10,
272
+ envPrefix: "NW_ANALYTICS",
273
+ extraFields: [
274
+ { name: "lookbackDays", type: "number", defaultValue: 7 },
275
+ {
276
+ name: "targetColumn",
277
+ type: "enum",
278
+ enumValues: ["Draft", "Ready", "In Progress", "Done", "Closed"],
279
+ defaultValue: "Draft"
280
+ },
281
+ { name: "analysisPrompt", type: "string", defaultValue: "" }
282
+ ],
283
+ defaultConfig: {
284
+ enabled: false,
285
+ schedule: "0 6 * * 1",
286
+ maxRuntime: 900,
287
+ lookbackDays: 7,
288
+ targetColumn: "Draft",
289
+ analysisPrompt: ""
290
+ }
291
+ }
292
+ ];
293
+ JOB_MAP = new Map(JOB_REGISTRY.map((job) => [job.id, job]));
294
+ }
295
+ });
296
+
20
297
  // ../core/dist/constants.js
21
298
  function resolveProviderBucketKey(provider, providerEnv) {
22
299
  if (provider === "codex")
@@ -35,6 +312,7 @@ var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEW
35
312
  var init_constants = __esm({
36
313
  "../core/dist/constants.js"() {
37
314
  "use strict";
315
+ init_job_registry();
38
316
  DEFAULT_DEFAULT_BRANCH = "";
39
317
  DEFAULT_PRD_DIR = "docs/prds";
40
318
  DEFAULT_MAX_RUNTIME = 7200;
@@ -131,14 +409,7 @@ If no issues are warranted, output an empty array: []`;
131
409
  PLANNER_LOG_NAME = "slicer";
132
410
  ANALYTICS_LOG_NAME = "analytics";
133
411
  VALID_PROVIDERS = ["claude", "codex"];
134
- VALID_JOB_TYPES = [
135
- "executor",
136
- "reviewer",
137
- "qa",
138
- "audit",
139
- "slicer",
140
- "analytics"
141
- ];
412
+ VALID_JOB_TYPES = getValidJobTypes();
142
413
  DEFAULT_JOB_PROVIDERS = {};
143
414
  BUILT_IN_PRESETS = {
144
415
  claude: {
@@ -212,14 +483,7 @@ If no issues are warranted, output an empty array: []`;
212
483
  REVIEWER_LOG_NAME = "reviewer";
213
484
  EXECUTOR_LOG_FILE = "executor.log";
214
485
  REVIEWER_LOG_FILE = "reviewer.log";
215
- LOG_FILE_NAMES = {
216
- executor: EXECUTOR_LOG_NAME,
217
- reviewer: REVIEWER_LOG_NAME,
218
- qa: QA_LOG_NAME,
219
- audit: AUDIT_LOG_NAME,
220
- planner: PLANNER_LOG_NAME,
221
- analytics: ANALYTICS_LOG_NAME
222
- };
486
+ LOG_FILE_NAMES = getLogFileNames();
223
487
  GLOBAL_CONFIG_DIR = ".night-watch";
224
488
  REGISTRY_FILE_NAME = "projects.json";
225
489
  HISTORY_FILE_NAME = "history.json";
@@ -231,14 +495,7 @@ If no issues are warranted, output an empty array: []`;
231
495
  DEFAULT_QUEUE_MODE = "conservative";
232
496
  DEFAULT_QUEUE_MAX_CONCURRENCY = 1;
233
497
  DEFAULT_QUEUE_MAX_WAIT_TIME = 7200;
234
- DEFAULT_QUEUE_PRIORITY = {
235
- executor: 50,
236
- reviewer: 40,
237
- slicer: 30,
238
- qa: 20,
239
- audit: 10,
240
- analytics: 10
241
- };
498
+ DEFAULT_QUEUE_PRIORITY = getDefaultQueuePriority();
242
499
  DEFAULT_QUEUE = {
243
500
  enabled: DEFAULT_QUEUE_ENABLED,
244
501
  mode: DEFAULT_QUEUE_MODE,
@@ -252,15 +509,6 @@ If no issues are warranted, output an empty array: []`;
252
509
  }
253
510
  });
254
511
 
255
- // ../core/dist/board/types.js
256
- var BOARD_COLUMNS;
257
- var init_types2 = __esm({
258
- "../core/dist/board/types.js"() {
259
- "use strict";
260
- BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
261
- }
262
- });
263
-
264
512
  // ../core/dist/config-normalize.js
265
513
  function validateProvider(value) {
266
514
  const trimmed = value.trim();
@@ -432,43 +680,14 @@ function normalizeConfig(rawConfig) {
432
680
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
433
681
  normalized.autoMergeMethod = mergeMethod;
434
682
  }
435
- const rawQa = readObject(rawConfig.qa);
436
- if (rawQa) {
437
- const artifactsValue = readString(rawQa.artifacts);
438
- const artifacts = artifactsValue && ["screenshot", "video", "both"].includes(artifactsValue) ? artifactsValue : DEFAULT_QA.artifacts;
439
- const qa = {
440
- enabled: readBoolean(rawQa.enabled) ?? DEFAULT_QA.enabled,
441
- schedule: readString(rawQa.schedule) ?? DEFAULT_QA.schedule,
442
- maxRuntime: readNumber(rawQa.maxRuntime) ?? DEFAULT_QA.maxRuntime,
443
- branchPatterns: readStringArray(rawQa.branchPatterns) ?? DEFAULT_QA.branchPatterns,
444
- artifacts,
445
- skipLabel: readString(rawQa.skipLabel) ?? DEFAULT_QA.skipLabel,
446
- autoInstallPlaywright: readBoolean(rawQa.autoInstallPlaywright) ?? DEFAULT_QA.autoInstallPlaywright
447
- };
448
- normalized.qa = qa;
449
- }
450
- const rawAudit = readObject(rawConfig.audit);
451
- if (rawAudit) {
452
- const audit = {
453
- enabled: readBoolean(rawAudit.enabled) ?? DEFAULT_AUDIT.enabled,
454
- schedule: readString(rawAudit.schedule) ?? DEFAULT_AUDIT.schedule,
455
- maxRuntime: readNumber(rawAudit.maxRuntime) ?? DEFAULT_AUDIT.maxRuntime
456
- };
457
- normalized.audit = audit;
458
- }
459
- const rawAnalytics = readObject(rawConfig.analytics);
460
- if (rawAnalytics) {
461
- const targetColumnRaw = readString(rawAnalytics.targetColumn);
462
- const targetColumn = targetColumnRaw && BOARD_COLUMNS.includes(targetColumnRaw) ? targetColumnRaw : DEFAULT_ANALYTICS.targetColumn;
463
- const analytics = {
464
- enabled: readBoolean(rawAnalytics.enabled) ?? DEFAULT_ANALYTICS.enabled,
465
- schedule: readString(rawAnalytics.schedule) ?? DEFAULT_ANALYTICS.schedule,
466
- maxRuntime: readNumber(rawAnalytics.maxRuntime) ?? DEFAULT_ANALYTICS.maxRuntime,
467
- lookbackDays: readNumber(rawAnalytics.lookbackDays) ?? DEFAULT_ANALYTICS.lookbackDays,
468
- targetColumn,
469
- analysisPrompt: readString(rawAnalytics.analysisPrompt) ?? DEFAULT_ANALYTICS.analysisPrompt
470
- };
471
- normalized.analytics = analytics;
683
+ for (const jobId of ["qa", "audit", "analytics"]) {
684
+ const jobDef = getJobDef(jobId);
685
+ if (!jobDef)
686
+ continue;
687
+ const rawJob = readObject(rawConfig[jobId]);
688
+ if (rawJob) {
689
+ normalized[jobId] = normalizeJobConfig(rawJob, jobDef);
690
+ }
472
691
  }
473
692
  const rawJobProviders = readObject(rawConfig.jobProviders);
474
693
  if (rawJobProviders) {
@@ -529,8 +748,8 @@ function normalizeConfig(rawConfig) {
529
748
  var init_config_normalize = __esm({
530
749
  "../core/dist/config-normalize.js"() {
531
750
  "use strict";
532
- init_types2();
533
751
  init_constants();
752
+ init_job_registry();
534
753
  }
535
754
  });
536
755
 
@@ -706,72 +925,16 @@ function buildEnvOverrideConfig(fileConfig) {
706
925
  env.claudeModel = model;
707
926
  }
708
927
  }
709
- const qaBase = () => env.qa ?? fileConfig?.qa ?? DEFAULT_QA;
710
- if (process.env.NW_QA_ENABLED) {
711
- const v = parseBoolean(process.env.NW_QA_ENABLED);
712
- if (v !== null)
713
- env.qa = { ...qaBase(), enabled: v };
714
- }
715
- if (process.env.NW_QA_SCHEDULE) {
716
- env.qa = { ...qaBase(), schedule: process.env.NW_QA_SCHEDULE };
717
- }
718
- if (process.env.NW_QA_MAX_RUNTIME) {
719
- const v = parseInt(process.env.NW_QA_MAX_RUNTIME, 10);
720
- if (!isNaN(v) && v > 0)
721
- env.qa = { ...qaBase(), maxRuntime: v };
722
- }
723
- if (process.env.NW_QA_ARTIFACTS) {
724
- const a = process.env.NW_QA_ARTIFACTS;
725
- if (["screenshot", "video", "both"].includes(a)) {
726
- env.qa = { ...qaBase(), artifacts: a };
928
+ for (const jobId of ["qa", "audit", "analytics"]) {
929
+ const jobDef = getJobDef(jobId);
930
+ if (!jobDef)
931
+ continue;
932
+ const currentBase = env[jobId] ?? fileConfig?.[jobId] ?? jobDef.defaultConfig;
933
+ const overrides = buildJobEnvOverrides(jobDef.envPrefix, currentBase, jobDef.extraFields);
934
+ if (overrides) {
935
+ env[jobId] = overrides;
727
936
  }
728
937
  }
729
- if (process.env.NW_QA_SKIP_LABEL) {
730
- env.qa = { ...qaBase(), skipLabel: process.env.NW_QA_SKIP_LABEL };
731
- }
732
- if (process.env.NW_QA_AUTO_INSTALL_PLAYWRIGHT) {
733
- const v = parseBoolean(process.env.NW_QA_AUTO_INSTALL_PLAYWRIGHT);
734
- if (v !== null)
735
- env.qa = { ...qaBase(), autoInstallPlaywright: v };
736
- }
737
- if (process.env.NW_QA_BRANCH_PATTERNS) {
738
- const patterns = process.env.NW_QA_BRANCH_PATTERNS.split(",").map((s) => s.trim()).filter(Boolean);
739
- if (patterns.length > 0)
740
- env.qa = { ...qaBase(), branchPatterns: patterns };
741
- }
742
- const auditBase = () => env.audit ?? fileConfig?.audit ?? DEFAULT_AUDIT;
743
- if (process.env.NW_AUDIT_ENABLED) {
744
- const v = parseBoolean(process.env.NW_AUDIT_ENABLED);
745
- if (v !== null)
746
- env.audit = { ...auditBase(), enabled: v };
747
- }
748
- if (process.env.NW_AUDIT_SCHEDULE) {
749
- env.audit = { ...auditBase(), schedule: process.env.NW_AUDIT_SCHEDULE };
750
- }
751
- if (process.env.NW_AUDIT_MAX_RUNTIME) {
752
- const v = parseInt(process.env.NW_AUDIT_MAX_RUNTIME, 10);
753
- if (!isNaN(v) && v > 0)
754
- env.audit = { ...auditBase(), maxRuntime: v };
755
- }
756
- const analyticsBase = () => env.analytics ?? fileConfig?.analytics ?? DEFAULT_ANALYTICS;
757
- if (process.env.NW_ANALYTICS_ENABLED) {
758
- const v = parseBoolean(process.env.NW_ANALYTICS_ENABLED);
759
- if (v !== null)
760
- env.analytics = { ...analyticsBase(), enabled: v };
761
- }
762
- if (process.env.NW_ANALYTICS_SCHEDULE) {
763
- env.analytics = { ...analyticsBase(), schedule: process.env.NW_ANALYTICS_SCHEDULE };
764
- }
765
- if (process.env.NW_ANALYTICS_MAX_RUNTIME) {
766
- const v = parseInt(process.env.NW_ANALYTICS_MAX_RUNTIME, 10);
767
- if (!isNaN(v) && v > 0)
768
- env.analytics = { ...analyticsBase(), maxRuntime: v };
769
- }
770
- if (process.env.NW_ANALYTICS_LOOKBACK_DAYS) {
771
- const v = parseInt(process.env.NW_ANALYTICS_LOOKBACK_DAYS, 10);
772
- if (!isNaN(v) && v > 0)
773
- env.analytics = { ...analyticsBase(), lookbackDays: v };
774
- }
775
938
  const jobProvidersEnv = {};
776
939
  for (const jobType of VALID_JOB_TYPES) {
777
940
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -821,6 +984,7 @@ var init_config_env = __esm({
821
984
  "use strict";
822
985
  init_constants();
823
986
  init_config_normalize();
987
+ init_job_registry();
824
988
  }
825
989
  });
826
990
 
@@ -1016,6 +1180,15 @@ var init_config = __esm({
1016
1180
  }
1017
1181
  });
1018
1182
 
1183
+ // ../core/dist/board/types.js
1184
+ var BOARD_COLUMNS;
1185
+ var init_types2 = __esm({
1186
+ "../core/dist/board/types.js"() {
1187
+ "use strict";
1188
+ BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
1189
+ }
1190
+ });
1191
+
1019
1192
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
1020
1193
  import Database from "better-sqlite3";
1021
1194
  import { inject, injectable } from "tsyringe";
@@ -4921,6 +5094,26 @@ function validateRegistry() {
4921
5094
  }
4922
5095
  return { valid, invalid };
4923
5096
  }
5097
+ function pruneProjectData(projectDir) {
5098
+ const resolvedPath = path12.resolve(projectDir);
5099
+ const db = getDb();
5100
+ db.transaction(() => {
5101
+ db.prepare("DELETE FROM execution_history WHERE project_path = ?").run(resolvedPath);
5102
+ db.prepare("DELETE FROM prd_states WHERE project_path = ?").run(resolvedPath);
5103
+ db.prepare("DELETE FROM job_queue WHERE project_path = ?").run(resolvedPath);
5104
+ db.prepare("DELETE FROM job_runs WHERE project_path = ?").run(resolvedPath);
5105
+ db.prepare("DELETE FROM roadmap_states WHERE prd_dir LIKE ?").run(`${resolvedPath}%`);
5106
+ })();
5107
+ }
5108
+ function removeProject(projectDir) {
5109
+ const resolvedPath = path12.resolve(projectDir);
5110
+ const projectName = getProjectName(resolvedPath);
5111
+ const marker = generateMarker(projectName);
5112
+ const cronEntriesRemoved = removeEntriesForProject(resolvedPath, marker);
5113
+ pruneProjectData(resolvedPath);
5114
+ const unregistered = unregisterProject(resolvedPath);
5115
+ return { cronEntriesRemoved, unregistered, dataPruned: true };
5116
+ }
4924
5117
  var init_registry = __esm({
4925
5118
  "../core/dist/utils/registry.js"() {
4926
5119
  "use strict";
@@ -4928,6 +5121,7 @@ var init_registry = __esm({
4928
5121
  init_repositories();
4929
5122
  init_client();
4930
5123
  init_client();
5124
+ init_crontab();
4931
5125
  init_status_data();
4932
5126
  }
4933
5127
  });
@@ -6986,6 +7180,14 @@ sequenceDiagram
6986
7180
  }
6987
7181
  });
6988
7182
 
7183
+ // ../core/dist/jobs/index.js
7184
+ var init_jobs = __esm({
7185
+ "../core/dist/jobs/index.js"() {
7186
+ "use strict";
7187
+ init_job_registry();
7188
+ }
7189
+ });
7190
+
6989
7191
  // ../core/dist/index.js
6990
7192
  var dist_exports = {};
6991
7193
  __export(dist_exports, {
@@ -7067,6 +7269,7 @@ __export(dist_exports, {
7067
7269
  HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
7068
7270
  HORIZON_LABELS: () => HORIZON_LABELS,
7069
7271
  HORIZON_LABEL_INFO: () => HORIZON_LABEL_INFO,
7272
+ JOB_REGISTRY: () => JOB_REGISTRY,
7070
7273
  LOCK_FILE_PREFIX: () => LOCK_FILE_PREFIX,
7071
7274
  LOG_DIR: () => LOG_DIR,
7072
7275
  LOG_FILE_NAMES: () => LOG_FILE_NAMES,
@@ -7098,7 +7301,9 @@ __export(dist_exports, {
7098
7301
  analyticsLockPath: () => analyticsLockPath,
7099
7302
  auditLockPath: () => auditLockPath,
7100
7303
  buildDescription: () => buildDescription,
7304
+ buildJobEnvOverrides: () => buildJobEnvOverrides,
7101
7305
  calculateStringSimilarity: () => calculateStringSimilarity,
7306
+ camelToUpperSnake: () => camelToUpperSnake,
7102
7307
  canStartJob: () => canStartJob,
7103
7308
  cancelProcess: () => cancelProcess,
7104
7309
  checkConfigFile: () => checkConfigFile,
@@ -7163,23 +7368,30 @@ __export(dist_exports, {
7163
7368
  formatTelegramPayload: () => formatTelegramPayload,
7164
7369
  generateItemHash: () => generateItemHash,
7165
7370
  generateMarker: () => generateMarker,
7371
+ getAllJobDefs: () => getAllJobDefs,
7166
7372
  getBranchTipTimestamp: () => getBranchTipTimestamp,
7167
7373
  getCrontabInfo: () => getCrontabInfo,
7168
7374
  getDb: () => getDb,
7169
7375
  getDbPath: () => getDbPath,
7170
7376
  getDefaultConfig: () => getDefaultConfig,
7377
+ getDefaultQueuePriority: () => getDefaultQueuePriority,
7171
7378
  getEntries: () => getEntries,
7172
7379
  getEventColor: () => getEventColor,
7173
7380
  getEventEmoji: () => getEventEmoji,
7174
7381
  getEventTitle: () => getEventTitle,
7175
7382
  getHistoryPath: () => getHistoryPath,
7176
7383
  getInFlightCount: () => getInFlightCount,
7384
+ getJobDef: () => getJobDef,
7385
+ getJobDefByCommand: () => getJobDefByCommand,
7386
+ getJobDefByLogName: () => getJobDefByLogName,
7177
7387
  getJobPriority: () => getJobPriority,
7178
7388
  getJobRunsAnalytics: () => getJobRunsAnalytics,
7179
7389
  getLabelsForSection: () => getLabelsForSection,
7180
7390
  getLastExecution: () => getLastExecution,
7181
7391
  getLastLogLines: () => getLastLogLines,
7182
7392
  getLockFilePaths: () => getLockFilePaths,
7393
+ getLockSuffix: () => getLockSuffix,
7394
+ getLogFileNames: () => getLogFileNames,
7183
7395
  getLogInfo: () => getLogInfo,
7184
7396
  getNextPendingJob: () => getNextPendingJob,
7185
7397
  getNextPrdNumber: () => getNextPrdNumber,
@@ -7200,6 +7412,7 @@ __export(dist_exports, {
7200
7412
  getStateFilePath: () => getStateFilePath,
7201
7413
  getStateItem: () => getStateItem,
7202
7414
  getUncheckedItems: () => getUncheckedItems,
7415
+ getValidJobTypes: () => getValidJobTypes,
7203
7416
  groupBySection: () => groupBySection2,
7204
7417
  hasEntry: () => hasEntry,
7205
7418
  hasNewItems: () => hasNewItems,
@@ -7226,6 +7439,7 @@ __export(dist_exports, {
7226
7439
  markJobRunning: () => markJobRunning,
7227
7440
  markPrdDone: () => markPrdDone,
7228
7441
  migrateJsonToSqlite: () => migrateJsonToSqlite,
7442
+ normalizeJobConfig: () => normalizeJobConfig,
7229
7443
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
7230
7444
  parsePrdDependencies: () => parsePrdDependencies,
7231
7445
  parseRoadmap: () => parseRoadmap,
@@ -7235,6 +7449,7 @@ __export(dist_exports, {
7235
7449
  prepareBranchWorktree: () => prepareBranchWorktree,
7236
7450
  prepareDetachedWorktree: () => prepareDetachedWorktree,
7237
7451
  projectRuntimeKey: () => projectRuntimeKey,
7452
+ pruneProjectData: () => pruneProjectData,
7238
7453
  qaLockPath: () => qaLockPath,
7239
7454
  readCrontab: () => readCrontab,
7240
7455
  readPrdStates: () => readPrdStates,
@@ -7245,6 +7460,7 @@ __export(dist_exports, {
7245
7460
  removeEntries: () => removeEntries,
7246
7461
  removeEntriesForProject: () => removeEntriesForProject,
7247
7462
  removeJob: () => removeJob,
7463
+ removeProject: () => removeProject,
7248
7464
  renderPrdTemplate: () => renderPrdTemplate,
7249
7465
  renderSlicerPrompt: () => renderSlicerPrompt,
7250
7466
  resetRepositories: () => resetRepositories,
@@ -7329,6 +7545,7 @@ var init_dist = __esm({
7329
7545
  init_analytics();
7330
7546
  init_prd_template();
7331
7547
  init_slicer_prompt();
7548
+ init_jobs();
7332
7549
  }
7333
7550
  });
7334
7551
 
@@ -7361,6 +7578,14 @@ function findTemplatesDir(startDir) {
7361
7578
  return join18(startDir, "templates");
7362
7579
  }
7363
7580
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
7581
+ var NW_SKILLS = [
7582
+ "nw-create-prd",
7583
+ "nw-add-issue",
7584
+ "nw-run",
7585
+ "nw-slice",
7586
+ "nw-board-sync",
7587
+ "nw-review"
7588
+ ];
7364
7589
  function hasPlaywrightDependency(cwd) {
7365
7590
  const packageJsonPath = path20.join(cwd, "package.json");
7366
7591
  if (!fs20.existsSync(packageJsonPath)) {
@@ -7656,12 +7881,67 @@ function addToGitignore(cwd) {
7656
7881
  fs20.writeFileSync(gitignorePath, newContent);
7657
7882
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
7658
7883
  }
7884
+ function installSkills(cwd, provider, force, templatesDir) {
7885
+ const skillsTemplatesDir = path20.join(templatesDir, "skills");
7886
+ if (!fs20.existsSync(skillsTemplatesDir)) {
7887
+ return { location: "", installed: 0, skipped: 0, type: "none" };
7888
+ }
7889
+ const isClaudeProvider = provider === "claude" || provider.startsWith("claude");
7890
+ const isCodexProvider = provider === "codex";
7891
+ const claudeDir = path20.join(cwd, ".claude");
7892
+ if (isClaudeProvider || fs20.existsSync(claudeDir)) {
7893
+ ensureDir(claudeDir);
7894
+ const skillsDir = path20.join(claudeDir, "skills");
7895
+ ensureDir(skillsDir);
7896
+ let installed = 0;
7897
+ let skipped = 0;
7898
+ for (const skillName of NW_SKILLS) {
7899
+ const templateFile = path20.join(skillsTemplatesDir, `${skillName}.md`);
7900
+ if (!fs20.existsSync(templateFile)) continue;
7901
+ const skillDir = path20.join(skillsDir, skillName);
7902
+ ensureDir(skillDir);
7903
+ const target = path20.join(skillDir, "SKILL.md");
7904
+ if (fs20.existsSync(target) && !force) {
7905
+ skipped++;
7906
+ continue;
7907
+ }
7908
+ fs20.copyFileSync(templateFile, target);
7909
+ installed++;
7910
+ }
7911
+ return { location: ".claude/skills/", installed, skipped, type: "claude" };
7912
+ }
7913
+ if (isCodexProvider) {
7914
+ const agentsFile = path20.join(cwd, "AGENTS.md");
7915
+ const blockFile = path20.join(skillsTemplatesDir, "_codex-block.md");
7916
+ if (!fs20.existsSync(blockFile)) {
7917
+ return { location: "", installed: 0, skipped: 0, type: "none" };
7918
+ }
7919
+ const block = fs20.readFileSync(blockFile, "utf-8");
7920
+ const marker = "## Night Watch Skills";
7921
+ if (!fs20.existsSync(agentsFile)) {
7922
+ fs20.writeFileSync(agentsFile, block);
7923
+ return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
7924
+ }
7925
+ const existing = fs20.readFileSync(agentsFile, "utf-8");
7926
+ if (existing.includes(marker)) {
7927
+ if (!force) {
7928
+ return { location: "AGENTS.md", installed: 0, skipped: NW_SKILLS.length, type: "codex" };
7929
+ }
7930
+ const withoutSection = existing.replace(/\n\n## Night Watch Skills[\s\S]*$/, "");
7931
+ fs20.writeFileSync(agentsFile, withoutSection + "\n\n" + block);
7932
+ } else {
7933
+ fs20.appendFileSync(agentsFile, "\n\n" + block);
7934
+ }
7935
+ return { location: "AGENTS.md", installed: NW_SKILLS.length, skipped: 0, type: "codex" };
7936
+ }
7937
+ return { location: "", installed: 0, skipped: 0, type: "none" };
7938
+ }
7659
7939
  function initCommand(program2) {
7660
7940
  program2.command("init").description("Initialize night-watch in the current project").option("-f, --force", "Overwrite existing configuration").option("-d, --prd-dir <path>", "Path to PRD directory").option("-p, --provider <name>", "AI provider to use (claude or codex)").option("--no-reviewer", "Disable reviewer cron job").action(async (options) => {
7661
7941
  const cwd = process.cwd();
7662
7942
  const force = options.force || false;
7663
7943
  const prdDir = options.prdDir || DEFAULT_PRD_DIR;
7664
- const totalSteps = 12;
7944
+ const totalSteps = 13;
7665
7945
  const interactive = isInteractiveInitSession();
7666
7946
  console.log();
7667
7947
  header("Night Watch CLI - Initializing");
@@ -7912,7 +8192,19 @@ function initCommand(program2) {
7912
8192
  ` Warning: Could not register in global registry: ${regErr instanceof Error ? regErr.message : String(regErr)}`
7913
8193
  );
7914
8194
  }
7915
- step(12, totalSteps, "Initialization complete!");
8195
+ step(12, totalSteps, "Installing Night Watch skills...");
8196
+ const skillsResult = installSkills(cwd, selectedProvider, force, TEMPLATES_DIR);
8197
+ if (skillsResult.installed > 0) {
8198
+ success(`Installed ${skillsResult.installed} skills to ${skillsResult.location}`);
8199
+ for (const skillName of NW_SKILLS) {
8200
+ console.log(` /${skillName}`);
8201
+ }
8202
+ } else if (skillsResult.skipped > 0) {
8203
+ info(`Skills already installed (use --force to overwrite)`);
8204
+ } else if (skillsResult.type === "none") {
8205
+ info("No compatible AI skills directory detected \u2014 skipping.");
8206
+ }
8207
+ step(13, totalSteps, "Initialization complete!");
7916
8208
  header("Initialization Complete");
7917
8209
  const filesTable = createTable({ head: ["Created Files", ""] });
7918
8210
  filesTable.push(["PRD Directory", `${prdDir}/done/`]);
@@ -7925,6 +8217,15 @@ function initCommand(program2) {
7925
8217
  filesTable.push(["Config File", CONFIG_FILE_NAME]);
7926
8218
  filesTable.push(["Board Setup", boardSetupStatus]);
7927
8219
  filesTable.push(["Global Registry", "~/.night-watch/projects.json"]);
8220
+ let skillsSummary;
8221
+ if (skillsResult.installed > 0) {
8222
+ skillsSummary = `${skillsResult.installed} skills \u2192 ${skillsResult.location}`;
8223
+ } else if (skillsResult.skipped > 0) {
8224
+ skillsSummary = `Already installed (${skillsResult.location})`;
8225
+ } else {
8226
+ skillsSummary = "Skipped";
8227
+ }
8228
+ filesTable.push(["Skills", skillsSummary]);
7928
8229
  console.log(filesTable.toString());
7929
8230
  header("Configuration");
7930
8231
  label("Provider", selectedProvider);
@@ -7936,6 +8237,9 @@ function initCommand(program2) {
7936
8237
  info("2. Run `night-watch install` to set up cron jobs");
7937
8238
  info("3. Run `night-watch doctor` to verify the full setup");
7938
8239
  info("4. Or run `night-watch run` to execute PRDs manually");
8240
+ if (skillsResult.installed > 0) {
8241
+ info(`5. Use /nw-create-prd, /nw-run, /nw-add-issue and more in your AI assistant`);
8242
+ }
7939
8243
  console.log();
7940
8244
  });
7941
8245
  }
@@ -12159,7 +12463,7 @@ function doctorCommand(program2) {
12159
12463
  program2.command("doctor").description("Check Night Watch configuration and system health").option("--fix", "Automatically fix fixable issues").action(async (options) => {
12160
12464
  const projectDir = process.cwd();
12161
12465
  const config = loadConfig(projectDir);
12162
- const totalChecks = 8;
12466
+ const totalChecks = 7;
12163
12467
  let checkNum = 1;
12164
12468
  let passedChecks = 0;
12165
12469
  let fixedChecks = 0;
@@ -12185,11 +12489,12 @@ function doctorCommand(program2) {
12185
12489
  const ghResult = runCheck(checkNum++, totalChecks, "GitHub CLI", () => checkGhCli(), options);
12186
12490
  if (ghResult.passed) passedChecks++;
12187
12491
  if (ghResult.fixed) fixedChecks++;
12492
+ const resolvedProviderCli = BUILT_IN_PRESETS[config.provider]?.command ?? config.provider;
12188
12493
  const providerResult = runCheck(
12189
12494
  checkNum++,
12190
12495
  totalChecks,
12191
12496
  "provider CLI",
12192
- () => checkProviderCli(config.provider),
12497
+ () => checkProviderCli(resolvedProviderCli),
12193
12498
  options
12194
12499
  );
12195
12500
  if (providerResult.passed) passedChecks++;
@@ -12203,15 +12508,6 @@ function doctorCommand(program2) {
12203
12508
  );
12204
12509
  if (configResult.passed) passedChecks++;
12205
12510
  if (configResult.fixed) fixedChecks++;
12206
- const prdResult = runCheck(
12207
- checkNum++,
12208
- totalChecks,
12209
- "PRD directory",
12210
- () => checkPrdDirectory(projectDir, config.prdDir),
12211
- options
12212
- );
12213
- if (prdResult.passed) passedChecks++;
12214
- if (prdResult.fixed) fixedChecks++;
12215
12511
  const logsResult = runCheck(
12216
12512
  checkNum++,
12217
12513
  totalChecks,
@@ -12549,18 +12845,16 @@ function createActionRouteHandlers(ctx) {
12549
12845
  router.post(`/${p}review`, (req, res) => {
12550
12846
  spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
12551
12847
  });
12552
- router.post(`/${p}qa`, (req, res) => {
12553
- spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
12554
- });
12555
- router.post(`/${p}audit`, (req, res) => {
12556
- spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
12557
- });
12558
- router.post(`/${p}analytics`, (req, res) => {
12559
- spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
12560
- });
12561
- router.post(`/${p}planner`, (req, res) => {
12562
- spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12563
- });
12848
+ for (const jobDef of JOB_REGISTRY) {
12849
+ if (jobDef.id === "executor")
12850
+ continue;
12851
+ if (jobDef.id === "reviewer")
12852
+ continue;
12853
+ const cmd = jobDef.cliCommand;
12854
+ router.post(`/${p}${cmd}`, (req, res) => {
12855
+ spawnAction2(ctx.getProjectDir(req), [cmd], req, res);
12856
+ });
12857
+ }
12564
12858
  router.post(`/${p}install-cron`, (req, res) => {
12565
12859
  const projectDir = ctx.getProjectDir(req);
12566
12860
  try {
@@ -14033,6 +14327,21 @@ function createGlobalApp() {
14033
14327
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
14034
14328
  }
14035
14329
  });
14330
+ app.delete("/api/projects/:projectId", (req, res) => {
14331
+ try {
14332
+ const rawId = decodeURIComponent(String(req.params.projectId)).replace(/~/g, "/");
14333
+ const entries = loadRegistry();
14334
+ const entry = entries.find((e) => e.name === rawId);
14335
+ if (!entry) {
14336
+ res.status(404).json({ error: "Project not found" });
14337
+ return;
14338
+ }
14339
+ const result = removeProject(entry.path);
14340
+ res.json(result);
14341
+ } catch (error2) {
14342
+ res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
14343
+ }
14344
+ });
14036
14345
  app.use("/api/queue", createGlobalQueueRoutes());
14037
14346
  app.use("/api/global-notifications", createGlobalNotificationsRoutes());
14038
14347
  app.use("/api/projects/:projectId", resolveProject, createProjectRouter());