@jonit-dev/night-watch-cli 1.8.2 → 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.
Files changed (32) hide show
  1. package/dist/cli.js +1017 -553
  2. package/dist/commands/board.d.ts.map +1 -1
  3. package/dist/commands/board.js +20 -0
  4. package/dist/commands/board.js.map +1 -1
  5. package/dist/commands/doctor.d.ts.map +1 -1
  6. package/dist/commands/doctor.js +8 -12
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +96 -2
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/scripts/publish.sh +94 -26
  12. package/dist/templates/prd-executor.md +1 -0
  13. package/dist/templates/skills/_codex-block.md +58 -0
  14. package/dist/templates/skills/nw-add-issue.md +39 -0
  15. package/dist/templates/skills/nw-board-sync.md +47 -0
  16. package/dist/templates/skills/nw-create-prd.md +61 -0
  17. package/dist/templates/skills/nw-review.md +39 -0
  18. package/dist/templates/skills/nw-run.md +39 -0
  19. package/dist/templates/skills/nw-slice.md +33 -0
  20. package/dist/templates/skills/skills/_codex-block.md +58 -0
  21. package/dist/templates/skills/skills/nw-add-issue.md +39 -0
  22. package/dist/templates/skills/skills/nw-board-sync.md +47 -0
  23. package/dist/templates/skills/skills/nw-create-prd.md +61 -0
  24. package/dist/templates/skills/skills/nw-review.md +39 -0
  25. package/dist/templates/skills/skills/nw-run.md +39 -0
  26. package/dist/templates/skills/skills/nw-slice.md +33 -0
  27. package/dist/web/assets/index-BFxPiKyy.js +381 -0
  28. package/dist/web/assets/index-Cp7RYjoy.css +1 -0
  29. package/dist/web/assets/index-DTsfDC7m.js +381 -0
  30. package/dist/web/assets/index-ZABWMEZR.js +381 -0
  31. package/dist/web/index.html +2 -2
  32. package/package.json +12 -3
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")
@@ -31,10 +308,11 @@ function resolveProviderBucketKey(provider, providerEnv) {
31
308
  return `claude-proxy:${baseUrl}`;
32
309
  }
33
310
  }
34
- var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY, QUEUE_LOCK_FILE_NAME;
311
+ var DEFAULT_DEFAULT_BRANCH, DEFAULT_PRD_DIR, DEFAULT_MAX_RUNTIME, DEFAULT_REVIEWER_MAX_RUNTIME, DEFAULT_CRON_SCHEDULE, DEFAULT_REVIEWER_SCHEDULE, DEFAULT_CRON_SCHEDULE_OFFSET, DEFAULT_MAX_RETRIES, DEFAULT_REVIEWER_MAX_RETRIES, DEFAULT_REVIEWER_RETRY_DELAY, DEFAULT_REVIEWER_MAX_PRS_PER_RUN, DEFAULT_BRANCH_PREFIX, DEFAULT_BRANCH_PATTERNS, DEFAULT_MIN_REVIEW_SCORE, DEFAULT_MAX_LOG_SIZE, DEFAULT_PROVIDER, DEFAULT_EXECUTOR_ENABLED, DEFAULT_REVIEWER_ENABLED, DEFAULT_PROVIDER_ENV, DEFAULT_FALLBACK_ON_RATE_LIMIT, DEFAULT_CLAUDE_MODEL, DEFAULT_PRIMARY_FALLBACK_MODEL, DEFAULT_SECONDARY_FALLBACK_MODEL, VALID_CLAUDE_MODELS, CLAUDE_MODEL_IDS, DEFAULT_NOTIFICATIONS, DEFAULT_PRD_PRIORITY, DEFAULT_SLICER_SCHEDULE, DEFAULT_SLICER_MAX_RUNTIME, DEFAULT_ROADMAP_SCANNER, DEFAULT_TEMPLATES_DIR, DEFAULT_BOARD_PROVIDER, DEFAULT_LOCAL_BOARD_INFO, DEFAULT_AUTO_MERGE, DEFAULT_AUTO_MERGE_METHOD, VALID_MERGE_METHODS, DEFAULT_QA_ENABLED, DEFAULT_QA_SCHEDULE, DEFAULT_QA_MAX_RUNTIME, DEFAULT_QA_ARTIFACTS, DEFAULT_QA_SKIP_LABEL, DEFAULT_QA_AUTO_INSTALL_PLAYWRIGHT, DEFAULT_QA, QA_LOG_NAME, DEFAULT_AUDIT_ENABLED, DEFAULT_AUDIT_SCHEDULE, DEFAULT_AUDIT_MAX_RUNTIME, DEFAULT_AUDIT, DEFAULT_ANALYTICS_ENABLED, DEFAULT_ANALYTICS_SCHEDULE, DEFAULT_ANALYTICS_MAX_RUNTIME, DEFAULT_ANALYTICS_LOOKBACK_DAYS, DEFAULT_ANALYTICS_TARGET_COLUMN, DEFAULT_ANALYTICS_PROMPT, DEFAULT_ANALYTICS, AUDIT_LOG_NAME, PLANNER_LOG_NAME, ANALYTICS_LOG_NAME, VALID_PROVIDERS, VALID_JOB_TYPES, DEFAULT_JOB_PROVIDERS, BUILT_IN_PRESETS, BUILT_IN_PRESET_IDS, PROVIDER_COMMANDS, CONFIG_FILE_NAME, LOCK_FILE_PREFIX, LOG_DIR, CLAIM_FILE_EXTENSION, EXECUTOR_LOG_NAME, REVIEWER_LOG_NAME, EXECUTOR_LOG_FILE, REVIEWER_LOG_FILE, LOG_FILE_NAMES, GLOBAL_CONFIG_DIR, REGISTRY_FILE_NAME, HISTORY_FILE_NAME, PRD_STATES_FILE_NAME, STATE_DB_FILE_NAME, GLOBAL_NOTIFICATIONS_FILE_NAME, MAX_HISTORY_RECORDS_PER_PRD, DEFAULT_QUEUE_ENABLED, DEFAULT_QUEUE_MODE, DEFAULT_QUEUE_MAX_CONCURRENCY, DEFAULT_QUEUE_MAX_WAIT_TIME, DEFAULT_QUEUE_PRIORITY, DEFAULT_QUEUE, DEFAULT_SCHEDULING_PRIORITY, QUEUE_LOCK_FILE_NAME;
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,32 +483,19 @@ 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";
226
490
  PRD_STATES_FILE_NAME = "prd-states.json";
227
491
  STATE_DB_FILE_NAME = "state.db";
492
+ GLOBAL_NOTIFICATIONS_FILE_NAME = "global-notifications.json";
228
493
  MAX_HISTORY_RECORDS_PER_PRD = 10;
229
494
  DEFAULT_QUEUE_ENABLED = true;
230
495
  DEFAULT_QUEUE_MODE = "conservative";
231
496
  DEFAULT_QUEUE_MAX_CONCURRENCY = 1;
232
497
  DEFAULT_QUEUE_MAX_WAIT_TIME = 7200;
233
- DEFAULT_QUEUE_PRIORITY = {
234
- executor: 50,
235
- reviewer: 40,
236
- slicer: 30,
237
- qa: 20,
238
- audit: 10,
239
- analytics: 10
240
- };
498
+ DEFAULT_QUEUE_PRIORITY = getDefaultQueuePriority();
241
499
  DEFAULT_QUEUE = {
242
500
  enabled: DEFAULT_QUEUE_ENABLED,
243
501
  mode: DEFAULT_QUEUE_MODE,
@@ -251,15 +509,6 @@ If no issues are warranted, output an empty array: []`;
251
509
  }
252
510
  });
253
511
 
254
- // ../core/dist/board/types.js
255
- var BOARD_COLUMNS;
256
- var init_types2 = __esm({
257
- "../core/dist/board/types.js"() {
258
- "use strict";
259
- BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
260
- }
261
- });
262
-
263
512
  // ../core/dist/config-normalize.js
264
513
  function validateProvider(value) {
265
514
  const trimmed = value.trim();
@@ -431,43 +680,14 @@ function normalizeConfig(rawConfig) {
431
680
  if (mergeMethod && VALID_MERGE_METHODS.includes(mergeMethod)) {
432
681
  normalized.autoMergeMethod = mergeMethod;
433
682
  }
434
- const rawQa = readObject(rawConfig.qa);
435
- if (rawQa) {
436
- const artifactsValue = readString(rawQa.artifacts);
437
- const artifacts = artifactsValue && ["screenshot", "video", "both"].includes(artifactsValue) ? artifactsValue : DEFAULT_QA.artifacts;
438
- const qa = {
439
- enabled: readBoolean(rawQa.enabled) ?? DEFAULT_QA.enabled,
440
- schedule: readString(rawQa.schedule) ?? DEFAULT_QA.schedule,
441
- maxRuntime: readNumber(rawQa.maxRuntime) ?? DEFAULT_QA.maxRuntime,
442
- branchPatterns: readStringArray(rawQa.branchPatterns) ?? DEFAULT_QA.branchPatterns,
443
- artifacts,
444
- skipLabel: readString(rawQa.skipLabel) ?? DEFAULT_QA.skipLabel,
445
- autoInstallPlaywright: readBoolean(rawQa.autoInstallPlaywright) ?? DEFAULT_QA.autoInstallPlaywright
446
- };
447
- normalized.qa = qa;
448
- }
449
- const rawAudit = readObject(rawConfig.audit);
450
- if (rawAudit) {
451
- const audit = {
452
- enabled: readBoolean(rawAudit.enabled) ?? DEFAULT_AUDIT.enabled,
453
- schedule: readString(rawAudit.schedule) ?? DEFAULT_AUDIT.schedule,
454
- maxRuntime: readNumber(rawAudit.maxRuntime) ?? DEFAULT_AUDIT.maxRuntime
455
- };
456
- normalized.audit = audit;
457
- }
458
- const rawAnalytics = readObject(rawConfig.analytics);
459
- if (rawAnalytics) {
460
- const targetColumnRaw = readString(rawAnalytics.targetColumn);
461
- const targetColumn = targetColumnRaw && BOARD_COLUMNS.includes(targetColumnRaw) ? targetColumnRaw : DEFAULT_ANALYTICS.targetColumn;
462
- const analytics = {
463
- enabled: readBoolean(rawAnalytics.enabled) ?? DEFAULT_ANALYTICS.enabled,
464
- schedule: readString(rawAnalytics.schedule) ?? DEFAULT_ANALYTICS.schedule,
465
- maxRuntime: readNumber(rawAnalytics.maxRuntime) ?? DEFAULT_ANALYTICS.maxRuntime,
466
- lookbackDays: readNumber(rawAnalytics.lookbackDays) ?? DEFAULT_ANALYTICS.lookbackDays,
467
- targetColumn,
468
- analysisPrompt: readString(rawAnalytics.analysisPrompt) ?? DEFAULT_ANALYTICS.analysisPrompt
469
- };
470
- 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
+ }
471
691
  }
472
692
  const rawJobProviders = readObject(rawConfig.jobProviders);
473
693
  if (rawJobProviders) {
@@ -528,8 +748,8 @@ function normalizeConfig(rawConfig) {
528
748
  var init_config_normalize = __esm({
529
749
  "../core/dist/config-normalize.js"() {
530
750
  "use strict";
531
- init_types2();
532
751
  init_constants();
752
+ init_job_registry();
533
753
  }
534
754
  });
535
755
 
@@ -705,72 +925,16 @@ function buildEnvOverrideConfig(fileConfig) {
705
925
  env.claudeModel = model;
706
926
  }
707
927
  }
708
- const qaBase = () => env.qa ?? fileConfig?.qa ?? DEFAULT_QA;
709
- if (process.env.NW_QA_ENABLED) {
710
- const v = parseBoolean(process.env.NW_QA_ENABLED);
711
- if (v !== null)
712
- env.qa = { ...qaBase(), enabled: v };
713
- }
714
- if (process.env.NW_QA_SCHEDULE) {
715
- env.qa = { ...qaBase(), schedule: process.env.NW_QA_SCHEDULE };
716
- }
717
- if (process.env.NW_QA_MAX_RUNTIME) {
718
- const v = parseInt(process.env.NW_QA_MAX_RUNTIME, 10);
719
- if (!isNaN(v) && v > 0)
720
- env.qa = { ...qaBase(), maxRuntime: v };
721
- }
722
- if (process.env.NW_QA_ARTIFACTS) {
723
- const a = process.env.NW_QA_ARTIFACTS;
724
- if (["screenshot", "video", "both"].includes(a)) {
725
- 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;
726
936
  }
727
937
  }
728
- if (process.env.NW_QA_SKIP_LABEL) {
729
- env.qa = { ...qaBase(), skipLabel: process.env.NW_QA_SKIP_LABEL };
730
- }
731
- if (process.env.NW_QA_AUTO_INSTALL_PLAYWRIGHT) {
732
- const v = parseBoolean(process.env.NW_QA_AUTO_INSTALL_PLAYWRIGHT);
733
- if (v !== null)
734
- env.qa = { ...qaBase(), autoInstallPlaywright: v };
735
- }
736
- if (process.env.NW_QA_BRANCH_PATTERNS) {
737
- const patterns = process.env.NW_QA_BRANCH_PATTERNS.split(",").map((s) => s.trim()).filter(Boolean);
738
- if (patterns.length > 0)
739
- env.qa = { ...qaBase(), branchPatterns: patterns };
740
- }
741
- const auditBase = () => env.audit ?? fileConfig?.audit ?? DEFAULT_AUDIT;
742
- if (process.env.NW_AUDIT_ENABLED) {
743
- const v = parseBoolean(process.env.NW_AUDIT_ENABLED);
744
- if (v !== null)
745
- env.audit = { ...auditBase(), enabled: v };
746
- }
747
- if (process.env.NW_AUDIT_SCHEDULE) {
748
- env.audit = { ...auditBase(), schedule: process.env.NW_AUDIT_SCHEDULE };
749
- }
750
- if (process.env.NW_AUDIT_MAX_RUNTIME) {
751
- const v = parseInt(process.env.NW_AUDIT_MAX_RUNTIME, 10);
752
- if (!isNaN(v) && v > 0)
753
- env.audit = { ...auditBase(), maxRuntime: v };
754
- }
755
- const analyticsBase = () => env.analytics ?? fileConfig?.analytics ?? DEFAULT_ANALYTICS;
756
- if (process.env.NW_ANALYTICS_ENABLED) {
757
- const v = parseBoolean(process.env.NW_ANALYTICS_ENABLED);
758
- if (v !== null)
759
- env.analytics = { ...analyticsBase(), enabled: v };
760
- }
761
- if (process.env.NW_ANALYTICS_SCHEDULE) {
762
- env.analytics = { ...analyticsBase(), schedule: process.env.NW_ANALYTICS_SCHEDULE };
763
- }
764
- if (process.env.NW_ANALYTICS_MAX_RUNTIME) {
765
- const v = parseInt(process.env.NW_ANALYTICS_MAX_RUNTIME, 10);
766
- if (!isNaN(v) && v > 0)
767
- env.analytics = { ...analyticsBase(), maxRuntime: v };
768
- }
769
- if (process.env.NW_ANALYTICS_LOOKBACK_DAYS) {
770
- const v = parseInt(process.env.NW_ANALYTICS_LOOKBACK_DAYS, 10);
771
- if (!isNaN(v) && v > 0)
772
- env.analytics = { ...analyticsBase(), lookbackDays: v };
773
- }
774
938
  const jobProvidersEnv = {};
775
939
  for (const jobType of VALID_JOB_TYPES) {
776
940
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -820,6 +984,7 @@ var init_config_env = __esm({
820
984
  "use strict";
821
985
  init_constants();
822
986
  init_config_normalize();
987
+ init_job_registry();
823
988
  }
824
989
  });
825
990
 
@@ -1015,6 +1180,15 @@ var init_config = __esm({
1015
1180
  }
1016
1181
  });
1017
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
+
1018
1192
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
1019
1193
  import Database from "better-sqlite3";
1020
1194
  import { inject, injectable } from "tsyringe";
@@ -2029,11 +2203,18 @@ var init_github_projects = __esm({
2029
2203
  await this.ensureStatusColumns(existing.id);
2030
2204
  return { id: existing.id, number: existing.number, title: existing.title, url: existing.url };
2031
2205
  }
2032
- const createData = await graphql(`mutation CreateProject($ownerId: ID!, $title: String!) {
2033
- createProjectV2(input: { ownerId: $ownerId, title: $title }) {
2034
- projectV2 { id number url title }
2206
+ const createData = await graphql(`
2207
+ mutation CreateProject($ownerId: ID!, $title: String!) {
2208
+ createProjectV2(input: { ownerId: $ownerId, title: $title }) {
2209
+ projectV2 {
2210
+ id
2211
+ number
2212
+ url
2213
+ title
2214
+ }
2215
+ }
2035
2216
  }
2036
- }`, { ownerId: owner.id, title }, this.cwd);
2217
+ `, { ownerId: owner.id, title }, this.cwd);
2037
2218
  const project = createData.createProjectV2.projectV2;
2038
2219
  this.cachedProjectId = project.id;
2039
2220
  await this.linkProjectToRepository(project.id);
@@ -2049,24 +2230,34 @@ var init_github_projects = __esm({
2049
2230
  const message = err instanceof Error ? err.message : String(err);
2050
2231
  if (!message.includes("Status field not found"))
2051
2232
  throw err;
2052
- const createFieldData = await graphql(`mutation CreateStatusField($projectId: ID!) {
2053
- createProjectV2Field(input: {
2054
- projectId: $projectId
2055
- dataType: SINGLE_SELECT
2056
- name: "Status"
2057
- singleSelectOptions: [
2058
- { name: "Draft", color: GRAY, description: "" }
2059
- { name: "Ready", color: BLUE, description: "" }
2060
- { name: "In Progress", color: YELLOW, description: "" }
2061
- { name: "Review", color: ORANGE, description: "" }
2062
- { name: "Done", color: GREEN, description: "" }
2063
- ]
2064
- }) {
2065
- projectV2Field {
2066
- ... on ProjectV2SingleSelectField { id options { id name } }
2233
+ const createFieldData = await graphql(`
2234
+ mutation CreateStatusField($projectId: ID!) {
2235
+ createProjectV2Field(
2236
+ input: {
2237
+ projectId: $projectId
2238
+ dataType: SINGLE_SELECT
2239
+ name: "Status"
2240
+ singleSelectOptions: [
2241
+ { name: "Draft", color: GRAY, description: "" }
2242
+ { name: "Ready", color: BLUE, description: "" }
2243
+ { name: "In Progress", color: YELLOW, description: "" }
2244
+ { name: "Review", color: ORANGE, description: "" }
2245
+ { name: "Done", color: GREEN, description: "" }
2246
+ ]
2247
+ }
2248
+ ) {
2249
+ projectV2Field {
2250
+ ... on ProjectV2SingleSelectField {
2251
+ id
2252
+ options {
2253
+ id
2254
+ name
2255
+ }
2256
+ }
2257
+ }
2067
2258
  }
2068
2259
  }
2069
- }`, { projectId: project.id }, this.cwd);
2260
+ `, { projectId: project.id }, this.cwd);
2070
2261
  const field = createFieldData.createProjectV2Field.projectV2Field;
2071
2262
  this.cachedFieldId = field.id;
2072
2263
  this.cachedOptionIds = new Map(field.options.map((o) => [o.name, o.id]));
@@ -2093,11 +2284,23 @@ var init_github_projects = __esm({
2093
2284
  async createIssue(input) {
2094
2285
  const repo = await this.getRepo();
2095
2286
  const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2096
- const issueArgs = ["issue", "create", "--title", input.title, "--body", input.body, "--repo", repo];
2287
+ const issueArgs = [
2288
+ "issue",
2289
+ "create",
2290
+ "--title",
2291
+ input.title,
2292
+ "--body",
2293
+ input.body,
2294
+ "--repo",
2295
+ repo
2296
+ ];
2097
2297
  if (input.labels && input.labels.length > 0) {
2098
2298
  issueArgs.push("--label", input.labels.join(","));
2099
2299
  }
2100
- const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, { cwd: this.cwd, encoding: "utf-8" });
2300
+ const { stdout: issueUrlRaw } = await execFileAsync2("gh", issueArgs, {
2301
+ cwd: this.cwd,
2302
+ encoding: "utf-8"
2303
+ });
2101
2304
  const issueUrl = issueUrlRaw.trim();
2102
2305
  const issueNumber = parseInt(issueUrl.split("/").pop() ?? "", 10);
2103
2306
  if (!issueNumber)
@@ -2105,11 +2308,15 @@ var init_github_projects = __esm({
2105
2308
  const [owner, repoName] = repo.split("/");
2106
2309
  const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
2107
2310
  const issueJson = { number: issueNumber, id: nodeIdRaw.trim(), url: issueUrl };
2108
- const addData = await graphql(`mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2109
- addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2110
- item { id }
2311
+ const addData = await graphql(`
2312
+ mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2313
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2314
+ item {
2315
+ id
2316
+ }
2317
+ }
2111
2318
  }
2112
- }`, { projectId, contentId: issueJson.id }, this.cwd);
2319
+ `, { projectId, contentId: issueJson.id }, this.cwd);
2113
2320
  const itemId = addData.addProjectV2ItemById.item.id;
2114
2321
  const targetColumn = input.column ?? "Draft";
2115
2322
  const optionId = optionIds.get(targetColumn);
@@ -2129,11 +2336,45 @@ var init_github_projects = __esm({
2129
2336
  assignees: []
2130
2337
  };
2131
2338
  }
2339
+ async addIssue(issueNumber, column = "Ready") {
2340
+ const repo = await this.getRepo();
2341
+ const { projectId, fieldId, optionIds } = await this.ensureProjectCache();
2342
+ const [owner, repoName] = repo.split("/");
2343
+ const { stdout: nodeIdRaw } = await execFileAsync2("gh", ["api", `repos/${owner}/${repoName}/issues/${issueNumber}`, "--jq", ".node_id"], { cwd: this.cwd, encoding: "utf-8" });
2344
+ const nodeId = nodeIdRaw.trim();
2345
+ if (!nodeId)
2346
+ throw new Error(`Issue #${issueNumber} not found in ${repo}.`);
2347
+ const addData = await graphql(`
2348
+ mutation AddProjectItem($projectId: ID!, $contentId: ID!) {
2349
+ addProjectV2ItemById(input: { projectId: $projectId, contentId: $contentId }) {
2350
+ item {
2351
+ id
2352
+ }
2353
+ }
2354
+ }
2355
+ `, { projectId, contentId: nodeId }, this.cwd);
2356
+ const itemId = addData.addProjectV2ItemById.item.id;
2357
+ const optionId = optionIds.get(column);
2358
+ if (optionId)
2359
+ await this.setItemStatus(projectId, itemId, fieldId, optionId);
2360
+ const full = await this.getIssue(issueNumber);
2361
+ if (full)
2362
+ return { ...full, column };
2363
+ throw new Error(`Added issue #${issueNumber} to project but failed to fetch it back.`);
2364
+ }
2132
2365
  async getIssue(issueNumber) {
2133
2366
  const repo = await this.getRepo();
2134
2367
  let rawIssue;
2135
2368
  try {
2136
- const { stdout: output } = await execFileAsync2("gh", ["issue", "view", String(issueNumber), "--repo", repo, "--json", "number,title,body,url,id,labels,assignees"], { cwd: this.cwd, encoding: "utf-8" });
2369
+ const { stdout: output } = await execFileAsync2("gh", [
2370
+ "issue",
2371
+ "view",
2372
+ String(issueNumber),
2373
+ "--repo",
2374
+ repo,
2375
+ "--json",
2376
+ "number,title,body,url,id,labels,assignees"
2377
+ ], { cwd: this.cwd, encoding: "utf-8" });
2137
2378
  rawIssue = JSON.parse(output);
2138
2379
  } catch {
2139
2380
  return null;
@@ -2238,6 +2479,9 @@ var init_local_kanban = __esm({
2238
2479
  });
2239
2480
  return toIBoardIssue(row);
2240
2481
  }
2482
+ async addIssue(_issueNumber, _column) {
2483
+ throw new Error("addIssue is not supported by the local Kanban provider.");
2484
+ }
2241
2485
  async getIssue(issueNumber) {
2242
2486
  const row = this.repo.getByNumber(issueNumber);
2243
2487
  return row ? toIBoardIssue(row) : null;
@@ -4275,6 +4519,36 @@ var init_log_utils = __esm({
4275
4519
  }
4276
4520
  });
4277
4521
 
4522
+ // ../core/dist/utils/global-config.js
4523
+ import * as fs11 from "fs";
4524
+ import * as os5 from "os";
4525
+ import * as path10 from "path";
4526
+ function getGlobalNotificationsPath() {
4527
+ return path10.join(os5.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_NOTIFICATIONS_FILE_NAME);
4528
+ }
4529
+ function loadGlobalNotificationsConfig() {
4530
+ const filePath = getGlobalNotificationsPath();
4531
+ try {
4532
+ if (!fs11.existsSync(filePath))
4533
+ return { webhook: null };
4534
+ const raw = fs11.readFileSync(filePath, "utf-8");
4535
+ return JSON.parse(raw);
4536
+ } catch {
4537
+ return { webhook: null };
4538
+ }
4539
+ }
4540
+ function saveGlobalNotificationsConfig(config) {
4541
+ const filePath = getGlobalNotificationsPath();
4542
+ fs11.mkdirSync(path10.dirname(filePath), { recursive: true });
4543
+ fs11.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
4544
+ }
4545
+ var init_global_config = __esm({
4546
+ "../core/dist/utils/global-config.js"() {
4547
+ "use strict";
4548
+ init_constants();
4549
+ }
4550
+ });
4551
+
4278
4552
  // ../core/dist/utils/ui.js
4279
4553
  import chalk from "chalk";
4280
4554
  import ora from "ora";
@@ -4631,24 +4905,33 @@ async function sendWebhook(webhook, ctx) {
4631
4905
  warn(`Notification failed (${webhook.type}): ${message}`);
4632
4906
  }
4633
4907
  }
4908
+ function webhookIdentity(wh) {
4909
+ if (wh.type === "telegram")
4910
+ return `telegram:${wh.botToken}:${wh.chatId}`;
4911
+ return `${wh.type}:${wh.url}`;
4912
+ }
4634
4913
  async function sendNotifications(config, ctx) {
4635
- const webhooks = config.notifications?.webhooks ?? [];
4636
- const tasks = [];
4637
- for (const wh of webhooks) {
4638
- tasks.push(sendWebhook(wh, ctx));
4914
+ const projectWebhooks = config.notifications?.webhooks ?? [];
4915
+ const globalConfig = loadGlobalNotificationsConfig();
4916
+ const allWebhooks = [...projectWebhooks];
4917
+ if (globalConfig.webhook) {
4918
+ const projectIds = new Set(projectWebhooks.map(webhookIdentity));
4919
+ if (!projectIds.has(webhookIdentity(globalConfig.webhook))) {
4920
+ allWebhooks.push(globalConfig.webhook);
4921
+ }
4639
4922
  }
4640
- if (tasks.length === 0) {
4923
+ if (allWebhooks.length === 0) {
4641
4924
  return;
4642
4925
  }
4643
- const results = await Promise.allSettled(tasks);
4926
+ const results = await Promise.allSettled(allWebhooks.map((wh) => sendWebhook(wh, ctx)));
4644
4927
  const sent = results.filter((r) => r.status === "fulfilled").length;
4645
- const total = results.length;
4646
- info(`Sent ${sent}/${total} notifications`);
4928
+ info(`Sent ${sent}/${allWebhooks.length} notifications`);
4647
4929
  }
4648
4930
  var MAX_QA_SCREENSHOTS_IN_NOTIFICATION;
4649
4931
  var init_notify = __esm({
4650
4932
  "../core/dist/utils/notify.js"() {
4651
4933
  "use strict";
4934
+ init_global_config();
4652
4935
  init_ui();
4653
4936
  init_github();
4654
4937
  MAX_QA_SCREENSHOTS_IN_NOTIFICATION = 3;
@@ -4685,15 +4968,15 @@ var init_prd_discovery = __esm({
4685
4968
  });
4686
4969
 
4687
4970
  // ../core/dist/utils/prd-utils.js
4688
- import * as fs11 from "fs";
4689
- import * as path10 from "path";
4971
+ import * as fs12 from "fs";
4972
+ import * as path11 from "path";
4690
4973
  function slugify(name) {
4691
4974
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
4692
4975
  }
4693
4976
  function getNextPrdNumber(prdDir) {
4694
- if (!fs11.existsSync(prdDir))
4977
+ if (!fs12.existsSync(prdDir))
4695
4978
  return 1;
4696
- const files = fs11.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
4979
+ const files = fs12.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
4697
4980
  const numbers = files.map((f) => {
4698
4981
  const match = f.match(/^(\d+)-/);
4699
4982
  return match ? parseInt(match[1], 10) : 0;
@@ -4701,16 +4984,16 @@ function getNextPrdNumber(prdDir) {
4701
4984
  return Math.max(0, ...numbers) + 1;
4702
4985
  }
4703
4986
  function markPrdDone(prdDir, prdFile) {
4704
- const sourcePath = path10.join(prdDir, prdFile);
4705
- if (!fs11.existsSync(sourcePath)) {
4987
+ const sourcePath = path11.join(prdDir, prdFile);
4988
+ if (!fs12.existsSync(sourcePath)) {
4706
4989
  return false;
4707
4990
  }
4708
- const doneDir = path10.join(prdDir, "done");
4709
- if (!fs11.existsSync(doneDir)) {
4710
- fs11.mkdirSync(doneDir, { recursive: true });
4991
+ const doneDir = path11.join(prdDir, "done");
4992
+ if (!fs12.existsSync(doneDir)) {
4993
+ fs12.mkdirSync(doneDir, { recursive: true });
4711
4994
  }
4712
- const destPath = path10.join(doneDir, prdFile);
4713
- fs11.renameSync(sourcePath, destPath);
4995
+ const destPath = path11.join(doneDir, prdFile);
4996
+ fs12.renameSync(sourcePath, destPath);
4714
4997
  return true;
4715
4998
  }
4716
4999
  var init_prd_utils = __esm({
@@ -4720,16 +5003,16 @@ var init_prd_utils = __esm({
4720
5003
  });
4721
5004
 
4722
5005
  // ../core/dist/utils/registry.js
4723
- import * as fs12 from "fs";
4724
- import * as os5 from "os";
4725
- import * as path11 from "path";
5006
+ import * as fs13 from "fs";
5007
+ import * as os6 from "os";
5008
+ import * as path12 from "path";
4726
5009
  function readLegacyRegistryEntries() {
4727
5010
  const registryPath = getRegistryPath();
4728
- if (!fs12.existsSync(registryPath)) {
5011
+ if (!fs13.existsSync(registryPath)) {
4729
5012
  return [];
4730
5013
  }
4731
5014
  try {
4732
- const raw = fs12.readFileSync(registryPath, "utf-8");
5015
+ const raw = fs13.readFileSync(registryPath, "utf-8");
4733
5016
  const parsed = JSON.parse(raw);
4734
5017
  if (!Array.isArray(parsed)) {
4735
5018
  return [];
@@ -4764,8 +5047,8 @@ function loadRegistryEntriesWithLegacyFallback() {
4764
5047
  return projectRegistry.getAll();
4765
5048
  }
4766
5049
  function getRegistryPath() {
4767
- const base = process.env.NIGHT_WATCH_HOME || path11.join(os5.homedir(), GLOBAL_CONFIG_DIR);
4768
- return path11.join(base, REGISTRY_FILE_NAME);
5050
+ const base = process.env.NIGHT_WATCH_HOME || path12.join(os6.homedir(), GLOBAL_CONFIG_DIR);
5051
+ return path12.join(base, REGISTRY_FILE_NAME);
4769
5052
  }
4770
5053
  function loadRegistry() {
4771
5054
  return loadRegistryEntriesWithLegacyFallback();
@@ -4778,7 +5061,7 @@ function saveRegistry(entries) {
4778
5061
  }
4779
5062
  }
4780
5063
  function registerProject(projectDir) {
4781
- const resolvedPath = path11.resolve(projectDir);
5064
+ const resolvedPath = path12.resolve(projectDir);
4782
5065
  const { projectRegistry } = getRepositories();
4783
5066
  const entries = loadRegistryEntriesWithLegacyFallback();
4784
5067
  const existing = entries.find((e) => e.path === resolvedPath);
@@ -4787,13 +5070,13 @@ function registerProject(projectDir) {
4787
5070
  }
4788
5071
  const name = getProjectName(resolvedPath);
4789
5072
  const nameExists = entries.some((e) => e.name === name);
4790
- const finalName = nameExists ? `${name}-${path11.basename(resolvedPath)}` : name;
5073
+ const finalName = nameExists ? `${name}-${path12.basename(resolvedPath)}` : name;
4791
5074
  const entry = { name: finalName, path: resolvedPath };
4792
5075
  projectRegistry.upsert(entry);
4793
5076
  return entry;
4794
5077
  }
4795
5078
  function unregisterProject(projectDir) {
4796
- const resolvedPath = path11.resolve(projectDir);
5079
+ const resolvedPath = path12.resolve(projectDir);
4797
5080
  loadRegistryEntriesWithLegacyFallback();
4798
5081
  const { projectRegistry } = getRepositories();
4799
5082
  return projectRegistry.remove(resolvedPath);
@@ -4803,7 +5086,7 @@ function validateRegistry() {
4803
5086
  const valid = [];
4804
5087
  const invalid = [];
4805
5088
  for (const entry of entries) {
4806
- if (fs12.existsSync(entry.path) && fs12.existsSync(path11.join(entry.path, CONFIG_FILE_NAME))) {
5089
+ if (fs13.existsSync(entry.path) && fs13.existsSync(path12.join(entry.path, CONFIG_FILE_NAME))) {
4807
5090
  valid.push(entry);
4808
5091
  } else {
4809
5092
  invalid.push(entry);
@@ -4811,6 +5094,26 @@ function validateRegistry() {
4811
5094
  }
4812
5095
  return { valid, invalid };
4813
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
+ }
4814
5117
  var init_registry = __esm({
4815
5118
  "../core/dist/utils/registry.js"() {
4816
5119
  "use strict";
@@ -4818,6 +5121,7 @@ var init_registry = __esm({
4818
5121
  init_repositories();
4819
5122
  init_client();
4820
5123
  init_client();
5124
+ init_crontab();
4821
5125
  init_status_data();
4822
5126
  }
4823
5127
  });
@@ -4991,18 +5295,18 @@ var init_roadmap_parser = __esm({
4991
5295
  });
4992
5296
 
4993
5297
  // ../core/dist/utils/roadmap-state.js
4994
- import * as fs13 from "fs";
4995
- import * as path12 from "path";
5298
+ import * as fs14 from "fs";
5299
+ import * as path13 from "path";
4996
5300
  function getStateFilePath(prdDir) {
4997
- return path12.join(prdDir, STATE_FILE_NAME);
5301
+ return path13.join(prdDir, STATE_FILE_NAME);
4998
5302
  }
4999
5303
  function readJsonState(prdDir) {
5000
5304
  const statePath = getStateFilePath(prdDir);
5001
- if (!fs13.existsSync(statePath)) {
5305
+ if (!fs14.existsSync(statePath)) {
5002
5306
  return null;
5003
5307
  }
5004
5308
  try {
5005
- const content = fs13.readFileSync(statePath, "utf-8");
5309
+ const content = fs14.readFileSync(statePath, "utf-8");
5006
5310
  const parsed = JSON.parse(content);
5007
5311
  if (typeof parsed !== "object" || parsed === null) {
5008
5312
  return null;
@@ -5040,11 +5344,11 @@ function saveRoadmapState(prdDir, state) {
5040
5344
  const { roadmapState } = getRepositories();
5041
5345
  roadmapState.save(prdDir, state);
5042
5346
  const statePath = getStateFilePath(prdDir);
5043
- const dir = path12.dirname(statePath);
5044
- if (!fs13.existsSync(dir)) {
5045
- fs13.mkdirSync(dir, { recursive: true });
5347
+ const dir = path13.dirname(statePath);
5348
+ if (!fs14.existsSync(dir)) {
5349
+ fs14.mkdirSync(dir, { recursive: true });
5046
5350
  }
5047
- fs13.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5351
+ fs14.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
5048
5352
  }
5049
5353
  function createEmptyState() {
5050
5354
  return {
@@ -5084,15 +5388,15 @@ var init_roadmap_state = __esm({
5084
5388
  });
5085
5389
 
5086
5390
  // ../core/dist/templates/slicer-prompt.js
5087
- import * as fs14 from "fs";
5088
- import * as path13 from "path";
5391
+ import * as fs15 from "fs";
5392
+ import * as path14 from "path";
5089
5393
  function loadSlicerTemplate(templateDir) {
5090
5394
  if (cachedTemplate) {
5091
5395
  return cachedTemplate;
5092
5396
  }
5093
- const templatePath = templateDir ? path13.join(templateDir, "slicer.md") : path13.resolve(__dirname, "..", "..", "templates", "slicer.md");
5397
+ const templatePath = templateDir ? path14.join(templateDir, "slicer.md") : path14.resolve(__dirname, "..", "..", "templates", "slicer.md");
5094
5398
  try {
5095
- cachedTemplate = fs14.readFileSync(templatePath, "utf-8");
5399
+ cachedTemplate = fs15.readFileSync(templatePath, "utf-8");
5096
5400
  return cachedTemplate;
5097
5401
  } catch (error2) {
5098
5402
  console.warn(`Warning: Could not load slicer template from ${templatePath}, using default:`, error2 instanceof Error ? error2.message : String(error2));
@@ -5117,7 +5421,7 @@ function createSlicerPromptVars(title, section, description, prdDir, prdFilename
5117
5421
  title,
5118
5422
  section,
5119
5423
  description: description || "(No description provided)",
5120
- outputFilePath: path13.join(prdDir, prdFilename),
5424
+ outputFilePath: path14.join(prdDir, prdFilename),
5121
5425
  prdDir
5122
5426
  };
5123
5427
  }
@@ -5211,8 +5515,8 @@ DO NOT forget to write the file.
5211
5515
  });
5212
5516
 
5213
5517
  // ../core/dist/utils/roadmap-scanner.js
5214
- import * as fs15 from "fs";
5215
- import * as path14 from "path";
5518
+ import * as fs16 from "fs";
5519
+ import * as path15 from "path";
5216
5520
  import { spawn } from "child_process";
5217
5521
  import { createHash as createHash3 } from "crypto";
5218
5522
  function normalizeAuditSeverity(raw) {
@@ -5313,11 +5617,11 @@ function auditFindingToRoadmapItem(finding) {
5313
5617
  };
5314
5618
  }
5315
5619
  function collectAuditPlannerItems(projectDir) {
5316
- const reportPath = path14.join(projectDir, "logs", "audit-report.md");
5317
- if (!fs15.existsSync(reportPath)) {
5620
+ const reportPath = path15.join(projectDir, "logs", "audit-report.md");
5621
+ if (!fs16.existsSync(reportPath)) {
5318
5622
  return [];
5319
5623
  }
5320
- const reportContent = fs15.readFileSync(reportPath, "utf-8");
5624
+ const reportContent = fs16.readFileSync(reportPath, "utf-8");
5321
5625
  if (!reportContent.trim() || /\bNO_ISSUES_FOUND\b/.test(reportContent)) {
5322
5626
  return [];
5323
5627
  }
@@ -5326,9 +5630,9 @@ function collectAuditPlannerItems(projectDir) {
5326
5630
  return findings.map(auditFindingToRoadmapItem);
5327
5631
  }
5328
5632
  function getRoadmapStatus(projectDir, config) {
5329
- const roadmapPath = path14.join(projectDir, config.roadmapScanner.roadmapPath);
5633
+ const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
5330
5634
  const scannerEnabled = config.roadmapScanner.enabled;
5331
- if (!fs15.existsSync(roadmapPath)) {
5635
+ if (!fs16.existsSync(roadmapPath)) {
5332
5636
  return {
5333
5637
  found: false,
5334
5638
  enabled: scannerEnabled,
@@ -5339,9 +5643,9 @@ function getRoadmapStatus(projectDir, config) {
5339
5643
  items: []
5340
5644
  };
5341
5645
  }
5342
- const content = fs15.readFileSync(roadmapPath, "utf-8");
5646
+ const content = fs16.readFileSync(roadmapPath, "utf-8");
5343
5647
  const items = parseRoadmap(content);
5344
- const prdDir = path14.join(projectDir, config.prdDir);
5648
+ const prdDir = path15.join(projectDir, config.prdDir);
5345
5649
  const state = loadRoadmapState(prdDir);
5346
5650
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5347
5651
  const statusItems = items.map((item) => {
@@ -5378,10 +5682,10 @@ function getRoadmapStatus(projectDir, config) {
5378
5682
  }
5379
5683
  function scanExistingPrdSlugs(prdDir) {
5380
5684
  const slugs = /* @__PURE__ */ new Set();
5381
- if (!fs15.existsSync(prdDir)) {
5685
+ if (!fs16.existsSync(prdDir)) {
5382
5686
  return slugs;
5383
5687
  }
5384
- const files = fs15.readdirSync(prdDir);
5688
+ const files = fs16.readdirSync(prdDir);
5385
5689
  for (const file of files) {
5386
5690
  if (!file.endsWith(".md")) {
5387
5691
  continue;
@@ -5417,20 +5721,20 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5417
5721
  const nextNum = getNextPrdNumber(prdDir);
5418
5722
  const padded = String(nextNum).padStart(2, "0");
5419
5723
  const filename = `${padded}-${itemSlug}.md`;
5420
- const filePath = path14.join(prdDir, filename);
5421
- if (!fs15.existsSync(prdDir)) {
5422
- fs15.mkdirSync(prdDir, { recursive: true });
5724
+ const filePath = path15.join(prdDir, filename);
5725
+ if (!fs16.existsSync(prdDir)) {
5726
+ fs16.mkdirSync(prdDir, { recursive: true });
5423
5727
  }
5424
5728
  const promptVars = createSlicerPromptVars(item.title, item.section, item.description, prdDir, filename);
5425
5729
  const prompt2 = renderSlicerPrompt(promptVars);
5426
5730
  const provider = resolveJobProvider(config, "slicer");
5427
5731
  const providerArgs = buildProviderArgs(provider, prompt2, projectDir);
5428
- const logDir = path14.join(projectDir, "logs");
5429
- if (!fs15.existsSync(logDir)) {
5430
- fs15.mkdirSync(logDir, { recursive: true });
5732
+ const logDir = path15.join(projectDir, "logs");
5733
+ if (!fs16.existsSync(logDir)) {
5734
+ fs16.mkdirSync(logDir, { recursive: true });
5431
5735
  }
5432
- const logFile = path14.join(logDir, `slicer-${itemSlug}.log`);
5433
- const logStream = fs15.createWriteStream(logFile, { flags: "w" });
5736
+ const logFile = path15.join(logDir, `slicer-${itemSlug}.log`);
5737
+ const logStream = fs16.createWriteStream(logFile, { flags: "w" });
5434
5738
  logStream.on("error", () => {
5435
5739
  });
5436
5740
  return new Promise((resolve10) => {
@@ -5467,7 +5771,7 @@ async function sliceRoadmapItem(projectDir, prdDir, item, config) {
5467
5771
  });
5468
5772
  return;
5469
5773
  }
5470
- if (!fs15.existsSync(filePath)) {
5774
+ if (!fs16.existsSync(filePath)) {
5471
5775
  resolve10({
5472
5776
  sliced: false,
5473
5777
  error: `Provider did not create expected file: ${filePath}`,
@@ -5490,23 +5794,23 @@ async function sliceNextItem(projectDir, config) {
5490
5794
  error: "Roadmap scanner is disabled"
5491
5795
  };
5492
5796
  }
5493
- const roadmapPath = path14.join(projectDir, config.roadmapScanner.roadmapPath);
5797
+ const roadmapPath = path15.join(projectDir, config.roadmapScanner.roadmapPath);
5494
5798
  const auditItems = collectAuditPlannerItems(projectDir);
5495
- const roadmapExists = fs15.existsSync(roadmapPath);
5799
+ const roadmapExists = fs16.existsSync(roadmapPath);
5496
5800
  if (!roadmapExists && auditItems.length === 0) {
5497
5801
  return {
5498
5802
  sliced: false,
5499
5803
  error: "No pending items to process"
5500
5804
  };
5501
5805
  }
5502
- const roadmapItems = roadmapExists ? parseRoadmap(fs15.readFileSync(roadmapPath, "utf-8")) : [];
5806
+ const roadmapItems = roadmapExists ? parseRoadmap(fs16.readFileSync(roadmapPath, "utf-8")) : [];
5503
5807
  if (roadmapExists && roadmapItems.length === 0 && auditItems.length === 0) {
5504
5808
  return {
5505
5809
  sliced: false,
5506
5810
  error: "No items in roadmap"
5507
5811
  };
5508
5812
  }
5509
- const prdDir = path14.join(projectDir, config.prdDir);
5813
+ const prdDir = path15.join(projectDir, config.prdDir);
5510
5814
  const state = loadRoadmapState(prdDir);
5511
5815
  const existingPrdSlugs = scanExistingPrdSlugs(prdDir);
5512
5816
  const pickEligibleItem = (items) => {
@@ -5677,8 +5981,8 @@ var init_shell = __esm({
5677
5981
  });
5678
5982
 
5679
5983
  // ../core/dist/utils/scheduling.js
5680
- import * as fs16 from "fs";
5681
- import * as path15 from "path";
5984
+ import * as fs17 from "fs";
5985
+ import * as path16 from "path";
5682
5986
  function normalizeSchedulingPriority(priority) {
5683
5987
  if (!Number.isFinite(priority)) {
5684
5988
  return DEFAULT_SCHEDULING_PRIORITY;
@@ -5704,7 +6008,7 @@ function isJobTypeEnabled(config, jobType) {
5704
6008
  }
5705
6009
  }
5706
6010
  function loadPeerConfig(projectPath) {
5707
- if (!fs16.existsSync(projectPath) || !fs16.existsSync(path15.join(projectPath, CONFIG_FILE_NAME))) {
6011
+ if (!fs17.existsSync(projectPath) || !fs17.existsSync(path16.join(projectPath, CONFIG_FILE_NAME))) {
5708
6012
  return null;
5709
6013
  }
5710
6014
  try {
@@ -5715,9 +6019,9 @@ function loadPeerConfig(projectPath) {
5715
6019
  }
5716
6020
  function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5717
6021
  const peers = /* @__PURE__ */ new Map();
5718
- const currentPath = path15.resolve(currentProjectDir);
6022
+ const currentPath = path16.resolve(currentProjectDir);
5719
6023
  const addPeer = (projectPath, config) => {
5720
- const resolvedPath = path15.resolve(projectPath);
6024
+ const resolvedPath = path16.resolve(projectPath);
5721
6025
  if (!isJobTypeEnabled(config, jobType)) {
5722
6026
  return;
5723
6027
  }
@@ -5725,12 +6029,12 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5725
6029
  path: resolvedPath,
5726
6030
  config,
5727
6031
  schedulingPriority: normalizeSchedulingPriority(config.schedulingPriority),
5728
- sortKey: `${path15.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
6032
+ sortKey: `${path16.basename(resolvedPath).toLowerCase()}::${resolvedPath.toLowerCase()}`
5729
6033
  });
5730
6034
  };
5731
6035
  addPeer(currentPath, currentConfig);
5732
6036
  for (const entry of loadRegistry()) {
5733
- const resolvedPath = path15.resolve(entry.path);
6037
+ const resolvedPath = path16.resolve(entry.path);
5734
6038
  if (resolvedPath === currentPath || peers.has(resolvedPath)) {
5735
6039
  continue;
5736
6040
  }
@@ -5748,7 +6052,7 @@ function collectSchedulingPeers(currentProjectDir, currentConfig, jobType) {
5748
6052
  }
5749
6053
  function getSchedulingPlan(projectDir, config, jobType) {
5750
6054
  const peers = collectSchedulingPeers(projectDir, config, jobType);
5751
- const currentPath = path15.resolve(projectDir);
6055
+ const currentPath = path16.resolve(projectDir);
5752
6056
  const slotIndex = Math.max(0, peers.findIndex((peer) => peer.path === currentPath));
5753
6057
  const peerCount = Math.max(1, peers.length);
5754
6058
  const balancedDelayMinutes = peerCount <= 1 ? 0 : Math.floor(slotIndex * 60 / peerCount);
@@ -5840,8 +6144,8 @@ var init_webhook_validator = __esm({
5840
6144
 
5841
6145
  // ../core/dist/utils/worktree-manager.js
5842
6146
  import { execFileSync as execFileSync4 } from "child_process";
5843
- import * as fs17 from "fs";
5844
- import * as path16 from "path";
6147
+ import * as fs18 from "fs";
6148
+ import * as path17 from "path";
5845
6149
  function gitExec(args, cwd, logFile) {
5846
6150
  try {
5847
6151
  const result = execFileSync4("git", args, {
@@ -5851,7 +6155,7 @@ function gitExec(args, cwd, logFile) {
5851
6155
  });
5852
6156
  if (logFile && result) {
5853
6157
  try {
5854
- fs17.appendFileSync(logFile, result);
6158
+ fs18.appendFileSync(logFile, result);
5855
6159
  } catch {
5856
6160
  }
5857
6161
  }
@@ -5860,7 +6164,7 @@ function gitExec(args, cwd, logFile) {
5860
6164
  const errorMessage = error2 instanceof Error ? error2.message : String(error2);
5861
6165
  if (logFile) {
5862
6166
  try {
5863
- fs17.appendFileSync(logFile, errorMessage + "\n");
6167
+ fs18.appendFileSync(logFile, errorMessage + "\n");
5864
6168
  } catch {
5865
6169
  }
5866
6170
  }
@@ -5885,11 +6189,11 @@ function branchExistsRemotely(projectDir, branchName) {
5885
6189
  }
5886
6190
  function prepareBranchWorktree(options) {
5887
6191
  const { projectDir, worktreeDir, branchName, defaultBranch, logFile } = options;
5888
- if (fs17.existsSync(worktreeDir)) {
6192
+ if (fs18.existsSync(worktreeDir)) {
5889
6193
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
5890
6194
  if (!isRegistered) {
5891
6195
  try {
5892
- fs17.rmSync(worktreeDir, { recursive: true, force: true });
6196
+ fs18.rmSync(worktreeDir, { recursive: true, force: true });
5893
6197
  } catch {
5894
6198
  }
5895
6199
  }
@@ -5928,11 +6232,11 @@ function prepareBranchWorktree(options) {
5928
6232
  }
5929
6233
  function prepareDetachedWorktree(options) {
5930
6234
  const { projectDir, worktreeDir, defaultBranch, logFile } = options;
5931
- if (fs17.existsSync(worktreeDir)) {
6235
+ if (fs18.existsSync(worktreeDir)) {
5932
6236
  const isRegistered = isWorktreeRegistered(projectDir, worktreeDir);
5933
6237
  if (!isRegistered) {
5934
6238
  try {
5935
- fs17.rmSync(worktreeDir, { recursive: true, force: true });
6239
+ fs18.rmSync(worktreeDir, { recursive: true, force: true });
5936
6240
  } catch {
5937
6241
  }
5938
6242
  }
@@ -5974,7 +6278,7 @@ function isWorktreeRegistered(projectDir, worktreePath) {
5974
6278
  }
5975
6279
  }
5976
6280
  function cleanupWorktrees(projectDir, scope) {
5977
- const projectName = path16.basename(projectDir);
6281
+ const projectName = path17.basename(projectDir);
5978
6282
  const matchToken = scope ? scope : `${projectName}-nw`;
5979
6283
  const removed = [];
5980
6284
  try {
@@ -6009,16 +6313,16 @@ var init_worktree_manager = __esm({
6009
6313
  });
6010
6314
 
6011
6315
  // ../core/dist/utils/job-queue.js
6012
- import * as os6 from "os";
6013
- import * as path17 from "path";
6316
+ import * as os7 from "os";
6317
+ import * as path18 from "path";
6014
6318
  import Database7 from "better-sqlite3";
6015
6319
  function getStateDbPath() {
6016
- const base = process.env.NIGHT_WATCH_HOME || path17.join(os6.homedir(), GLOBAL_CONFIG_DIR);
6017
- return path17.join(base, STATE_DB_FILE_NAME);
6320
+ const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6321
+ return path18.join(base, STATE_DB_FILE_NAME);
6018
6322
  }
6019
6323
  function getQueueLockPath() {
6020
- const base = process.env.NIGHT_WATCH_HOME || path17.join(os6.homedir(), GLOBAL_CONFIG_DIR);
6021
- return path17.join(base, QUEUE_LOCK_FILE_NAME);
6324
+ const base = process.env.NIGHT_WATCH_HOME || path18.join(os7.homedir(), GLOBAL_CONFIG_DIR);
6325
+ return path18.join(base, QUEUE_LOCK_FILE_NAME);
6022
6326
  }
6023
6327
  function openDb() {
6024
6328
  const dbPath = getStateDbPath();
@@ -6586,9 +6890,9 @@ var init_amplitude_client = __esm({
6586
6890
  });
6587
6891
 
6588
6892
  // ../core/dist/analytics/analytics-runner.js
6589
- import * as fs18 from "fs";
6590
- import * as os7 from "os";
6591
- import * as path18 from "path";
6893
+ import * as fs19 from "fs";
6894
+ import * as os8 from "os";
6895
+ import * as path19 from "path";
6592
6896
  function parseIssuesFromResponse(text) {
6593
6897
  const start = text.indexOf("[");
6594
6898
  const end = text.lastIndexOf("]");
@@ -6617,9 +6921,9 @@ async function runAnalytics(config, projectDir) {
6617
6921
 
6618
6922
  --- AMPLITUDE DATA ---
6619
6923
  ${JSON.stringify(data, null, 2)}`;
6620
- const tmpDir = fs18.mkdtempSync(path18.join(os7.tmpdir(), "nw-analytics-"));
6621
- const promptFile = path18.join(tmpDir, "analytics-prompt.md");
6622
- fs18.writeFileSync(promptFile, prompt2, "utf-8");
6924
+ const tmpDir = fs19.mkdtempSync(path19.join(os8.tmpdir(), "nw-analytics-"));
6925
+ const promptFile = path19.join(tmpDir, "analytics-prompt.md");
6926
+ fs19.writeFileSync(promptFile, prompt2, "utf-8");
6623
6927
  try {
6624
6928
  const provider = resolveJobProvider(config, "analytics");
6625
6929
  const providerCmd = PROVIDER_COMMANDS[provider];
@@ -6636,8 +6940,8 @@ set -euo pipefail
6636
6940
  ${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
6637
6941
  `;
6638
6942
  }
6639
- const scriptFile = path18.join(tmpDir, "run-analytics.sh");
6640
- fs18.writeFileSync(scriptFile, scriptContent, { mode: 493 });
6943
+ const scriptFile = path19.join(tmpDir, "run-analytics.sh");
6944
+ fs19.writeFileSync(scriptFile, scriptContent, { mode: 493 });
6641
6945
  const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
6642
6946
  if (exitCode !== 0) {
6643
6947
  throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
@@ -6675,7 +6979,7 @@ ${stderr}`;
6675
6979
  };
6676
6980
  } finally {
6677
6981
  try {
6678
- fs18.rmSync(tmpDir, { recursive: true, force: true });
6982
+ fs19.rmSync(tmpDir, { recursive: true, force: true });
6679
6983
  } catch {
6680
6984
  }
6681
6985
  }
@@ -6876,6 +7180,14 @@ sequenceDiagram
6876
7180
  }
6877
7181
  });
6878
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
+
6879
7191
  // ../core/dist/index.js
6880
7192
  var dist_exports = {};
6881
7193
  __export(dist_exports, {
@@ -6953,9 +7265,11 @@ __export(dist_exports, {
6953
7265
  EXECUTOR_LOG_FILE: () => EXECUTOR_LOG_FILE,
6954
7266
  EXECUTOR_LOG_NAME: () => EXECUTOR_LOG_NAME,
6955
7267
  GLOBAL_CONFIG_DIR: () => GLOBAL_CONFIG_DIR,
7268
+ GLOBAL_NOTIFICATIONS_FILE_NAME: () => GLOBAL_NOTIFICATIONS_FILE_NAME,
6956
7269
  HISTORY_FILE_NAME: () => HISTORY_FILE_NAME,
6957
7270
  HORIZON_LABELS: () => HORIZON_LABELS,
6958
7271
  HORIZON_LABEL_INFO: () => HORIZON_LABEL_INFO,
7272
+ JOB_REGISTRY: () => JOB_REGISTRY,
6959
7273
  LOCK_FILE_PREFIX: () => LOCK_FILE_PREFIX,
6960
7274
  LOG_DIR: () => LOG_DIR,
6961
7275
  LOG_FILE_NAMES: () => LOG_FILE_NAMES,
@@ -6987,7 +7301,9 @@ __export(dist_exports, {
6987
7301
  analyticsLockPath: () => analyticsLockPath,
6988
7302
  auditLockPath: () => auditLockPath,
6989
7303
  buildDescription: () => buildDescription,
7304
+ buildJobEnvOverrides: () => buildJobEnvOverrides,
6990
7305
  calculateStringSimilarity: () => calculateStringSimilarity,
7306
+ camelToUpperSnake: () => camelToUpperSnake,
6991
7307
  canStartJob: () => canStartJob,
6992
7308
  cancelProcess: () => cancelProcess,
6993
7309
  checkConfigFile: () => checkConfigFile,
@@ -7052,23 +7368,30 @@ __export(dist_exports, {
7052
7368
  formatTelegramPayload: () => formatTelegramPayload,
7053
7369
  generateItemHash: () => generateItemHash,
7054
7370
  generateMarker: () => generateMarker,
7371
+ getAllJobDefs: () => getAllJobDefs,
7055
7372
  getBranchTipTimestamp: () => getBranchTipTimestamp,
7056
7373
  getCrontabInfo: () => getCrontabInfo,
7057
7374
  getDb: () => getDb,
7058
7375
  getDbPath: () => getDbPath,
7059
7376
  getDefaultConfig: () => getDefaultConfig,
7377
+ getDefaultQueuePriority: () => getDefaultQueuePriority,
7060
7378
  getEntries: () => getEntries,
7061
7379
  getEventColor: () => getEventColor,
7062
7380
  getEventEmoji: () => getEventEmoji,
7063
7381
  getEventTitle: () => getEventTitle,
7064
7382
  getHistoryPath: () => getHistoryPath,
7065
7383
  getInFlightCount: () => getInFlightCount,
7384
+ getJobDef: () => getJobDef,
7385
+ getJobDefByCommand: () => getJobDefByCommand,
7386
+ getJobDefByLogName: () => getJobDefByLogName,
7066
7387
  getJobPriority: () => getJobPriority,
7067
7388
  getJobRunsAnalytics: () => getJobRunsAnalytics,
7068
7389
  getLabelsForSection: () => getLabelsForSection,
7069
7390
  getLastExecution: () => getLastExecution,
7070
7391
  getLastLogLines: () => getLastLogLines,
7071
7392
  getLockFilePaths: () => getLockFilePaths,
7393
+ getLockSuffix: () => getLockSuffix,
7394
+ getLogFileNames: () => getLogFileNames,
7072
7395
  getLogInfo: () => getLogInfo,
7073
7396
  getNextPendingJob: () => getNextPendingJob,
7074
7397
  getNextPrdNumber: () => getNextPrdNumber,
@@ -7089,6 +7412,7 @@ __export(dist_exports, {
7089
7412
  getStateFilePath: () => getStateFilePath,
7090
7413
  getStateItem: () => getStateItem,
7091
7414
  getUncheckedItems: () => getUncheckedItems,
7415
+ getValidJobTypes: () => getValidJobTypes,
7092
7416
  groupBySection: () => groupBySection2,
7093
7417
  hasEntry: () => hasEntry,
7094
7418
  hasNewItems: () => hasNewItems,
@@ -7106,6 +7430,7 @@ __export(dist_exports, {
7106
7430
  label: () => label,
7107
7431
  listPrdStatesByStatus: () => listPrdStatesByStatus,
7108
7432
  loadConfig: () => loadConfig,
7433
+ loadGlobalNotificationsConfig: () => loadGlobalNotificationsConfig,
7109
7434
  loadHistory: () => loadHistory,
7110
7435
  loadRegistry: () => loadRegistry,
7111
7436
  loadRoadmapState: () => loadRoadmapState,
@@ -7114,6 +7439,7 @@ __export(dist_exports, {
7114
7439
  markJobRunning: () => markJobRunning,
7115
7440
  markPrdDone: () => markPrdDone,
7116
7441
  migrateJsonToSqlite: () => migrateJsonToSqlite,
7442
+ normalizeJobConfig: () => normalizeJobConfig,
7117
7443
  normalizeSchedulingPriority: () => normalizeSchedulingPriority,
7118
7444
  parsePrdDependencies: () => parsePrdDependencies,
7119
7445
  parseRoadmap: () => parseRoadmap,
@@ -7123,6 +7449,7 @@ __export(dist_exports, {
7123
7449
  prepareBranchWorktree: () => prepareBranchWorktree,
7124
7450
  prepareDetachedWorktree: () => prepareDetachedWorktree,
7125
7451
  projectRuntimeKey: () => projectRuntimeKey,
7452
+ pruneProjectData: () => pruneProjectData,
7126
7453
  qaLockPath: () => qaLockPath,
7127
7454
  readCrontab: () => readCrontab,
7128
7455
  readPrdStates: () => readPrdStates,
@@ -7133,6 +7460,7 @@ __export(dist_exports, {
7133
7460
  removeEntries: () => removeEntries,
7134
7461
  removeEntriesForProject: () => removeEntriesForProject,
7135
7462
  removeJob: () => removeJob,
7463
+ removeProject: () => removeProject,
7136
7464
  renderPrdTemplate: () => renderPrdTemplate,
7137
7465
  renderSlicerPrompt: () => renderSlicerPrompt,
7138
7466
  resetRepositories: () => resetRepositories,
@@ -7146,6 +7474,7 @@ __export(dist_exports, {
7146
7474
  runAnalytics: () => runAnalytics,
7147
7475
  runMigrations: () => runMigrations,
7148
7476
  saveConfig: () => saveConfig,
7477
+ saveGlobalNotificationsConfig: () => saveGlobalNotificationsConfig,
7149
7478
  saveHistory: () => saveHistory,
7150
7479
  saveRegistry: () => saveRegistry,
7151
7480
  saveRoadmapState: () => saveRoadmapState,
@@ -7195,6 +7524,7 @@ var init_dist = __esm({
7195
7524
  init_git_utils();
7196
7525
  init_github();
7197
7526
  init_log_utils();
7527
+ init_global_config();
7198
7528
  init_notify();
7199
7529
  init_prd_discovery();
7200
7530
  init_prd_states();
@@ -7215,45 +7545,54 @@ var init_dist = __esm({
7215
7545
  init_analytics();
7216
7546
  init_prd_template();
7217
7547
  init_slicer_prompt();
7548
+ init_jobs();
7218
7549
  }
7219
7550
  });
7220
7551
 
7221
7552
  // src/cli.ts
7222
7553
  import "reflect-metadata";
7223
7554
  import { Command as Command3 } from "commander";
7224
- import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
7555
+ import { existsSync as existsSync30, readFileSync as readFileSync19 } from "fs";
7225
7556
  import { fileURLToPath as fileURLToPath4 } from "url";
7226
- import { dirname as dirname8, join as join35 } from "path";
7557
+ import { dirname as dirname9, join as join36 } from "path";
7227
7558
 
7228
7559
  // src/commands/init.ts
7229
7560
  init_dist();
7230
- import fs19 from "fs";
7231
- import path19 from "path";
7561
+ import fs20 from "fs";
7562
+ import path20 from "path";
7232
7563
  import { execSync as execSync3 } from "child_process";
7233
7564
  import { fileURLToPath as fileURLToPath2 } from "url";
7234
- import { dirname as dirname4, join as join17 } from "path";
7565
+ import { dirname as dirname5, join as join18 } from "path";
7235
7566
  import * as readline from "readline";
7236
7567
  var __filename = fileURLToPath2(import.meta.url);
7237
- var __dirname2 = dirname4(__filename);
7568
+ var __dirname2 = dirname5(__filename);
7238
7569
  function findTemplatesDir(startDir) {
7239
7570
  let d = startDir;
7240
7571
  for (let i = 0; i < 8; i++) {
7241
- const candidate = join17(d, "templates");
7242
- if (fs19.existsSync(candidate) && fs19.statSync(candidate).isDirectory()) {
7572
+ const candidate = join18(d, "templates");
7573
+ if (fs20.existsSync(candidate) && fs20.statSync(candidate).isDirectory()) {
7243
7574
  return candidate;
7244
7575
  }
7245
- d = dirname4(d);
7576
+ d = dirname5(d);
7246
7577
  }
7247
- return join17(startDir, "templates");
7578
+ return join18(startDir, "templates");
7248
7579
  }
7249
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
+ ];
7250
7589
  function hasPlaywrightDependency(cwd) {
7251
- const packageJsonPath = path19.join(cwd, "package.json");
7252
- if (!fs19.existsSync(packageJsonPath)) {
7590
+ const packageJsonPath = path20.join(cwd, "package.json");
7591
+ if (!fs20.existsSync(packageJsonPath)) {
7253
7592
  return false;
7254
7593
  }
7255
7594
  try {
7256
- const packageJson2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
7595
+ const packageJson2 = JSON.parse(fs20.readFileSync(packageJsonPath, "utf-8"));
7257
7596
  return Boolean(
7258
7597
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
7259
7598
  );
@@ -7265,7 +7604,7 @@ function detectPlaywright(cwd) {
7265
7604
  if (hasPlaywrightDependency(cwd)) {
7266
7605
  return true;
7267
7606
  }
7268
- if (fs19.existsSync(path19.join(cwd, "node_modules", ".bin", "playwright"))) {
7607
+ if (fs20.existsSync(path20.join(cwd, "node_modules", ".bin", "playwright"))) {
7269
7608
  return true;
7270
7609
  }
7271
7610
  try {
@@ -7281,10 +7620,10 @@ function detectPlaywright(cwd) {
7281
7620
  }
7282
7621
  }
7283
7622
  function resolvePlaywrightInstallCommand(cwd) {
7284
- if (fs19.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) {
7623
+ if (fs20.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) {
7285
7624
  return "pnpm add -D @playwright/test";
7286
7625
  }
7287
- if (fs19.existsSync(path19.join(cwd, "yarn.lock"))) {
7626
+ if (fs20.existsSync(path20.join(cwd, "yarn.lock"))) {
7288
7627
  return "yarn add -D @playwright/test";
7289
7628
  }
7290
7629
  return "npm install -D @playwright/test";
@@ -7430,8 +7769,8 @@ function promptProviderSelection(providers) {
7430
7769
  });
7431
7770
  }
7432
7771
  function ensureDir(dirPath) {
7433
- if (!fs19.existsSync(dirPath)) {
7434
- fs19.mkdirSync(dirPath, { recursive: true });
7772
+ if (!fs20.existsSync(dirPath)) {
7773
+ fs20.mkdirSync(dirPath, { recursive: true });
7435
7774
  }
7436
7775
  }
7437
7776
  function buildInitConfig(params) {
@@ -7488,30 +7827,30 @@ function buildInitConfig(params) {
7488
7827
  }
7489
7828
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
7490
7829
  if (customTemplatesDir !== null) {
7491
- const customPath = join17(customTemplatesDir, templateName);
7492
- if (fs19.existsSync(customPath)) {
7830
+ const customPath = join18(customTemplatesDir, templateName);
7831
+ if (fs20.existsSync(customPath)) {
7493
7832
  return { path: customPath, source: "custom" };
7494
7833
  }
7495
7834
  }
7496
- return { path: join17(bundledTemplatesDir, templateName), source: "bundled" };
7835
+ return { path: join18(bundledTemplatesDir, templateName), source: "bundled" };
7497
7836
  }
7498
7837
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
7499
- if (fs19.existsSync(targetPath) && !force) {
7838
+ if (fs20.existsSync(targetPath) && !force) {
7500
7839
  console.log(` Skipped (exists): ${targetPath}`);
7501
7840
  return { created: false, source: source ?? "bundled" };
7502
7841
  }
7503
- const templatePath = sourcePath ?? join17(TEMPLATES_DIR, templateName);
7842
+ const templatePath = sourcePath ?? join18(TEMPLATES_DIR, templateName);
7504
7843
  const resolvedSource = source ?? "bundled";
7505
- let content = fs19.readFileSync(templatePath, "utf-8");
7844
+ let content = fs20.readFileSync(templatePath, "utf-8");
7506
7845
  for (const [key, value] of Object.entries(replacements)) {
7507
7846
  content = content.replaceAll(key, value);
7508
7847
  }
7509
- fs19.writeFileSync(targetPath, content);
7848
+ fs20.writeFileSync(targetPath, content);
7510
7849
  console.log(` Created: ${targetPath} (${resolvedSource})`);
7511
7850
  return { created: true, source: resolvedSource };
7512
7851
  }
7513
7852
  function addToGitignore(cwd) {
7514
- const gitignorePath = path19.join(cwd, ".gitignore");
7853
+ const gitignorePath = path20.join(cwd, ".gitignore");
7515
7854
  const entries = [
7516
7855
  {
7517
7856
  pattern: "/logs/",
@@ -7525,13 +7864,13 @@ function addToGitignore(cwd) {
7525
7864
  },
7526
7865
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
7527
7866
  ];
7528
- if (!fs19.existsSync(gitignorePath)) {
7867
+ if (!fs20.existsSync(gitignorePath)) {
7529
7868
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
7530
- fs19.writeFileSync(gitignorePath, lines.join("\n"));
7869
+ fs20.writeFileSync(gitignorePath, lines.join("\n"));
7531
7870
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
7532
7871
  return;
7533
7872
  }
7534
- const content = fs19.readFileSync(gitignorePath, "utf-8");
7873
+ const content = fs20.readFileSync(gitignorePath, "utf-8");
7535
7874
  const missing = entries.filter((e) => !e.check(content));
7536
7875
  if (missing.length === 0) {
7537
7876
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -7539,15 +7878,70 @@ function addToGitignore(cwd) {
7539
7878
  }
7540
7879
  const additions = missing.map((e) => e.pattern).join("\n");
7541
7880
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
7542
- fs19.writeFileSync(gitignorePath, newContent);
7881
+ fs20.writeFileSync(gitignorePath, newContent);
7543
7882
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
7544
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
+ }
7545
7939
  function initCommand(program2) {
7546
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) => {
7547
7941
  const cwd = process.cwd();
7548
7942
  const force = options.force || false;
7549
7943
  const prdDir = options.prdDir || DEFAULT_PRD_DIR;
7550
- const totalSteps = 12;
7944
+ const totalSteps = 13;
7551
7945
  const interactive = isInteractiveInitSession();
7552
7946
  console.log();
7553
7947
  header("Night Watch CLI - Initializing");
@@ -7660,28 +8054,28 @@ function initCommand(program2) {
7660
8054
  "${DEFAULT_BRANCH}": defaultBranch
7661
8055
  };
7662
8056
  step(6, totalSteps, "Creating PRD directory structure...");
7663
- const prdDirPath = path19.join(cwd, prdDir);
7664
- const doneDirPath = path19.join(prdDirPath, "done");
8057
+ const prdDirPath = path20.join(cwd, prdDir);
8058
+ const doneDirPath = path20.join(prdDirPath, "done");
7665
8059
  ensureDir(doneDirPath);
7666
8060
  success(`Created ${prdDirPath}/`);
7667
8061
  success(`Created ${doneDirPath}/`);
7668
8062
  step(7, totalSteps, "Creating logs directory...");
7669
- const logsPath = path19.join(cwd, LOG_DIR);
8063
+ const logsPath = path20.join(cwd, LOG_DIR);
7670
8064
  ensureDir(logsPath);
7671
8065
  success(`Created ${logsPath}/`);
7672
8066
  addToGitignore(cwd);
7673
8067
  step(8, totalSteps, "Creating instructions directory...");
7674
- const instructionsDir = path19.join(cwd, "instructions");
8068
+ const instructionsDir = path20.join(cwd, "instructions");
7675
8069
  ensureDir(instructionsDir);
7676
8070
  success(`Created ${instructionsDir}/`);
7677
8071
  const existingConfig = loadConfig(cwd);
7678
- const customTemplatesDirPath = path19.join(cwd, existingConfig.templatesDir);
7679
- const customTemplatesDir = fs19.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
8072
+ const customTemplatesDirPath = path20.join(cwd, existingConfig.templatesDir);
8073
+ const customTemplatesDir = fs20.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7680
8074
  const templateSources = [];
7681
8075
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
7682
8076
  const nwResult = processTemplate(
7683
8077
  "executor.md",
7684
- path19.join(instructionsDir, "executor.md"),
8078
+ path20.join(instructionsDir, "executor.md"),
7685
8079
  replacements,
7686
8080
  force,
7687
8081
  nwResolution.path,
@@ -7695,7 +8089,7 @@ function initCommand(program2) {
7695
8089
  );
7696
8090
  const peResult = processTemplate(
7697
8091
  "prd-executor.md",
7698
- path19.join(instructionsDir, "prd-executor.md"),
8092
+ path20.join(instructionsDir, "prd-executor.md"),
7699
8093
  replacements,
7700
8094
  force,
7701
8095
  peResolution.path,
@@ -7705,7 +8099,7 @@ function initCommand(program2) {
7705
8099
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
7706
8100
  const prResult = processTemplate(
7707
8101
  "pr-reviewer.md",
7708
- path19.join(instructionsDir, "pr-reviewer.md"),
8102
+ path20.join(instructionsDir, "pr-reviewer.md"),
7709
8103
  replacements,
7710
8104
  force,
7711
8105
  prResolution.path,
@@ -7715,7 +8109,7 @@ function initCommand(program2) {
7715
8109
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
7716
8110
  const qaResult = processTemplate(
7717
8111
  "qa.md",
7718
- path19.join(instructionsDir, "qa.md"),
8112
+ path20.join(instructionsDir, "qa.md"),
7719
8113
  replacements,
7720
8114
  force,
7721
8115
  qaResolution.path,
@@ -7725,7 +8119,7 @@ function initCommand(program2) {
7725
8119
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
7726
8120
  const auditResult = processTemplate(
7727
8121
  "audit.md",
7728
- path19.join(instructionsDir, "audit.md"),
8122
+ path20.join(instructionsDir, "audit.md"),
7729
8123
  replacements,
7730
8124
  force,
7731
8125
  auditResolution.path,
@@ -7733,8 +8127,8 @@ function initCommand(program2) {
7733
8127
  );
7734
8128
  templateSources.push({ name: "audit.md", source: auditResult.source });
7735
8129
  step(9, totalSteps, "Creating configuration file...");
7736
- const configPath = path19.join(cwd, CONFIG_FILE_NAME);
7737
- if (fs19.existsSync(configPath) && !force) {
8130
+ const configPath = path20.join(cwd, CONFIG_FILE_NAME);
8131
+ if (fs20.existsSync(configPath) && !force) {
7738
8132
  console.log(` Skipped (exists): ${configPath}`);
7739
8133
  } else {
7740
8134
  const config = buildInitConfig({
@@ -7744,11 +8138,11 @@ function initCommand(program2) {
7744
8138
  reviewerEnabled,
7745
8139
  prdDir
7746
8140
  });
7747
- fs19.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
8141
+ fs20.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7748
8142
  success(`Created ${configPath}`);
7749
8143
  }
7750
8144
  step(10, totalSteps, "Setting up GitHub Project board...");
7751
- const existingRaw = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
8145
+ const existingRaw = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
7752
8146
  const existingBoard = existingRaw.boardProvider;
7753
8147
  let boardSetupStatus = "Skipped";
7754
8148
  if (existingBoard?.projectNumber && !force) {
@@ -7770,13 +8164,13 @@ function initCommand(program2) {
7770
8164
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
7771
8165
  const boardTitle = `${projectName} Night Watch`;
7772
8166
  const board = await provider.setupBoard(boardTitle);
7773
- const rawConfig = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
8167
+ const rawConfig = JSON.parse(fs20.readFileSync(configPath, "utf-8"));
7774
8168
  rawConfig.boardProvider = {
7775
8169
  enabled: true,
7776
8170
  provider: "github",
7777
8171
  projectNumber: board.number
7778
8172
  };
7779
- fs19.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
8173
+ fs20.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7780
8174
  boardSetupStatus = `Created (#${board.number})`;
7781
8175
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
7782
8176
  } catch (boardErr) {
@@ -7798,7 +8192,19 @@ function initCommand(program2) {
7798
8192
  ` Warning: Could not register in global registry: ${regErr instanceof Error ? regErr.message : String(regErr)}`
7799
8193
  );
7800
8194
  }
7801
- 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!");
7802
8208
  header("Initialization Complete");
7803
8209
  const filesTable = createTable({ head: ["Created Files", ""] });
7804
8210
  filesTable.push(["PRD Directory", `${prdDir}/done/`]);
@@ -7811,6 +8217,15 @@ function initCommand(program2) {
7811
8217
  filesTable.push(["Config File", CONFIG_FILE_NAME]);
7812
8218
  filesTable.push(["Board Setup", boardSetupStatus]);
7813
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]);
7814
8229
  console.log(filesTable.toString());
7815
8230
  header("Configuration");
7816
8231
  label("Provider", selectedProvider);
@@ -7822,6 +8237,9 @@ function initCommand(program2) {
7822
8237
  info("2. Run `night-watch install` to set up cron jobs");
7823
8238
  info("3. Run `night-watch doctor` to verify the full setup");
7824
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
+ }
7825
8243
  console.log();
7826
8244
  });
7827
8245
  }
@@ -7900,8 +8318,8 @@ function getTelegramStatusWebhooks(config) {
7900
8318
  }
7901
8319
 
7902
8320
  // src/commands/run.ts
7903
- import * as fs20 from "fs";
7904
- import * as path20 from "path";
8321
+ import * as fs21 from "fs";
8322
+ import * as path21 from "path";
7905
8323
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
7906
8324
  if (exitCode === 124) {
7907
8325
  return "run_timeout";
@@ -7933,12 +8351,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7933
8351
  return scriptStatus === "skip_no_eligible_prd";
7934
8352
  }
7935
8353
  function getCrossProjectFallbackCandidates(currentProjectDir) {
7936
- const current = path20.resolve(currentProjectDir);
8354
+ const current = path21.resolve(currentProjectDir);
7937
8355
  const { valid, invalid } = validateRegistry();
7938
8356
  for (const entry of invalid) {
7939
8357
  warn(`Skipping invalid registry entry: ${entry.path}`);
7940
8358
  }
7941
- return valid.filter((entry) => path20.resolve(entry.path) !== current);
8359
+ return valid.filter((entry) => path21.resolve(entry.path) !== current);
7942
8360
  }
7943
8361
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
7944
8362
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -7948,7 +8366,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7948
8366
  if (nonTelegramWebhooks.length > 0) {
7949
8367
  const _rateLimitCtx = {
7950
8368
  event: "rate_limit_fallback",
7951
- projectName: path20.basename(projectDir),
8369
+ projectName: path21.basename(projectDir),
7952
8370
  exitCode,
7953
8371
  provider: config.provider
7954
8372
  };
@@ -7976,7 +8394,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7976
8394
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
7977
8395
  const _ctx = {
7978
8396
  event,
7979
- projectName: path20.basename(projectDir),
8397
+ projectName: path21.basename(projectDir),
7980
8398
  exitCode,
7981
8399
  provider: config.provider,
7982
8400
  prdName: scriptResult?.data.prd,
@@ -8138,20 +8556,20 @@ function applyCliOverrides(config, options) {
8138
8556
  return overridden;
8139
8557
  }
8140
8558
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
8141
- const absolutePrdDir = path20.join(projectDir, prdDir);
8142
- const doneDir = path20.join(absolutePrdDir, "done");
8559
+ const absolutePrdDir = path21.join(projectDir, prdDir);
8560
+ const doneDir = path21.join(absolutePrdDir, "done");
8143
8561
  const pending = [];
8144
8562
  const completed = [];
8145
- if (fs20.existsSync(absolutePrdDir)) {
8146
- const entries = fs20.readdirSync(absolutePrdDir, { withFileTypes: true });
8563
+ if (fs21.existsSync(absolutePrdDir)) {
8564
+ const entries = fs21.readdirSync(absolutePrdDir, { withFileTypes: true });
8147
8565
  for (const entry of entries) {
8148
8566
  if (entry.isFile() && entry.name.endsWith(".md")) {
8149
- const claimPath = path20.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
8567
+ const claimPath = path21.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
8150
8568
  let claimed = false;
8151
8569
  let claimInfo = null;
8152
- if (fs20.existsSync(claimPath)) {
8570
+ if (fs21.existsSync(claimPath)) {
8153
8571
  try {
8154
- const content = fs20.readFileSync(claimPath, "utf-8");
8572
+ const content = fs21.readFileSync(claimPath, "utf-8");
8155
8573
  const data = JSON.parse(content);
8156
8574
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
8157
8575
  if (age < maxRuntime) {
@@ -8165,8 +8583,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
8165
8583
  }
8166
8584
  }
8167
8585
  }
8168
- if (fs20.existsSync(doneDir)) {
8169
- const entries = fs20.readdirSync(doneDir, { withFileTypes: true });
8586
+ if (fs21.existsSync(doneDir)) {
8587
+ const entries = fs21.readdirSync(doneDir, { withFileTypes: true });
8170
8588
  for (const entry of entries) {
8171
8589
  if (entry.isFile() && entry.name.endsWith(".md")) {
8172
8590
  completed.push(entry.name);
@@ -8326,7 +8744,7 @@ ${stderr}`);
8326
8744
  // src/commands/review.ts
8327
8745
  init_dist();
8328
8746
  import { execFileSync as execFileSync5 } from "child_process";
8329
- import * as path21 from "path";
8747
+ import * as path22 from "path";
8330
8748
  function shouldSendReviewNotification(scriptStatus) {
8331
8749
  if (!scriptStatus) {
8332
8750
  return true;
@@ -8566,7 +8984,7 @@ ${stderr}`);
8566
8984
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
8567
8985
  const _reviewCtx = {
8568
8986
  event: "review_completed",
8569
- projectName: path21.basename(projectDir),
8987
+ projectName: path22.basename(projectDir),
8570
8988
  exitCode,
8571
8989
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8572
8990
  prUrl: prDetails?.url,
@@ -8587,7 +9005,7 @@ ${stderr}`);
8587
9005
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
8588
9006
  const _mergeCtx = {
8589
9007
  event: "pr_auto_merged",
8590
- projectName: path21.basename(projectDir),
9008
+ projectName: path22.basename(projectDir),
8591
9009
  exitCode,
8592
9010
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8593
9011
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -8612,7 +9030,7 @@ ${stderr}`);
8612
9030
 
8613
9031
  // src/commands/qa.ts
8614
9032
  init_dist();
8615
- import * as path22 from "path";
9033
+ import * as path23 from "path";
8616
9034
  function shouldSendQaNotification(scriptStatus) {
8617
9035
  if (!scriptStatus) {
8618
9036
  return true;
@@ -8746,7 +9164,7 @@ ${stderr}`);
8746
9164
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
8747
9165
  const _qaCtx = {
8748
9166
  event: "qa_completed",
8749
- projectName: path22.basename(projectDir),
9167
+ projectName: path23.basename(projectDir),
8750
9168
  exitCode,
8751
9169
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8752
9170
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -8772,8 +9190,8 @@ ${stderr}`);
8772
9190
 
8773
9191
  // src/commands/audit.ts
8774
9192
  init_dist();
8775
- import * as fs21 from "fs";
8776
- import * as path23 from "path";
9193
+ import * as fs22 from "fs";
9194
+ import * as path24 from "path";
8777
9195
  function buildEnvVars4(config, options) {
8778
9196
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
8779
9197
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -8816,7 +9234,7 @@ function auditCommand(program2) {
8816
9234
  configTable.push(["Provider", auditProvider]);
8817
9235
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
8818
9236
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
8819
- configTable.push(["Report File", path23.join(projectDir, "logs", "audit-report.md")]);
9237
+ configTable.push(["Report File", path24.join(projectDir, "logs", "audit-report.md")]);
8820
9238
  console.log(configTable.toString());
8821
9239
  header("Provider Invocation");
8822
9240
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -8851,8 +9269,8 @@ ${stderr}`);
8851
9269
  } else if (scriptResult?.status?.startsWith("skip_")) {
8852
9270
  spinner.succeed("Code audit skipped");
8853
9271
  } else {
8854
- const reportPath = path23.join(projectDir, "logs", "audit-report.md");
8855
- if (!fs21.existsSync(reportPath)) {
9272
+ const reportPath = path24.join(projectDir, "logs", "audit-report.md");
9273
+ if (!fs22.existsSync(reportPath)) {
8856
9274
  spinner.fail("Code audit finished without a report file");
8857
9275
  process.exit(1);
8858
9276
  }
@@ -8863,9 +9281,9 @@ ${stderr}`);
8863
9281
  const providerExit = scriptResult?.data?.provider_exit;
8864
9282
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
8865
9283
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
8866
- const logPath = path23.join(projectDir, "logs", "audit.log");
8867
- if (fs21.existsSync(logPath)) {
8868
- const logLines = fs21.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
9284
+ const logPath = path24.join(projectDir, "logs", "audit.log");
9285
+ if (fs22.existsSync(logPath)) {
9286
+ const logLines = fs22.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
8869
9287
  if (logLines.length > 0) {
8870
9288
  process.stderr.write(logLines.join("\n") + "\n");
8871
9289
  }
@@ -8943,16 +9361,16 @@ function analyticsCommand(program2) {
8943
9361
  // src/commands/install.ts
8944
9362
  init_dist();
8945
9363
  import { execSync as execSync4 } from "child_process";
8946
- import * as path24 from "path";
8947
- import * as fs22 from "fs";
9364
+ import * as path25 from "path";
9365
+ import * as fs23 from "fs";
8948
9366
  function shellQuote(value) {
8949
9367
  return `'${value.replace(/'/g, `'"'"'`)}'`;
8950
9368
  }
8951
9369
  function getNightWatchBinPath() {
8952
9370
  try {
8953
9371
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
8954
- const binPath = path24.join(npmBin, "night-watch");
8955
- if (fs22.existsSync(binPath)) {
9372
+ const binPath = path25.join(npmBin, "night-watch");
9373
+ if (fs23.existsSync(binPath)) {
8956
9374
  return binPath;
8957
9375
  }
8958
9376
  } catch {
@@ -8965,17 +9383,17 @@ function getNightWatchBinPath() {
8965
9383
  }
8966
9384
  function getNodeBinDir() {
8967
9385
  if (process.execPath && process.execPath !== "node") {
8968
- return path24.dirname(process.execPath);
9386
+ return path25.dirname(process.execPath);
8969
9387
  }
8970
9388
  try {
8971
9389
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
8972
- return path24.dirname(nodePath);
9390
+ return path25.dirname(nodePath);
8973
9391
  } catch {
8974
9392
  return "";
8975
9393
  }
8976
9394
  }
8977
9395
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
8978
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path24.dirname(nightWatchBin) : "";
9396
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path25.dirname(nightWatchBin) : "";
8979
9397
  const pathParts = Array.from(
8980
9398
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
8981
9399
  );
@@ -8991,12 +9409,12 @@ function performInstall(projectDir, config, options) {
8991
9409
  const nightWatchBin = getNightWatchBinPath();
8992
9410
  const projectName = getProjectName(projectDir);
8993
9411
  const marker = generateMarker(projectName);
8994
- const logDir = path24.join(projectDir, LOG_DIR);
8995
- if (!fs22.existsSync(logDir)) {
8996
- fs22.mkdirSync(logDir, { recursive: true });
9412
+ const logDir = path25.join(projectDir, LOG_DIR);
9413
+ if (!fs23.existsSync(logDir)) {
9414
+ fs23.mkdirSync(logDir, { recursive: true });
8997
9415
  }
8998
- const executorLog = path24.join(logDir, "executor.log");
8999
- const reviewerLog = path24.join(logDir, "reviewer.log");
9416
+ const executorLog = path25.join(logDir, "executor.log");
9417
+ const reviewerLog = path25.join(logDir, "reviewer.log");
9000
9418
  if (!options?.force) {
9001
9419
  const existingEntries2 = Array.from(
9002
9420
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -9033,7 +9451,7 @@ function performInstall(projectDir, config, options) {
9033
9451
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
9034
9452
  if (installSlicer) {
9035
9453
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
9036
- const slicerLog = path24.join(logDir, "slicer.log");
9454
+ const slicerLog = path25.join(logDir, "slicer.log");
9037
9455
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
9038
9456
  entries.push(slicerEntry);
9039
9457
  }
@@ -9041,7 +9459,7 @@ function performInstall(projectDir, config, options) {
9041
9459
  const installQa = disableQa ? false : config.qa.enabled;
9042
9460
  if (installQa) {
9043
9461
  const qaSchedule = config.qa.schedule;
9044
- const qaLog = path24.join(logDir, "qa.log");
9462
+ const qaLog = path25.join(logDir, "qa.log");
9045
9463
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
9046
9464
  entries.push(qaEntry);
9047
9465
  }
@@ -9049,7 +9467,7 @@ function performInstall(projectDir, config, options) {
9049
9467
  const installAudit = disableAudit ? false : config.audit.enabled;
9050
9468
  if (installAudit) {
9051
9469
  const auditSchedule = config.audit.schedule;
9052
- const auditLog = path24.join(logDir, "audit.log");
9470
+ const auditLog = path25.join(logDir, "audit.log");
9053
9471
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
9054
9472
  entries.push(auditEntry);
9055
9473
  }
@@ -9057,7 +9475,7 @@ function performInstall(projectDir, config, options) {
9057
9475
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
9058
9476
  if (installAnalytics) {
9059
9477
  const analyticsSchedule = config.analytics.schedule;
9060
- const analyticsLog = path24.join(logDir, "analytics.log");
9478
+ const analyticsLog = path25.join(logDir, "analytics.log");
9061
9479
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9062
9480
  entries.push(analyticsEntry);
9063
9481
  }
@@ -9088,12 +9506,12 @@ function installCommand(program2) {
9088
9506
  const nightWatchBin = getNightWatchBinPath();
9089
9507
  const projectName = getProjectName(projectDir);
9090
9508
  const marker = generateMarker(projectName);
9091
- const logDir = path24.join(projectDir, LOG_DIR);
9092
- if (!fs22.existsSync(logDir)) {
9093
- fs22.mkdirSync(logDir, { recursive: true });
9509
+ const logDir = path25.join(projectDir, LOG_DIR);
9510
+ if (!fs23.existsSync(logDir)) {
9511
+ fs23.mkdirSync(logDir, { recursive: true });
9094
9512
  }
9095
- const executorLog = path24.join(logDir, "executor.log");
9096
- const reviewerLog = path24.join(logDir, "reviewer.log");
9513
+ const executorLog = path25.join(logDir, "executor.log");
9514
+ const reviewerLog = path25.join(logDir, "reviewer.log");
9097
9515
  const existingEntries = Array.from(
9098
9516
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
9099
9517
  );
@@ -9129,7 +9547,7 @@ function installCommand(program2) {
9129
9547
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
9130
9548
  let slicerLog;
9131
9549
  if (installSlicer) {
9132
- slicerLog = path24.join(logDir, "slicer.log");
9550
+ slicerLog = path25.join(logDir, "slicer.log");
9133
9551
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
9134
9552
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
9135
9553
  entries.push(slicerEntry);
@@ -9138,7 +9556,7 @@ function installCommand(program2) {
9138
9556
  const installQa = disableQa ? false : config.qa.enabled;
9139
9557
  let qaLog;
9140
9558
  if (installQa) {
9141
- qaLog = path24.join(logDir, "qa.log");
9559
+ qaLog = path25.join(logDir, "qa.log");
9142
9560
  const qaSchedule = config.qa.schedule;
9143
9561
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
9144
9562
  entries.push(qaEntry);
@@ -9147,7 +9565,7 @@ function installCommand(program2) {
9147
9565
  const installAudit = disableAudit ? false : config.audit.enabled;
9148
9566
  let auditLog;
9149
9567
  if (installAudit) {
9150
- auditLog = path24.join(logDir, "audit.log");
9568
+ auditLog = path25.join(logDir, "audit.log");
9151
9569
  const auditSchedule = config.audit.schedule;
9152
9570
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
9153
9571
  entries.push(auditEntry);
@@ -9156,7 +9574,7 @@ function installCommand(program2) {
9156
9574
  const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
9157
9575
  let analyticsLog;
9158
9576
  if (installAnalytics) {
9159
- analyticsLog = path24.join(logDir, "analytics.log");
9577
+ analyticsLog = path25.join(logDir, "analytics.log");
9160
9578
  const analyticsSchedule = config.analytics.schedule;
9161
9579
  const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9162
9580
  entries.push(analyticsEntry);
@@ -9204,8 +9622,8 @@ function installCommand(program2) {
9204
9622
 
9205
9623
  // src/commands/uninstall.ts
9206
9624
  init_dist();
9207
- import * as path25 from "path";
9208
- import * as fs23 from "fs";
9625
+ import * as path26 from "path";
9626
+ import * as fs24 from "fs";
9209
9627
  function performUninstall(projectDir, options) {
9210
9628
  try {
9211
9629
  const projectName = getProjectName(projectDir);
@@ -9220,19 +9638,19 @@ function performUninstall(projectDir, options) {
9220
9638
  const removedCount = removeEntriesForProject(projectDir, marker);
9221
9639
  unregisterProject(projectDir);
9222
9640
  if (!options?.keepLogs) {
9223
- const logDir = path25.join(projectDir, "logs");
9224
- if (fs23.existsSync(logDir)) {
9641
+ const logDir = path26.join(projectDir, "logs");
9642
+ if (fs24.existsSync(logDir)) {
9225
9643
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
9226
9644
  logFiles.forEach((logFile) => {
9227
- const logPath = path25.join(logDir, logFile);
9228
- if (fs23.existsSync(logPath)) {
9229
- fs23.unlinkSync(logPath);
9645
+ const logPath = path26.join(logDir, logFile);
9646
+ if (fs24.existsSync(logPath)) {
9647
+ fs24.unlinkSync(logPath);
9230
9648
  }
9231
9649
  });
9232
9650
  try {
9233
- const remainingFiles = fs23.readdirSync(logDir);
9651
+ const remainingFiles = fs24.readdirSync(logDir);
9234
9652
  if (remainingFiles.length === 0) {
9235
- fs23.rmdirSync(logDir);
9653
+ fs24.rmdirSync(logDir);
9236
9654
  }
9237
9655
  } catch {
9238
9656
  }
@@ -9265,21 +9683,21 @@ function uninstallCommand(program2) {
9265
9683
  existingEntries.forEach((entry) => dim(` ${entry}`));
9266
9684
  const removedCount = removeEntriesForProject(projectDir, marker);
9267
9685
  if (!options.keepLogs) {
9268
- const logDir = path25.join(projectDir, "logs");
9269
- if (fs23.existsSync(logDir)) {
9686
+ const logDir = path26.join(projectDir, "logs");
9687
+ if (fs24.existsSync(logDir)) {
9270
9688
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
9271
9689
  let logsRemoved = 0;
9272
9690
  logFiles.forEach((logFile) => {
9273
- const logPath = path25.join(logDir, logFile);
9274
- if (fs23.existsSync(logPath)) {
9275
- fs23.unlinkSync(logPath);
9691
+ const logPath = path26.join(logDir, logFile);
9692
+ if (fs24.existsSync(logPath)) {
9693
+ fs24.unlinkSync(logPath);
9276
9694
  logsRemoved++;
9277
9695
  }
9278
9696
  });
9279
9697
  try {
9280
- const remainingFiles = fs23.readdirSync(logDir);
9698
+ const remainingFiles = fs24.readdirSync(logDir);
9281
9699
  if (remainingFiles.length === 0) {
9282
- fs23.rmdirSync(logDir);
9700
+ fs24.rmdirSync(logDir);
9283
9701
  }
9284
9702
  } catch {
9285
9703
  }
@@ -9515,14 +9933,14 @@ function statusCommand(program2) {
9515
9933
  // src/commands/logs.ts
9516
9934
  init_dist();
9517
9935
  import { spawn as spawn3 } from "child_process";
9518
- import * as path26 from "path";
9519
- import * as fs24 from "fs";
9936
+ import * as path27 from "path";
9937
+ import * as fs25 from "fs";
9520
9938
  function getLastLines(filePath, lineCount) {
9521
- if (!fs24.existsSync(filePath)) {
9939
+ if (!fs25.existsSync(filePath)) {
9522
9940
  return `Log file not found: ${filePath}`;
9523
9941
  }
9524
9942
  try {
9525
- const content = fs24.readFileSync(filePath, "utf-8");
9943
+ const content = fs25.readFileSync(filePath, "utf-8");
9526
9944
  const lines = content.trim().split("\n");
9527
9945
  return lines.slice(-lineCount).join("\n");
9528
9946
  } catch (error2) {
@@ -9530,7 +9948,7 @@ function getLastLines(filePath, lineCount) {
9530
9948
  }
9531
9949
  }
9532
9950
  function followLog(filePath) {
9533
- if (!fs24.existsSync(filePath)) {
9951
+ if (!fs25.existsSync(filePath)) {
9534
9952
  console.log(`Log file not found: ${filePath}`);
9535
9953
  console.log("The log file will be created when the first execution runs.");
9536
9954
  return;
@@ -9550,13 +9968,13 @@ function logsCommand(program2) {
9550
9968
  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) => {
9551
9969
  try {
9552
9970
  const projectDir = process.cwd();
9553
- const logDir = path26.join(projectDir, LOG_DIR);
9971
+ const logDir = path27.join(projectDir, LOG_DIR);
9554
9972
  const lineCount = parseInt(options.lines || "50", 10);
9555
- const executorLog = path26.join(logDir, EXECUTOR_LOG_FILE);
9556
- const reviewerLog = path26.join(logDir, REVIEWER_LOG_FILE);
9557
- const qaLog = path26.join(logDir, `${QA_LOG_NAME}.log`);
9558
- const auditLog = path26.join(logDir, `${AUDIT_LOG_NAME}.log`);
9559
- const plannerLog = path26.join(logDir, `${PLANNER_LOG_NAME}.log`);
9973
+ const executorLog = path27.join(logDir, EXECUTOR_LOG_FILE);
9974
+ const reviewerLog = path27.join(logDir, REVIEWER_LOG_FILE);
9975
+ const qaLog = path27.join(logDir, `${QA_LOG_NAME}.log`);
9976
+ const auditLog = path27.join(logDir, `${AUDIT_LOG_NAME}.log`);
9977
+ const plannerLog = path27.join(logDir, `${PLANNER_LOG_NAME}.log`);
9560
9978
  const logType = options.type?.toLowerCase() || "all";
9561
9979
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
9562
9980
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
@@ -9620,15 +10038,15 @@ function logsCommand(program2) {
9620
10038
 
9621
10039
  // src/commands/prd.ts
9622
10040
  init_dist();
9623
- import * as fs25 from "fs";
9624
- import * as path27 from "path";
10041
+ import * as fs26 from "fs";
10042
+ import * as path28 from "path";
9625
10043
  import * as readline2 from "readline";
9626
10044
  function slugify2(name) {
9627
10045
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9628
10046
  }
9629
10047
  function getNextPrdNumber2(prdDir) {
9630
- if (!fs25.existsSync(prdDir)) return 1;
9631
- const files = fs25.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
10048
+ if (!fs26.existsSync(prdDir)) return 1;
10049
+ const files = fs26.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
9632
10050
  const numbers = files.map((f) => {
9633
10051
  const match = f.match(/^(\d+)-/);
9634
10052
  return match ? parseInt(match[1], 10) : 0;
@@ -9649,10 +10067,10 @@ function parseDependencies(content) {
9649
10067
  }
9650
10068
  function isClaimActive(claimPath, maxRuntime) {
9651
10069
  try {
9652
- if (!fs25.existsSync(claimPath)) {
10070
+ if (!fs26.existsSync(claimPath)) {
9653
10071
  return { active: false };
9654
10072
  }
9655
- const content = fs25.readFileSync(claimPath, "utf-8");
10073
+ const content = fs26.readFileSync(claimPath, "utf-8");
9656
10074
  const claim = JSON.parse(content);
9657
10075
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
9658
10076
  if (age < maxRuntime) {
@@ -9668,9 +10086,9 @@ function prdCommand(program2) {
9668
10086
  prd.command("create").description("Generate a new PRD markdown file from template").argument("<name>", "PRD name (used for title and filename)").option("-i, --interactive", "Prompt for complexity, dependencies, and phase count", false).option("-t, --template <path>", "Path to a custom template file").option("--deps <files>", "Comma-separated dependency filenames").option("--phases <count>", "Number of execution phases", "3").option("--no-number", "Skip auto-numbering prefix").action(async (name, options) => {
9669
10087
  const projectDir = process.cwd();
9670
10088
  const config = loadConfig(projectDir);
9671
- const prdDir = path27.join(projectDir, config.prdDir);
9672
- if (!fs25.existsSync(prdDir)) {
9673
- fs25.mkdirSync(prdDir, { recursive: true });
10089
+ const prdDir = path28.join(projectDir, config.prdDir);
10090
+ if (!fs26.existsSync(prdDir)) {
10091
+ fs26.mkdirSync(prdDir, { recursive: true });
9674
10092
  }
9675
10093
  let complexityScore = 5;
9676
10094
  let dependsOn = [];
@@ -9729,20 +10147,20 @@ function prdCommand(program2) {
9729
10147
  } else {
9730
10148
  filename = `${slug}.md`;
9731
10149
  }
9732
- const filePath = path27.join(prdDir, filename);
9733
- if (fs25.existsSync(filePath)) {
10150
+ const filePath = path28.join(prdDir, filename);
10151
+ if (fs26.existsSync(filePath)) {
9734
10152
  error(`File already exists: ${filePath}`);
9735
10153
  dim("Use a different name or remove the existing file.");
9736
10154
  process.exit(1);
9737
10155
  }
9738
10156
  let customTemplate;
9739
10157
  if (options.template) {
9740
- const templatePath = path27.resolve(options.template);
9741
- if (!fs25.existsSync(templatePath)) {
10158
+ const templatePath = path28.resolve(options.template);
10159
+ if (!fs26.existsSync(templatePath)) {
9742
10160
  error(`Template file not found: ${templatePath}`);
9743
10161
  process.exit(1);
9744
10162
  }
9745
- customTemplate = fs25.readFileSync(templatePath, "utf-8");
10163
+ customTemplate = fs26.readFileSync(templatePath, "utf-8");
9746
10164
  }
9747
10165
  const vars = {
9748
10166
  title: name,
@@ -9753,7 +10171,7 @@ function prdCommand(program2) {
9753
10171
  phaseCount
9754
10172
  };
9755
10173
  const content = renderPrdTemplate(vars, customTemplate);
9756
- fs25.writeFileSync(filePath, content, "utf-8");
10174
+ fs26.writeFileSync(filePath, content, "utf-8");
9757
10175
  header("PRD Created");
9758
10176
  success(`Created: ${filePath}`);
9759
10177
  info(`Title: ${name}`);
@@ -9765,15 +10183,15 @@ function prdCommand(program2) {
9765
10183
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
9766
10184
  const projectDir = process.cwd();
9767
10185
  const config = loadConfig(projectDir);
9768
- const absolutePrdDir = path27.join(projectDir, config.prdDir);
9769
- const doneDir = path27.join(absolutePrdDir, "done");
10186
+ const absolutePrdDir = path28.join(projectDir, config.prdDir);
10187
+ const doneDir = path28.join(absolutePrdDir, "done");
9770
10188
  const pending = [];
9771
- if (fs25.existsSync(absolutePrdDir)) {
9772
- const files = fs25.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
10189
+ if (fs26.existsSync(absolutePrdDir)) {
10190
+ const files = fs26.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
9773
10191
  for (const file of files) {
9774
- const content = fs25.readFileSync(path27.join(absolutePrdDir, file), "utf-8");
10192
+ const content = fs26.readFileSync(path28.join(absolutePrdDir, file), "utf-8");
9775
10193
  const deps = parseDependencies(content);
9776
- const claimPath = path27.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
10194
+ const claimPath = path28.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9777
10195
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
9778
10196
  pending.push({
9779
10197
  name: file,
@@ -9784,10 +10202,10 @@ function prdCommand(program2) {
9784
10202
  }
9785
10203
  }
9786
10204
  const done = [];
9787
- if (fs25.existsSync(doneDir)) {
9788
- const files = fs25.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
10205
+ if (fs26.existsSync(doneDir)) {
10206
+ const files = fs26.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
9789
10207
  for (const file of files) {
9790
- const content = fs25.readFileSync(path27.join(doneDir, file), "utf-8");
10208
+ const content = fs26.readFileSync(path28.join(doneDir, file), "utf-8");
9791
10209
  const deps = parseDependencies(content);
9792
10210
  done.push({ name: file, dependencies: deps });
9793
10211
  }
@@ -9826,7 +10244,7 @@ import blessed6 from "blessed";
9826
10244
  // src/commands/dashboard/tab-status.ts
9827
10245
  init_dist();
9828
10246
  import blessed from "blessed";
9829
- import * as fs26 from "fs";
10247
+ import * as fs27 from "fs";
9830
10248
  function sortPrdsByPriority(prds, priority) {
9831
10249
  if (priority.length === 0) return prds;
9832
10250
  const priorityMap = /* @__PURE__ */ new Map();
@@ -9922,7 +10340,7 @@ function renderLogPane(projectDir, logs) {
9922
10340
  let newestMtime = 0;
9923
10341
  for (const log of existingLogs) {
9924
10342
  try {
9925
- const stat = fs26.statSync(log.path);
10343
+ const stat = fs27.statSync(log.path);
9926
10344
  if (stat.mtimeMs > newestMtime) {
9927
10345
  newestMtime = stat.mtimeMs;
9928
10346
  newestLog = log;
@@ -11577,8 +11995,8 @@ function createActionsTab() {
11577
11995
  // src/commands/dashboard/tab-logs.ts
11578
11996
  init_dist();
11579
11997
  import blessed5 from "blessed";
11580
- import * as fs27 from "fs";
11581
- import * as path28 from "path";
11998
+ import * as fs28 from "fs";
11999
+ import * as path29 from "path";
11582
12000
  var LOG_NAMES = ["executor", "reviewer"];
11583
12001
  var LOG_LINES = 200;
11584
12002
  function createLogsTab() {
@@ -11619,7 +12037,7 @@ function createLogsTab() {
11619
12037
  let activeKeyHandlers = [];
11620
12038
  let activeCtx = null;
11621
12039
  function getLogPath(projectDir, logName) {
11622
- return path28.join(projectDir, "logs", `${logName}.log`);
12040
+ return path29.join(projectDir, "logs", `${logName}.log`);
11623
12041
  }
11624
12042
  function updateSelector() {
11625
12043
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -11633,7 +12051,7 @@ function createLogsTab() {
11633
12051
  function loadLog(ctx) {
11634
12052
  const logName = LOG_NAMES[selectedLogIndex];
11635
12053
  const logPath = getLogPath(ctx.projectDir, logName);
11636
- if (!fs27.existsSync(logPath)) {
12054
+ if (!fs28.existsSync(logPath)) {
11637
12055
  logContent.setContent(
11638
12056
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
11639
12057
 
@@ -11643,7 +12061,7 @@ Log will appear here once the ${logName} runs.`
11643
12061
  return;
11644
12062
  }
11645
12063
  try {
11646
- const stat = fs27.statSync(logPath);
12064
+ const stat = fs28.statSync(logPath);
11647
12065
  const sizeKB = (stat.size / 1024).toFixed(1);
11648
12066
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
11649
12067
  } catch {
@@ -12045,7 +12463,7 @@ function doctorCommand(program2) {
12045
12463
  program2.command("doctor").description("Check Night Watch configuration and system health").option("--fix", "Automatically fix fixable issues").action(async (options) => {
12046
12464
  const projectDir = process.cwd();
12047
12465
  const config = loadConfig(projectDir);
12048
- const totalChecks = 8;
12466
+ const totalChecks = 7;
12049
12467
  let checkNum = 1;
12050
12468
  let passedChecks = 0;
12051
12469
  let fixedChecks = 0;
@@ -12071,11 +12489,12 @@ function doctorCommand(program2) {
12071
12489
  const ghResult = runCheck(checkNum++, totalChecks, "GitHub CLI", () => checkGhCli(), options);
12072
12490
  if (ghResult.passed) passedChecks++;
12073
12491
  if (ghResult.fixed) fixedChecks++;
12492
+ const resolvedProviderCli = BUILT_IN_PRESETS[config.provider]?.command ?? config.provider;
12074
12493
  const providerResult = runCheck(
12075
12494
  checkNum++,
12076
12495
  totalChecks,
12077
12496
  "provider CLI",
12078
- () => checkProviderCli(config.provider),
12497
+ () => checkProviderCli(resolvedProviderCli),
12079
12498
  options
12080
12499
  );
12081
12500
  if (providerResult.passed) passedChecks++;
@@ -12089,15 +12508,6 @@ function doctorCommand(program2) {
12089
12508
  );
12090
12509
  if (configResult.passed) passedChecks++;
12091
12510
  if (configResult.fixed) fixedChecks++;
12092
- const prdResult = runCheck(
12093
- checkNum++,
12094
- totalChecks,
12095
- "PRD directory",
12096
- () => checkPrdDirectory(projectDir, config.prdDir),
12097
- options
12098
- );
12099
- if (prdResult.passed) passedChecks++;
12100
- if (prdResult.fixed) fixedChecks++;
12101
12511
  const logsResult = runCheck(
12102
12512
  checkNum++,
12103
12513
  totalChecks,
@@ -12149,13 +12559,13 @@ function doctorCommand(program2) {
12149
12559
 
12150
12560
  // src/commands/serve.ts
12151
12561
  init_dist();
12152
- import * as fs32 from "fs";
12562
+ import * as fs33 from "fs";
12153
12563
 
12154
12564
  // ../server/dist/index.js
12155
12565
  init_dist();
12156
- import * as fs31 from "fs";
12157
- import * as path34 from "path";
12158
- import { dirname as dirname7 } from "path";
12566
+ import * as fs32 from "fs";
12567
+ import * as path35 from "path";
12568
+ import { dirname as dirname8 } from "path";
12159
12569
  import { fileURLToPath as fileURLToPath3 } from "url";
12160
12570
  import cors from "cors";
12161
12571
  import express from "express";
@@ -12239,8 +12649,8 @@ function setupGracefulShutdown(server, beforeClose) {
12239
12649
 
12240
12650
  // ../server/dist/middleware/project-resolver.middleware.js
12241
12651
  init_dist();
12242
- import * as fs28 from "fs";
12243
- import * as path29 from "path";
12652
+ import * as fs29 from "fs";
12653
+ import * as path30 from "path";
12244
12654
  function resolveProject(req, res, next) {
12245
12655
  const projectId = req.params.projectId;
12246
12656
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -12250,7 +12660,7 @@ function resolveProject(req, res, next) {
12250
12660
  res.status(404).json({ error: `Project not found: ${decodedId}` });
12251
12661
  return;
12252
12662
  }
12253
- if (!fs28.existsSync(entry.path) || !fs28.existsSync(path29.join(entry.path, CONFIG_FILE_NAME))) {
12663
+ if (!fs29.existsSync(entry.path) || !fs29.existsSync(path30.join(entry.path, CONFIG_FILE_NAME))) {
12254
12664
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
12255
12665
  return;
12256
12666
  }
@@ -12295,8 +12705,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
12295
12705
 
12296
12706
  // ../server/dist/routes/action.routes.js
12297
12707
  init_dist();
12298
- import * as fs29 from "fs";
12299
- import * as path30 from "path";
12708
+ import * as fs30 from "fs";
12709
+ import * as path31 from "path";
12300
12710
  import { execSync as execSync5, spawn as spawn5 } from "child_process";
12301
12711
  import { Router } from "express";
12302
12712
 
@@ -12334,17 +12744,17 @@ function getBoardProvider(config, projectDir) {
12334
12744
  function cleanOrphanedClaims(dir) {
12335
12745
  let entries;
12336
12746
  try {
12337
- entries = fs29.readdirSync(dir, { withFileTypes: true });
12747
+ entries = fs30.readdirSync(dir, { withFileTypes: true });
12338
12748
  } catch {
12339
12749
  return;
12340
12750
  }
12341
12751
  for (const entry of entries) {
12342
- const fullPath = path30.join(dir, entry.name);
12752
+ const fullPath = path31.join(dir, entry.name);
12343
12753
  if (entry.isDirectory() && entry.name !== "done") {
12344
12754
  cleanOrphanedClaims(fullPath);
12345
12755
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
12346
12756
  try {
12347
- fs29.unlinkSync(fullPath);
12757
+ fs30.unlinkSync(fullPath);
12348
12758
  } catch {
12349
12759
  }
12350
12760
  }
@@ -12435,18 +12845,16 @@ function createActionRouteHandlers(ctx) {
12435
12845
  router.post(`/${p}review`, (req, res) => {
12436
12846
  spawnAction2(ctx.getProjectDir(req), ["review"], req, res);
12437
12847
  });
12438
- router.post(`/${p}qa`, (req, res) => {
12439
- spawnAction2(ctx.getProjectDir(req), ["qa"], req, res);
12440
- });
12441
- router.post(`/${p}audit`, (req, res) => {
12442
- spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
12443
- });
12444
- router.post(`/${p}analytics`, (req, res) => {
12445
- spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
12446
- });
12447
- router.post(`/${p}planner`, (req, res) => {
12448
- spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12449
- });
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
+ }
12450
12858
  router.post(`/${p}install-cron`, (req, res) => {
12451
12859
  const projectDir = ctx.getProjectDir(req);
12452
12860
  try {
@@ -12501,19 +12909,19 @@ function createActionRouteHandlers(ctx) {
12501
12909
  res.status(400).json({ error: "Invalid PRD name" });
12502
12910
  return;
12503
12911
  }
12504
- const prdDir = path30.join(projectDir, config.prdDir);
12912
+ const prdDir = path31.join(projectDir, config.prdDir);
12505
12913
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
12506
- const pendingPath = path30.join(prdDir, normalized);
12507
- const donePath = path30.join(prdDir, "done", normalized);
12508
- if (fs29.existsSync(pendingPath)) {
12914
+ const pendingPath = path31.join(prdDir, normalized);
12915
+ const donePath = path31.join(prdDir, "done", normalized);
12916
+ if (fs30.existsSync(pendingPath)) {
12509
12917
  res.json({ message: `"${normalized}" is already pending` });
12510
12918
  return;
12511
12919
  }
12512
- if (!fs29.existsSync(donePath)) {
12920
+ if (!fs30.existsSync(donePath)) {
12513
12921
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
12514
12922
  return;
12515
12923
  }
12516
- fs29.renameSync(donePath, pendingPath);
12924
+ fs30.renameSync(donePath, pendingPath);
12517
12925
  res.json({ message: `Moved "${normalized}" back to pending` });
12518
12926
  } catch (error2) {
12519
12927
  res.status(500).json({
@@ -12531,11 +12939,11 @@ function createActionRouteHandlers(ctx) {
12531
12939
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
12532
12940
  return;
12533
12941
  }
12534
- if (fs29.existsSync(lockPath)) {
12535
- fs29.unlinkSync(lockPath);
12942
+ if (fs30.existsSync(lockPath)) {
12943
+ fs30.unlinkSync(lockPath);
12536
12944
  }
12537
- const prdDir = path30.join(projectDir, config.prdDir);
12538
- if (fs29.existsSync(prdDir)) {
12945
+ const prdDir = path31.join(projectDir, config.prdDir);
12946
+ if (fs30.existsSync(prdDir)) {
12539
12947
  cleanOrphanedClaims(prdDir);
12540
12948
  }
12541
12949
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -13194,6 +13602,26 @@ function createConfigRoutes(deps) {
13194
13602
  });
13195
13603
  return router;
13196
13604
  }
13605
+ function createGlobalNotificationsRoutes() {
13606
+ const router = Router3();
13607
+ router.get("/", (_req, res) => {
13608
+ res.json(loadGlobalNotificationsConfig());
13609
+ });
13610
+ router.put("/", (req, res) => {
13611
+ const { webhook } = req.body;
13612
+ if (webhook !== null && webhook !== void 0) {
13613
+ const issues = validateWebhook(webhook);
13614
+ if (issues.length > 0) {
13615
+ res.status(400).json({ error: `Invalid webhook: ${issues.join(", ")}` });
13616
+ return;
13617
+ }
13618
+ }
13619
+ const config = { webhook: webhook ?? null };
13620
+ saveGlobalNotificationsConfig(config);
13621
+ res.json(loadGlobalNotificationsConfig());
13622
+ });
13623
+ return router;
13624
+ }
13197
13625
  function createProjectConfigRoutes() {
13198
13626
  const router = Router3({ mergeParams: true });
13199
13627
  router.get("/config", (req, res) => {
@@ -13227,8 +13655,8 @@ function createProjectConfigRoutes() {
13227
13655
 
13228
13656
  // ../server/dist/routes/doctor.routes.js
13229
13657
  init_dist();
13230
- import * as fs30 from "fs";
13231
- import * as path31 from "path";
13658
+ import * as fs31 from "fs";
13659
+ import * as path32 from "path";
13232
13660
  import { execSync as execSync6 } from "child_process";
13233
13661
  import { Router as Router4 } from "express";
13234
13662
  function runDoctorChecks(projectDir, config) {
@@ -13261,7 +13689,7 @@ function runDoctorChecks(projectDir, config) {
13261
13689
  });
13262
13690
  }
13263
13691
  try {
13264
- const projectName = path31.basename(projectDir);
13692
+ const projectName = path32.basename(projectDir);
13265
13693
  const marker = generateMarker(projectName);
13266
13694
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
13267
13695
  if (crontabEntries.length > 0) {
@@ -13284,8 +13712,8 @@ function runDoctorChecks(projectDir, config) {
13284
13712
  detail: "Failed to check crontab"
13285
13713
  });
13286
13714
  }
13287
- const configPath = path31.join(projectDir, CONFIG_FILE_NAME);
13288
- if (fs30.existsSync(configPath)) {
13715
+ const configPath = path32.join(projectDir, CONFIG_FILE_NAME);
13716
+ if (fs31.existsSync(configPath)) {
13289
13717
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
13290
13718
  } else {
13291
13719
  checks.push({
@@ -13294,9 +13722,9 @@ function runDoctorChecks(projectDir, config) {
13294
13722
  detail: "Config file not found (using defaults)"
13295
13723
  });
13296
13724
  }
13297
- const prdDir = path31.join(projectDir, config.prdDir);
13298
- if (fs30.existsSync(prdDir)) {
13299
- const prds = fs30.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
13725
+ const prdDir = path32.join(projectDir, config.prdDir);
13726
+ if (fs31.existsSync(prdDir)) {
13727
+ const prds = fs31.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
13300
13728
  checks.push({
13301
13729
  name: "prdDir",
13302
13730
  status: "pass",
@@ -13339,7 +13767,7 @@ function createProjectDoctorRoutes() {
13339
13767
 
13340
13768
  // ../server/dist/routes/log.routes.js
13341
13769
  init_dist();
13342
- import * as path32 from "path";
13770
+ import * as path33 from "path";
13343
13771
  import { Router as Router5 } from "express";
13344
13772
  function createLogRoutes(deps) {
13345
13773
  const { projectDir } = deps;
@@ -13358,7 +13786,7 @@ function createLogRoutes(deps) {
13358
13786
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
13359
13787
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
13360
13788
  const fileName = LOG_FILE_NAMES[name] || name;
13361
- const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
13789
+ const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
13362
13790
  const logLines = getLastLogLines(logPath, linesToRead);
13363
13791
  res.json({ name, lines: logLines });
13364
13792
  } catch (error2) {
@@ -13384,7 +13812,7 @@ function createProjectLogRoutes() {
13384
13812
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
13385
13813
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
13386
13814
  const fileName = LOG_FILE_NAMES[name] || name;
13387
- const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
13815
+ const logPath = path33.join(projectDir, LOG_DIR, `${fileName}.log`);
13388
13816
  const logLines = getLastLogLines(logPath, linesToRead);
13389
13817
  res.json({ name, lines: logLines });
13390
13818
  } catch (error2) {
@@ -13419,7 +13847,7 @@ function createProjectPrdRoutes() {
13419
13847
 
13420
13848
  // ../server/dist/routes/roadmap.routes.js
13421
13849
  init_dist();
13422
- import * as path33 from "path";
13850
+ import * as path34 from "path";
13423
13851
  import { Router as Router7 } from "express";
13424
13852
  function createRoadmapRouteHandlers(ctx) {
13425
13853
  const router = Router7({ mergeParams: true });
@@ -13429,7 +13857,7 @@ function createRoadmapRouteHandlers(ctx) {
13429
13857
  const config = ctx.getConfig(req);
13430
13858
  const projectDir = ctx.getProjectDir(req);
13431
13859
  const status = getRoadmapStatus(projectDir, config);
13432
- const prdDir = path33.join(projectDir, config.prdDir);
13860
+ const prdDir = path34.join(projectDir, config.prdDir);
13433
13861
  const state = loadRoadmapState(prdDir);
13434
13862
  res.json({
13435
13863
  ...status,
@@ -13761,26 +14189,26 @@ function createQueueRoutes(deps) {
13761
14189
 
13762
14190
  // ../server/dist/index.js
13763
14191
  var __filename2 = fileURLToPath3(import.meta.url);
13764
- var __dirname3 = dirname7(__filename2);
14192
+ var __dirname3 = dirname8(__filename2);
13765
14193
  function resolveWebDistPath() {
13766
- const bundled = path34.join(__dirname3, "web");
13767
- if (fs31.existsSync(path34.join(bundled, "index.html")))
14194
+ const bundled = path35.join(__dirname3, "web");
14195
+ if (fs32.existsSync(path35.join(bundled, "index.html")))
13768
14196
  return bundled;
13769
14197
  let d = __dirname3;
13770
14198
  for (let i = 0; i < 8; i++) {
13771
- if (fs31.existsSync(path34.join(d, "turbo.json"))) {
13772
- const dev = path34.join(d, "web/dist");
13773
- if (fs31.existsSync(path34.join(dev, "index.html")))
14199
+ if (fs32.existsSync(path35.join(d, "turbo.json"))) {
14200
+ const dev = path35.join(d, "web/dist");
14201
+ if (fs32.existsSync(path35.join(dev, "index.html")))
13774
14202
  return dev;
13775
14203
  break;
13776
14204
  }
13777
- d = dirname7(d);
14205
+ d = dirname8(d);
13778
14206
  }
13779
14207
  return bundled;
13780
14208
  }
13781
14209
  function setupStaticFiles(app) {
13782
14210
  const webDistPath = resolveWebDistPath();
13783
- if (fs31.existsSync(webDistPath)) {
14211
+ if (fs32.existsSync(webDistPath)) {
13784
14212
  app.use(express.static(webDistPath));
13785
14213
  }
13786
14214
  app.use((req, res, next) => {
@@ -13788,8 +14216,8 @@ function setupStaticFiles(app) {
13788
14216
  next();
13789
14217
  return;
13790
14218
  }
13791
- const indexPath = path34.resolve(webDistPath, "index.html");
13792
- if (fs31.existsSync(indexPath)) {
14219
+ const indexPath = path35.resolve(webDistPath, "index.html");
14220
+ if (fs32.existsSync(indexPath)) {
13793
14221
  res.sendFile(indexPath, (err) => {
13794
14222
  if (err)
13795
14223
  next();
@@ -13827,6 +14255,7 @@ function createApp(projectDir) {
13827
14255
  app.use("/api/logs", createLogRoutes({ projectDir }));
13828
14256
  app.use("/api/doctor", createDoctorRoutes({ projectDir, getConfig: () => config }));
13829
14257
  app.use("/api/queue", createQueueRoutes({ getConfig: () => config }));
14258
+ app.use("/api/global-notifications", createGlobalNotificationsRoutes());
13830
14259
  app.get("/api/prs", async (_req, res) => {
13831
14260
  try {
13832
14261
  res.json(await collectPrInfo(projectDir, config.branchPatterns));
@@ -13898,14 +14327,30 @@ function createGlobalApp() {
13898
14327
  res.status(500).json({ error: error2 instanceof Error ? error2.message : String(error2) });
13899
14328
  }
13900
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
+ });
13901
14345
  app.use("/api/queue", createGlobalQueueRoutes());
14346
+ app.use("/api/global-notifications", createGlobalNotificationsRoutes());
13902
14347
  app.use("/api/projects/:projectId", resolveProject, createProjectRouter());
13903
14348
  setupStaticFiles(app);
13904
14349
  app.use(errorHandler);
13905
14350
  return app;
13906
14351
  }
13907
14352
  function bootContainer() {
13908
- initContainer(path34.dirname(getDbPath()));
14353
+ initContainer(path35.dirname(getDbPath()));
13909
14354
  }
13910
14355
  function startServer(projectDir, port) {
13911
14356
  bootContainer();
@@ -13958,8 +14403,8 @@ function isProcessRunning2(pid) {
13958
14403
  }
13959
14404
  function readPid(lockPath) {
13960
14405
  try {
13961
- if (!fs32.existsSync(lockPath)) return null;
13962
- const raw = fs32.readFileSync(lockPath, "utf-8").trim();
14406
+ if (!fs33.existsSync(lockPath)) return null;
14407
+ const raw = fs33.readFileSync(lockPath, "utf-8").trim();
13963
14408
  const pid = parseInt(raw, 10);
13964
14409
  return Number.isFinite(pid) ? pid : null;
13965
14410
  } catch {
@@ -13971,10 +14416,10 @@ function acquireServeLock(mode, port) {
13971
14416
  let stalePidCleaned;
13972
14417
  for (let attempt = 0; attempt < 2; attempt++) {
13973
14418
  try {
13974
- const fd = fs32.openSync(lockPath, "wx");
13975
- fs32.writeFileSync(fd, `${process.pid}
14419
+ const fd = fs33.openSync(lockPath, "wx");
14420
+ fs33.writeFileSync(fd, `${process.pid}
13976
14421
  `);
13977
- fs32.closeSync(fd);
14422
+ fs33.closeSync(fd);
13978
14423
  return { acquired: true, lockPath, stalePidCleaned };
13979
14424
  } catch (error2) {
13980
14425
  const err = error2;
@@ -13995,7 +14440,7 @@ function acquireServeLock(mode, port) {
13995
14440
  };
13996
14441
  }
13997
14442
  try {
13998
- fs32.unlinkSync(lockPath);
14443
+ fs33.unlinkSync(lockPath);
13999
14444
  if (existingPid) {
14000
14445
  stalePidCleaned = existingPid;
14001
14446
  }
@@ -14018,10 +14463,10 @@ function acquireServeLock(mode, port) {
14018
14463
  }
14019
14464
  function releaseServeLock(lockPath) {
14020
14465
  try {
14021
- if (!fs32.existsSync(lockPath)) return;
14466
+ if (!fs33.existsSync(lockPath)) return;
14022
14467
  const lockPid = readPid(lockPath);
14023
14468
  if (lockPid !== null && lockPid !== process.pid) return;
14024
- fs32.unlinkSync(lockPath);
14469
+ fs33.unlinkSync(lockPath);
14025
14470
  } catch {
14026
14471
  }
14027
14472
  }
@@ -14117,14 +14562,14 @@ function historyCommand(program2) {
14117
14562
  // src/commands/update.ts
14118
14563
  init_dist();
14119
14564
  import { spawnSync } from "child_process";
14120
- import * as fs33 from "fs";
14121
- import * as path35 from "path";
14565
+ import * as fs34 from "fs";
14566
+ import * as path36 from "path";
14122
14567
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
14123
14568
  function parseProjectDirs(projects, cwd) {
14124
14569
  if (!projects || projects.trim().length === 0) {
14125
14570
  return [cwd];
14126
14571
  }
14127
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path35.resolve(cwd, entry));
14572
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path36.resolve(cwd, entry));
14128
14573
  return Array.from(new Set(dirs));
14129
14574
  }
14130
14575
  function shouldInstallGlobal(options) {
@@ -14166,7 +14611,7 @@ function updateCommand(program2) {
14166
14611
  }
14167
14612
  const nightWatchBin = resolveNightWatchBin();
14168
14613
  for (const projectDir of projectDirs) {
14169
- if (!fs33.existsSync(projectDir) || !fs33.statSync(projectDir).isDirectory()) {
14614
+ if (!fs34.existsSync(projectDir) || !fs34.statSync(projectDir).isDirectory()) {
14170
14615
  warn(`Skipping invalid project directory: ${projectDir}`);
14171
14616
  continue;
14172
14617
  }
@@ -14210,8 +14655,8 @@ function prdStateCommand(program2) {
14210
14655
 
14211
14656
  // src/commands/retry.ts
14212
14657
  init_dist();
14213
- import * as fs34 from "fs";
14214
- import * as path36 from "path";
14658
+ import * as fs35 from "fs";
14659
+ import * as path37 from "path";
14215
14660
  function normalizePrdName(name) {
14216
14661
  if (!name.endsWith(".md")) {
14217
14662
  return `${name}.md`;
@@ -14219,26 +14664,26 @@ function normalizePrdName(name) {
14219
14664
  return name;
14220
14665
  }
14221
14666
  function getDonePrds(doneDir) {
14222
- if (!fs34.existsSync(doneDir)) {
14667
+ if (!fs35.existsSync(doneDir)) {
14223
14668
  return [];
14224
14669
  }
14225
- return fs34.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
14670
+ return fs35.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
14226
14671
  }
14227
14672
  function retryCommand(program2) {
14228
14673
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
14229
14674
  const projectDir = process.cwd();
14230
14675
  const config = loadConfig(projectDir);
14231
- const prdDir = path36.join(projectDir, config.prdDir);
14232
- const doneDir = path36.join(prdDir, "done");
14676
+ const prdDir = path37.join(projectDir, config.prdDir);
14677
+ const doneDir = path37.join(prdDir, "done");
14233
14678
  const normalizedPrdName = normalizePrdName(prdName);
14234
- const pendingPath = path36.join(prdDir, normalizedPrdName);
14235
- if (fs34.existsSync(pendingPath)) {
14679
+ const pendingPath = path37.join(prdDir, normalizedPrdName);
14680
+ if (fs35.existsSync(pendingPath)) {
14236
14681
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
14237
14682
  return;
14238
14683
  }
14239
- const donePath = path36.join(doneDir, normalizedPrdName);
14240
- if (fs34.existsSync(donePath)) {
14241
- fs34.renameSync(donePath, pendingPath);
14684
+ const donePath = path37.join(doneDir, normalizedPrdName);
14685
+ if (fs35.existsSync(donePath)) {
14686
+ fs35.renameSync(donePath, pendingPath);
14242
14687
  success(`Moved "${normalizedPrdName}" back to pending.`);
14243
14688
  dim(`From: ${donePath}`);
14244
14689
  dim(`To: ${pendingPath}`);
@@ -14490,7 +14935,7 @@ function prdsCommand(program2) {
14490
14935
 
14491
14936
  // src/commands/cancel.ts
14492
14937
  init_dist();
14493
- import * as fs35 from "fs";
14938
+ import * as fs36 from "fs";
14494
14939
  import * as readline3 from "readline";
14495
14940
  function getLockFilePaths2(projectDir) {
14496
14941
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -14537,7 +14982,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14537
14982
  const pid = lockStatus.pid;
14538
14983
  if (!lockStatus.running) {
14539
14984
  try {
14540
- fs35.unlinkSync(lockPath);
14985
+ fs36.unlinkSync(lockPath);
14541
14986
  return {
14542
14987
  success: true,
14543
14988
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -14575,7 +15020,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14575
15020
  await sleep2(3e3);
14576
15021
  if (!isProcessRunning3(pid)) {
14577
15022
  try {
14578
- fs35.unlinkSync(lockPath);
15023
+ fs36.unlinkSync(lockPath);
14579
15024
  } catch {
14580
15025
  }
14581
15026
  return {
@@ -14610,7 +15055,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14610
15055
  await sleep2(500);
14611
15056
  if (!isProcessRunning3(pid)) {
14612
15057
  try {
14613
- fs35.unlinkSync(lockPath);
15058
+ fs36.unlinkSync(lockPath);
14614
15059
  } catch {
14615
15060
  }
14616
15061
  return {
@@ -14671,31 +15116,31 @@ function cancelCommand(program2) {
14671
15116
 
14672
15117
  // src/commands/slice.ts
14673
15118
  init_dist();
14674
- import * as fs36 from "fs";
14675
- import * as path37 from "path";
15119
+ import * as fs37 from "fs";
15120
+ import * as path38 from "path";
14676
15121
  function plannerLockPath2(projectDir) {
14677
15122
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
14678
15123
  }
14679
15124
  function acquirePlannerLock(projectDir) {
14680
15125
  const lockFile = plannerLockPath2(projectDir);
14681
- if (fs36.existsSync(lockFile)) {
14682
- const pidRaw = fs36.readFileSync(lockFile, "utf-8").trim();
15126
+ if (fs37.existsSync(lockFile)) {
15127
+ const pidRaw = fs37.readFileSync(lockFile, "utf-8").trim();
14683
15128
  const pid = parseInt(pidRaw, 10);
14684
15129
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
14685
15130
  return { acquired: false, lockFile, pid };
14686
15131
  }
14687
15132
  try {
14688
- fs36.unlinkSync(lockFile);
15133
+ fs37.unlinkSync(lockFile);
14689
15134
  } catch {
14690
15135
  }
14691
15136
  }
14692
- fs36.writeFileSync(lockFile, String(process.pid));
15137
+ fs37.writeFileSync(lockFile, String(process.pid));
14693
15138
  return { acquired: true, lockFile };
14694
15139
  }
14695
15140
  function releasePlannerLock(lockFile) {
14696
15141
  try {
14697
- if (fs36.existsSync(lockFile)) {
14698
- fs36.unlinkSync(lockFile);
15142
+ if (fs37.existsSync(lockFile)) {
15143
+ fs37.unlinkSync(lockFile);
14699
15144
  }
14700
15145
  } catch {
14701
15146
  }
@@ -14704,12 +15149,12 @@ function resolvePlannerIssueColumn(config) {
14704
15149
  return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
14705
15150
  }
14706
15151
  function buildPlannerIssueBody(projectDir, config, result) {
14707
- const relativePrdPath = path37.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
14708
- const absolutePrdPath = path37.join(projectDir, config.prdDir, result.file ?? "");
15152
+ const relativePrdPath = path38.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
15153
+ const absolutePrdPath = path38.join(projectDir, config.prdDir, result.file ?? "");
14709
15154
  const sourceItem = result.item;
14710
15155
  let prdContent;
14711
15156
  try {
14712
- prdContent = fs36.readFileSync(absolutePrdPath, "utf-8");
15157
+ prdContent = fs37.readFileSync(absolutePrdPath, "utf-8");
14713
15158
  } catch {
14714
15159
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
14715
15160
  }
@@ -14885,7 +15330,7 @@ function sliceCommand(program2) {
14885
15330
  if (!options.dryRun) {
14886
15331
  await sendNotifications(config, {
14887
15332
  event: "run_started",
14888
- projectName: path37.basename(projectDir),
15333
+ projectName: path38.basename(projectDir),
14889
15334
  exitCode: 0,
14890
15335
  provider: config.provider
14891
15336
  });
@@ -14920,7 +15365,7 @@ function sliceCommand(program2) {
14920
15365
  if (!options.dryRun && result.sliced) {
14921
15366
  await sendNotifications(config, {
14922
15367
  event: "run_succeeded",
14923
- projectName: path37.basename(projectDir),
15368
+ projectName: path38.basename(projectDir),
14924
15369
  exitCode,
14925
15370
  provider: config.provider,
14926
15371
  prTitle: result.item?.title
@@ -14928,7 +15373,7 @@ function sliceCommand(program2) {
14928
15373
  } else if (!options.dryRun && !nothingPending) {
14929
15374
  await sendNotifications(config, {
14930
15375
  event: "run_failed",
14931
- projectName: path37.basename(projectDir),
15376
+ projectName: path38.basename(projectDir),
14932
15377
  exitCode,
14933
15378
  provider: config.provider
14934
15379
  });
@@ -14944,21 +15389,21 @@ function sliceCommand(program2) {
14944
15389
 
14945
15390
  // src/commands/state.ts
14946
15391
  init_dist();
14947
- import * as os8 from "os";
14948
- import * as path38 from "path";
15392
+ import * as os9 from "os";
15393
+ import * as path39 from "path";
14949
15394
  import chalk5 from "chalk";
14950
15395
  import { Command } from "commander";
14951
15396
  function createStateCommand() {
14952
15397
  const state = new Command("state");
14953
15398
  state.description("Manage Night Watch state");
14954
15399
  state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
14955
- const nightWatchHome = process.env.NIGHT_WATCH_HOME || path38.join(os8.homedir(), GLOBAL_CONFIG_DIR);
15400
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path39.join(os9.homedir(), GLOBAL_CONFIG_DIR);
14956
15401
  if (opts.dryRun) {
14957
15402
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
14958
15403
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
14959
- console.log(` ${path38.join(nightWatchHome, "projects.json")}`);
14960
- console.log(` ${path38.join(nightWatchHome, "history.json")}`);
14961
- console.log(` ${path38.join(nightWatchHome, "prd-states.json")}`);
15404
+ console.log(` ${path39.join(nightWatchHome, "projects.json")}`);
15405
+ console.log(` ${path39.join(nightWatchHome, "history.json")}`);
15406
+ console.log(` ${path39.join(nightWatchHome, "prd-states.json")}`);
14962
15407
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
14963
15408
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
14964
15409
  return;
@@ -14996,8 +15441,8 @@ function createStateCommand() {
14996
15441
  init_dist();
14997
15442
  init_dist();
14998
15443
  import { execFileSync as execFileSync6 } from "child_process";
14999
- import * as fs37 from "fs";
15000
- import * as path39 from "path";
15444
+ import * as fs38 from "fs";
15445
+ import * as path40 from "path";
15001
15446
  import * as readline4 from "readline";
15002
15447
  import chalk6 from "chalk";
15003
15448
  async function run(fn) {
@@ -15019,7 +15464,7 @@ function getProvider(config, cwd) {
15019
15464
  return createBoardProvider(bp, cwd);
15020
15465
  }
15021
15466
  function defaultBoardTitle(cwd) {
15022
- return `${path39.basename(cwd)} Night Watch`;
15467
+ return `${path40.basename(cwd)} Night Watch`;
15023
15468
  }
15024
15469
  async function ensureBoardConfigured(config, cwd, provider, options) {
15025
15470
  if (config.boardProvider?.projectNumber) {
@@ -15218,11 +15663,11 @@ function boardCommand(program2) {
15218
15663
  let body = options.body ?? "";
15219
15664
  if (options.bodyFile) {
15220
15665
  const filePath = options.bodyFile;
15221
- if (!fs37.existsSync(filePath)) {
15666
+ if (!fs38.existsSync(filePath)) {
15222
15667
  console.error(`File not found: ${filePath}`);
15223
15668
  process.exit(1);
15224
15669
  }
15225
- body = fs37.readFileSync(filePath, "utf-8");
15670
+ body = fs38.readFileSync(filePath, "utf-8");
15226
15671
  }
15227
15672
  const labels = [];
15228
15673
  if (options.label) {
@@ -15250,6 +15695,25 @@ function boardCommand(program2) {
15250
15695
  }
15251
15696
  })
15252
15697
  );
15698
+ board.command("add-issue").description("Add an existing GitHub issue to the board").argument("<number>", "Issue number").option("--column <name>", "Target column (default: Ready)", "Ready").action(
15699
+ async (number, options) => run(async () => {
15700
+ const cwd = process.cwd();
15701
+ const config = loadConfig(cwd);
15702
+ const provider = getProvider(config, cwd);
15703
+ await ensureBoardConfigured(config, cwd, provider);
15704
+ if (!BOARD_COLUMNS.includes(options.column)) {
15705
+ console.error(
15706
+ `Invalid column "${options.column}". Valid columns: ${BOARD_COLUMNS.join(", ")}`
15707
+ );
15708
+ process.exit(1);
15709
+ }
15710
+ const issue = await provider.addIssue(
15711
+ parseInt(number, 10),
15712
+ options.column
15713
+ );
15714
+ success(`Added issue #${issue.number} "${issue.title}" to ${options.column}`);
15715
+ })
15716
+ );
15253
15717
  board.command("status").description("Show the current state of all issues grouped by column").option("--json", "Output raw JSON").option("--group-by <field>", "Group by: priority, category, or column (default: column)").action(
15254
15718
  async (options) => run(async () => {
15255
15719
  const cwd = process.cwd();
@@ -15444,12 +15908,12 @@ function boardCommand(program2) {
15444
15908
  const config = loadConfig(cwd);
15445
15909
  const provider = getProvider(config, cwd);
15446
15910
  await ensureBoardConfigured(config, cwd, provider);
15447
- const roadmapPath = options.roadmap ?? path39.join(cwd, "ROADMAP.md");
15448
- if (!fs37.existsSync(roadmapPath)) {
15911
+ const roadmapPath = options.roadmap ?? path40.join(cwd, "ROADMAP.md");
15912
+ if (!fs38.existsSync(roadmapPath)) {
15449
15913
  console.error(`Roadmap file not found: ${roadmapPath}`);
15450
15914
  process.exit(1);
15451
15915
  }
15452
- const roadmapContent = fs37.readFileSync(roadmapPath, "utf-8");
15916
+ const roadmapContent = fs38.readFileSync(roadmapPath, "utf-8");
15453
15917
  const items = parseRoadmap(roadmapContent);
15454
15918
  const uncheckedItems = getUncheckedItems(items);
15455
15919
  if (uncheckedItems.length === 0) {
@@ -15573,7 +16037,7 @@ function boardCommand(program2) {
15573
16037
  // src/commands/queue.ts
15574
16038
  init_dist();
15575
16039
  init_dist();
15576
- import * as path40 from "path";
16040
+ import * as path41 from "path";
15577
16041
  import { spawn as spawn6 } from "child_process";
15578
16042
  import chalk7 from "chalk";
15579
16043
  import { Command as Command2 } from "commander";
@@ -15693,7 +16157,7 @@ function createQueueCommand() {
15693
16157
  process.exit(1);
15694
16158
  }
15695
16159
  }
15696
- const projectName = path40.basename(projectDir);
16160
+ const projectName = path41.basename(projectDir);
15697
16161
  const queueConfig = loadConfig(projectDir).queue;
15698
16162
  const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
15699
16163
  console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
@@ -15849,17 +16313,17 @@ function notifyCommand(program2) {
15849
16313
 
15850
16314
  // src/cli.ts
15851
16315
  var __filename3 = fileURLToPath4(import.meta.url);
15852
- var __dirname4 = dirname8(__filename3);
16316
+ var __dirname4 = dirname9(__filename3);
15853
16317
  function findPackageRoot(dir) {
15854
16318
  let d = dir;
15855
16319
  for (let i = 0; i < 5; i++) {
15856
- if (existsSync29(join35(d, "package.json"))) return d;
15857
- d = dirname8(d);
16320
+ if (existsSync30(join36(d, "package.json"))) return d;
16321
+ d = dirname9(d);
15858
16322
  }
15859
16323
  return dir;
15860
16324
  }
15861
16325
  var packageRoot = findPackageRoot(__dirname4);
15862
- var packageJson = JSON.parse(readFileSync18(join35(packageRoot, "package.json"), "utf-8"));
16326
+ var packageJson = JSON.parse(readFileSync19(join36(packageRoot, "package.json"), "utf-8"));
15863
16327
  var program = new Command3();
15864
16328
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
15865
16329
  initCommand(program);