@jonit-dev/night-watch-cli 1.7.99 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -31,7 +31,7 @@ function resolveProviderBucketKey(provider, providerEnv) {
31
31
  return `claude-proxy:${baseUrl}`;
32
32
  }
33
33
  }
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_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, AUDIT_LOG_NAME, PLANNER_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;
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;
35
35
  var init_constants = __esm({
36
36
  "../core/dist/constants.js"() {
37
37
  "use strict";
@@ -45,6 +45,7 @@ var init_constants = __esm({
45
45
  DEFAULT_MAX_RETRIES = 3;
46
46
  DEFAULT_REVIEWER_MAX_RETRIES = 2;
47
47
  DEFAULT_REVIEWER_RETRY_DELAY = 30;
48
+ DEFAULT_REVIEWER_MAX_PRS_PER_RUN = 0;
48
49
  DEFAULT_BRANCH_PREFIX = "night-watch";
49
50
  DEFAULT_BRANCH_PATTERNS = ["feat/", "night-watch/"];
50
51
  DEFAULT_MIN_REVIEW_SCORE = 80;
@@ -108,10 +109,36 @@ var init_constants = __esm({
108
109
  schedule: DEFAULT_AUDIT_SCHEDULE,
109
110
  maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
110
111
  };
112
+ DEFAULT_ANALYTICS_ENABLED = false;
113
+ DEFAULT_ANALYTICS_SCHEDULE = "0 6 * * 1";
114
+ DEFAULT_ANALYTICS_MAX_RUNTIME = 900;
115
+ DEFAULT_ANALYTICS_LOOKBACK_DAYS = 7;
116
+ DEFAULT_ANALYTICS_TARGET_COLUMN = "Draft";
117
+ DEFAULT_ANALYTICS_PROMPT = `You are an analytics reviewer. Analyze the following Amplitude product analytics data.
118
+ Identify significant trends, anomalies, or drops that warrant engineering attention.
119
+ For each actionable finding, output a JSON array of issues:
120
+ [{ "title": "...", "body": "...", "labels": ["analytics"] }]
121
+ If no issues are warranted, output an empty array: []`;
122
+ DEFAULT_ANALYTICS = {
123
+ enabled: DEFAULT_ANALYTICS_ENABLED,
124
+ schedule: DEFAULT_ANALYTICS_SCHEDULE,
125
+ maxRuntime: DEFAULT_ANALYTICS_MAX_RUNTIME,
126
+ lookbackDays: DEFAULT_ANALYTICS_LOOKBACK_DAYS,
127
+ targetColumn: DEFAULT_ANALYTICS_TARGET_COLUMN,
128
+ analysisPrompt: DEFAULT_ANALYTICS_PROMPT
129
+ };
111
130
  AUDIT_LOG_NAME = "audit";
112
131
  PLANNER_LOG_NAME = "slicer";
132
+ ANALYTICS_LOG_NAME = "analytics";
113
133
  VALID_PROVIDERS = ["claude", "codex"];
114
- VALID_JOB_TYPES = ["executor", "reviewer", "qa", "audit", "slicer"];
134
+ VALID_JOB_TYPES = [
135
+ "executor",
136
+ "reviewer",
137
+ "qa",
138
+ "audit",
139
+ "slicer",
140
+ "analytics"
141
+ ];
115
142
  DEFAULT_JOB_PROVIDERS = {};
116
143
  BUILT_IN_PRESETS = {
117
144
  claude: {
@@ -190,7 +217,8 @@ var init_constants = __esm({
190
217
  reviewer: REVIEWER_LOG_NAME,
191
218
  qa: QA_LOG_NAME,
192
219
  audit: AUDIT_LOG_NAME,
193
- planner: PLANNER_LOG_NAME
220
+ planner: PLANNER_LOG_NAME,
221
+ analytics: ANALYTICS_LOG_NAME
194
222
  };
195
223
  GLOBAL_CONFIG_DIR = ".night-watch";
196
224
  REGISTRY_FILE_NAME = "projects.json";
@@ -207,7 +235,8 @@ var init_constants = __esm({
207
235
  reviewer: 40,
208
236
  slicer: 30,
209
237
  qa: 20,
210
- audit: 10
238
+ audit: 10,
239
+ analytics: 10
211
240
  };
212
241
  DEFAULT_QUEUE = {
213
242
  enabled: DEFAULT_QUEUE_ENABLED,
@@ -222,6 +251,15 @@ var init_constants = __esm({
222
251
  }
223
252
  });
224
253
 
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
+
225
263
  // ../core/dist/config-normalize.js
226
264
  function validateProvider(value) {
227
265
  const trimmed = value.trim();
@@ -263,6 +301,7 @@ function normalizeConfig(rawConfig) {
263
301
  normalized.maxRetries = readNumber(rawConfig.maxRetries);
264
302
  normalized.reviewerMaxRetries = readNumber(rawConfig.reviewerMaxRetries);
265
303
  normalized.reviewerRetryDelay = readNumber(rawConfig.reviewerRetryDelay);
304
+ normalized.reviewerMaxPrsPerRun = readNumber(rawConfig.reviewerMaxPrsPerRun);
266
305
  normalized.provider = validateProvider(String(rawConfig.provider ?? "")) ?? void 0;
267
306
  normalized.executorEnabled = readBoolean(rawConfig.executorEnabled);
268
307
  normalized.reviewerEnabled = readBoolean(rawConfig.reviewerEnabled);
@@ -416,6 +455,20 @@ function normalizeConfig(rawConfig) {
416
455
  };
417
456
  normalized.audit = audit;
418
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;
471
+ }
419
472
  const rawJobProviders = readObject(rawConfig.jobProviders);
420
473
  if (rawJobProviders) {
421
474
  const jobProviders = {};
@@ -475,6 +528,7 @@ function normalizeConfig(rawConfig) {
475
528
  var init_config_normalize = __esm({
476
529
  "../core/dist/config-normalize.js"() {
477
530
  "use strict";
531
+ init_types2();
478
532
  init_constants();
479
533
  }
480
534
  });
@@ -558,6 +612,11 @@ function buildEnvOverrideConfig(fileConfig) {
558
612
  if (!isNaN(v) && v >= 0)
559
613
  env.reviewerRetryDelay = v;
560
614
  }
615
+ if (process.env.NW_REVIEWER_MAX_PRS_PER_RUN !== void 0) {
616
+ const v = parseInt(process.env.NW_REVIEWER_MAX_PRS_PER_RUN, 10);
617
+ if (!isNaN(v) && v >= 0)
618
+ env.reviewerMaxPrsPerRun = v;
619
+ }
561
620
  if (process.env.NW_PROVIDER) {
562
621
  const p = validateProvider(process.env.NW_PROVIDER);
563
622
  if (p !== null)
@@ -693,6 +752,25 @@ function buildEnvOverrideConfig(fileConfig) {
693
752
  if (!isNaN(v) && v > 0)
694
753
  env.audit = { ...auditBase(), maxRuntime: v };
695
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
+ }
696
774
  const jobProvidersEnv = {};
697
775
  for (const jobType of VALID_JOB_TYPES) {
698
776
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -767,6 +845,7 @@ function getDefaultConfig() {
767
845
  maxRetries: DEFAULT_MAX_RETRIES,
768
846
  reviewerMaxRetries: DEFAULT_REVIEWER_MAX_RETRIES,
769
847
  reviewerRetryDelay: DEFAULT_REVIEWER_RETRY_DELAY,
848
+ reviewerMaxPrsPerRun: DEFAULT_REVIEWER_MAX_PRS_PER_RUN,
770
849
  provider: DEFAULT_PROVIDER,
771
850
  executorEnabled: DEFAULT_EXECUTOR_ENABLED,
772
851
  reviewerEnabled: DEFAULT_REVIEWER_ENABLED,
@@ -784,6 +863,7 @@ function getDefaultConfig() {
784
863
  claudeModel: DEFAULT_CLAUDE_MODEL,
785
864
  qa: { ...DEFAULT_QA },
786
865
  audit: { ...DEFAULT_AUDIT },
866
+ analytics: { ...DEFAULT_ANALYTICS },
787
867
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
788
868
  queue: { ...DEFAULT_QUEUE }
789
869
  };
@@ -827,6 +907,16 @@ function sanitizeReviewerRetryDelay(value, fallback) {
827
907
  return 300;
828
908
  return n;
829
909
  }
910
+ function sanitizeReviewerMaxPrsPerRun(value, fallback) {
911
+ if (!Number.isFinite(value))
912
+ return fallback;
913
+ const n = Math.floor(value);
914
+ if (n < 0)
915
+ return 0;
916
+ if (n > 100)
917
+ return 100;
918
+ return n;
919
+ }
830
920
  function mergeConfigLayer(base, layer) {
831
921
  for (const _key of Object.keys(layer)) {
832
922
  const value = layer[_key];
@@ -840,7 +930,7 @@ function mergeConfigLayer(base, layer) {
840
930
  ...layerQueue,
841
931
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
842
932
  };
843
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit") {
933
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
844
934
  base[_key] = {
845
935
  ...base[_key],
846
936
  ...value
@@ -862,6 +952,7 @@ function mergeConfigs(base, fileConfig, envConfig) {
862
952
  merged.maxRetries = sanitizeMaxRetries(merged.maxRetries, DEFAULT_MAX_RETRIES);
863
953
  merged.reviewerMaxRetries = sanitizeReviewerMaxRetries(merged.reviewerMaxRetries, DEFAULT_REVIEWER_MAX_RETRIES);
864
954
  merged.reviewerRetryDelay = sanitizeReviewerRetryDelay(merged.reviewerRetryDelay, DEFAULT_REVIEWER_RETRY_DELAY);
955
+ merged.reviewerMaxPrsPerRun = sanitizeReviewerMaxPrsPerRun(merged.reviewerMaxPrsPerRun, DEFAULT_REVIEWER_MAX_PRS_PER_RUN);
865
956
  merged.primaryFallbackModel = merged.primaryFallbackModel ?? merged.claudeModel ?? DEFAULT_PRIMARY_FALLBACK_MODEL;
866
957
  merged.secondaryFallbackModel = merged.secondaryFallbackModel ?? merged.primaryFallbackModel ?? DEFAULT_SECONDARY_FALLBACK_MODEL;
867
958
  merged.claudeModel = merged.primaryFallbackModel;
@@ -924,15 +1015,6 @@ var init_config = __esm({
924
1015
  }
925
1016
  });
926
1017
 
927
- // ../core/dist/board/types.js
928
- var BOARD_COLUMNS;
929
- var init_types2 = __esm({
930
- "../core/dist/board/types.js"() {
931
- "use strict";
932
- BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
933
- }
934
- });
935
-
936
1018
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
937
1019
  import Database from "better-sqlite3";
938
1020
  import { inject, injectable } from "tsyringe";
@@ -3027,6 +3109,9 @@ function auditLockPath(projectDir) {
3027
3109
  function plannerLockPath(projectDir) {
3028
3110
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
3029
3111
  }
3112
+ function analyticsLockPath(projectDir) {
3113
+ return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
3114
+ }
3030
3115
  function isProcessRunning(pid) {
3031
3116
  try {
3032
3117
  process.kill(pid, 0);
@@ -3385,7 +3470,8 @@ function collectLogInfo(projectDir) {
3385
3470
  { name: "reviewer", fileName: "reviewer.log" },
3386
3471
  { name: "qa", fileName: `${QA_LOG_NAME}.log` },
3387
3472
  { name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
3388
- { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
3473
+ { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
3474
+ { name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
3389
3475
  ];
3390
3476
  return logEntries.map(({ name, fileName }) => {
3391
3477
  const logPath = path6.join(projectDir, LOG_DIR, fileName);
@@ -3414,12 +3500,14 @@ async function fetchStatusSnapshot(projectDir, config) {
3414
3500
  const qaLock = checkLockFile(qaLockPath(projectDir));
3415
3501
  const auditLock = checkLockFile(auditLockPath(projectDir));
3416
3502
  const plannerLock = checkLockFile(plannerLockPath(projectDir));
3503
+ const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
3417
3504
  const processes = [
3418
3505
  { name: "executor", running: executorLock.running, pid: executorLock.pid },
3419
3506
  { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
3420
3507
  { name: "qa", running: qaLock.running, pid: qaLock.pid },
3421
3508
  { name: "audit", running: auditLock.running, pid: auditLock.pid },
3422
- { name: "planner", running: plannerLock.running, pid: plannerLock.pid }
3509
+ { name: "planner", running: plannerLock.running, pid: plannerLock.pid },
3510
+ { name: "analytics", running: analyticsLock.running, pid: analyticsLock.pid }
3423
3511
  ];
3424
3512
  const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
3425
3513
  const prs = await collectPrInfo(projectDir, config.branchPatterns);
@@ -3760,6 +3848,18 @@ import * as path8 from "path";
3760
3848
  function isPlainObject(value) {
3761
3849
  return value !== null && typeof value === "object" && !Array.isArray(value);
3762
3850
  }
3851
+ function cleanJobProviders(value) {
3852
+ if (!isPlainObject(value)) {
3853
+ return {};
3854
+ }
3855
+ const entries = [];
3856
+ for (const [key, provider] of Object.entries(value)) {
3857
+ if (typeof provider === "string" && provider.trim().length > 0) {
3858
+ entries.push([key, provider]);
3859
+ }
3860
+ }
3861
+ return Object.fromEntries(entries);
3862
+ }
3763
3863
  function saveConfig(projectDir, changes) {
3764
3864
  const configPath = path8.join(projectDir, CONFIG_FILE_NAME);
3765
3865
  try {
@@ -3771,7 +3871,9 @@ function saveConfig(projectDir, changes) {
3771
3871
  const merged = { ...existing };
3772
3872
  for (const [key, value] of Object.entries(changes)) {
3773
3873
  if (value !== void 0) {
3774
- if (PARTIAL_MERGE_KEYS.has(key) && isPlainObject(existing[key]) && isPlainObject(value)) {
3874
+ if (key === "jobProviders") {
3875
+ merged[key] = cleanJobProviders(value);
3876
+ } else if (PARTIAL_MERGE_KEYS.has(key) && isPlainObject(existing[key]) && isPlainObject(value)) {
3775
3877
  merged[key] = { ...existing[key], ...value };
3776
3878
  } else {
3777
3879
  merged[key] = value;
@@ -3792,7 +3894,14 @@ var init_config_writer = __esm({
3792
3894
  "../core/dist/utils/config-writer.js"() {
3793
3895
  "use strict";
3794
3896
  init_constants();
3795
- PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set(["notifications", "qa", "audit", "roadmapScanner", "queue", "providerPresets", "jobProviders"]);
3897
+ PARTIAL_MERGE_KEYS = /* @__PURE__ */ new Set([
3898
+ "notifications",
3899
+ "qa",
3900
+ "audit",
3901
+ "roadmapScanner",
3902
+ "queue",
3903
+ "providerPresets"
3904
+ ]);
3796
3905
  }
3797
3906
  });
3798
3907
 
@@ -5588,6 +5697,8 @@ function isJobTypeEnabled(config, jobType) {
5588
5697
  return config.audit.enabled;
5589
5698
  case "slicer":
5590
5699
  return config.roadmapScanner.enabled;
5700
+ case "analytics":
5701
+ return config.analytics.enabled;
5591
5702
  default:
5592
5703
  return true;
5593
5704
  }
@@ -5947,6 +6058,8 @@ function getLockPathForJob(projectPath, jobType) {
5947
6058
  return auditLockPath(projectPath);
5948
6059
  case "slicer":
5949
6060
  return plannerLockPath(projectPath);
6061
+ case "analytics":
6062
+ return analyticsLockPath(projectPath);
5950
6063
  }
5951
6064
  }
5952
6065
  function reconcileStaleRunningJobs(db) {
@@ -6126,7 +6239,10 @@ function fitsProviderCapacity(candidate, config, inFlightByBucket) {
6126
6239
  }
6127
6240
  const bucketConfig = config?.providerBuckets?.[bucketKey];
6128
6241
  if (!bucketConfig) {
6129
- logger.debug("Capacity check skipped: bucket not configured", { id: candidate.id, bucket: bucketKey });
6242
+ logger.debug("Capacity check skipped: bucket not configured", {
6243
+ id: candidate.id,
6244
+ bucket: bucketKey
6245
+ });
6130
6246
  return true;
6131
6247
  }
6132
6248
  const inFlightCount = inFlightByBucket[bucketKey] ?? 0;
@@ -6157,7 +6273,10 @@ function dispatchNextJob(config) {
6157
6273
  const runningCount = running?.count ?? 0;
6158
6274
  logger.debug("Dispatch attempt", { mode, runningCount, maxConcurrency });
6159
6275
  if (runningCount >= maxConcurrency) {
6160
- logger.info("Dispatch skipped: global concurrency limit reached", { runningCount, maxConcurrency });
6276
+ logger.info("Dispatch skipped: global concurrency limit reached", {
6277
+ runningCount,
6278
+ maxConcurrency
6279
+ });
6161
6280
  return null;
6162
6281
  }
6163
6282
  const now = Math.floor(Date.now() / 1e3);
@@ -6183,7 +6302,9 @@ function dispatchNextJob(config) {
6183
6302
  logger.debug("Dispatch skipped: no pending jobs");
6184
6303
  return null;
6185
6304
  }
6186
- logger.debug("Provider-aware dispatch: evaluating candidates", { candidateCount: candidates.length });
6305
+ logger.debug("Provider-aware dispatch: evaluating candidates", {
6306
+ candidateCount: candidates.length
6307
+ });
6187
6308
  const inFlightByBucket = getInFlightCountByBucket(db);
6188
6309
  for (const candidate of candidates) {
6189
6310
  if (fitsProviderCapacity(candidate, config, inFlightByBucket)) {
@@ -6259,7 +6380,11 @@ function clearQueue(filter, force) {
6259
6380
  } else {
6260
6381
  result = db.prepare(`DELETE FROM job_queue WHERE status IN ${statuses}`).run();
6261
6382
  }
6262
- logger.info("Queue cleared", { count: result.changes, filter: filter ?? "all", force: force ?? false });
6383
+ logger.info("Queue cleared", {
6384
+ count: result.changes,
6385
+ filter: filter ?? "all",
6386
+ force: force ?? false
6387
+ });
6263
6388
  return result.changes;
6264
6389
  } finally {
6265
6390
  db.close();
@@ -6392,6 +6517,192 @@ var init_job_queue = __esm({
6392
6517
  }
6393
6518
  });
6394
6519
 
6520
+ // ../core/dist/analytics/amplitude-client.js
6521
+ function buildAuthHeader(apiKey, secretKey) {
6522
+ return `Basic ${Buffer.from(`${apiKey}:${secretKey}`).toString("base64")}`;
6523
+ }
6524
+ function buildDateRange(lookbackDays) {
6525
+ const end = /* @__PURE__ */ new Date();
6526
+ const start = /* @__PURE__ */ new Date();
6527
+ start.setDate(start.getDate() - lookbackDays);
6528
+ const fmt = (d) => d.toISOString().slice(0, 10).replace(/-/g, "");
6529
+ return { start: fmt(start), end: fmt(end) };
6530
+ }
6531
+ async function amplitudeFetch(url, authHeader, label2) {
6532
+ logger2.debug(`Fetching ${label2}`, { url });
6533
+ const response = await fetch(url, {
6534
+ headers: { Authorization: authHeader }
6535
+ });
6536
+ if (!response.ok) {
6537
+ if (response.status === 401) {
6538
+ throw new Error(`Amplitude authentication failed (401). Check your API Key and Secret Key.`);
6539
+ }
6540
+ if (response.status === 429) {
6541
+ throw new Error(`Amplitude rate limit exceeded (429). Try again later.`);
6542
+ }
6543
+ throw new Error(`Amplitude API error: ${response.status} ${response.statusText}`);
6544
+ }
6545
+ return response.json();
6546
+ }
6547
+ async function fetchAmplitudeData(apiKey, secretKey, lookbackDays) {
6548
+ const authHeader = buildAuthHeader(apiKey, secretKey);
6549
+ const { start, end } = buildDateRange(lookbackDays);
6550
+ logger2.info("Fetching Amplitude data", { lookbackDays, start, end });
6551
+ const baseUrl = "https://amplitude.com/api/2";
6552
+ const allEventsParam = encodeURIComponent('{"event_type":"_all"}');
6553
+ const [activeUsers, eventSegmentation, retention, userSessions] = await Promise.allSettled([
6554
+ amplitudeFetch(`${baseUrl}/users/active?start=${start}&end=${end}`, authHeader, "active users"),
6555
+ amplitudeFetch(`${baseUrl}/events/segmentation?start=${start}&end=${end}&e=${allEventsParam}`, authHeader, "event segmentation"),
6556
+ amplitudeFetch(`${baseUrl}/retention?se=${allEventsParam}&re=${allEventsParam}&start=${start}&end=${end}`, authHeader, "retention"),
6557
+ amplitudeFetch(`${baseUrl}/sessions/average?start=${start}&end=${end}`, authHeader, "user sessions")
6558
+ ]);
6559
+ const settled = [activeUsers, eventSegmentation, retention, userSessions];
6560
+ const labels = ["active users", "event segmentation", "retention", "user sessions"];
6561
+ if (settled.every((r) => r.status === "rejected")) {
6562
+ throw settled[0].reason;
6563
+ }
6564
+ const extract = (result, label2) => {
6565
+ if (result.status === "fulfilled")
6566
+ return result.value;
6567
+ logger2.warn(`Failed to fetch ${label2}`, { error: String(result.reason) });
6568
+ return null;
6569
+ };
6570
+ return {
6571
+ activeUsers: extract(activeUsers, labels[0]),
6572
+ eventSegmentation: extract(eventSegmentation, labels[1]),
6573
+ retention: extract(retention, labels[2]),
6574
+ userSessions: extract(userSessions, labels[3]),
6575
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
6576
+ lookbackDays
6577
+ };
6578
+ }
6579
+ var logger2;
6580
+ var init_amplitude_client = __esm({
6581
+ "../core/dist/analytics/amplitude-client.js"() {
6582
+ "use strict";
6583
+ init_logger();
6584
+ logger2 = createLogger("amplitude-client");
6585
+ }
6586
+ });
6587
+
6588
+ // ../core/dist/analytics/analytics-runner.js
6589
+ import * as fs18 from "fs";
6590
+ import * as os7 from "os";
6591
+ import * as path18 from "path";
6592
+ function parseIssuesFromResponse(text) {
6593
+ const start = text.indexOf("[");
6594
+ const end = text.lastIndexOf("]");
6595
+ if (start === -1 || end === -1 || end <= start)
6596
+ return [];
6597
+ try {
6598
+ const parsed = JSON.parse(text.slice(start, end + 1));
6599
+ if (!Array.isArray(parsed))
6600
+ return [];
6601
+ return parsed.filter((item) => typeof item === "object" && item !== null && typeof item.title === "string" && typeof item.body === "string");
6602
+ } catch {
6603
+ logger3.warn("Failed to parse AI response as JSON");
6604
+ return [];
6605
+ }
6606
+ }
6607
+ async function runAnalytics(config, projectDir) {
6608
+ const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
6609
+ const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
6610
+ if (!apiKey || !secretKey) {
6611
+ throw new Error("AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics");
6612
+ }
6613
+ logger3.info("Fetching Amplitude data", { lookbackDays: config.analytics.lookbackDays });
6614
+ const data = await fetchAmplitudeData(apiKey, secretKey, config.analytics.lookbackDays);
6615
+ const systemPrompt = config.analytics.analysisPrompt?.trim() || DEFAULT_ANALYTICS_PROMPT;
6616
+ const prompt2 = `${systemPrompt}
6617
+
6618
+ --- AMPLITUDE DATA ---
6619
+ ${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");
6623
+ try {
6624
+ const provider = resolveJobProvider(config, "analytics");
6625
+ const providerCmd = PROVIDER_COMMANDS[provider];
6626
+ let scriptContent;
6627
+ if (provider === "claude") {
6628
+ const modelId = CLAUDE_MODEL_IDS[config.claudeModel ?? "sonnet"];
6629
+ scriptContent = `#!/usr/bin/env bash
6630
+ set -euo pipefail
6631
+ ${providerCmd} -p "$(cat ${promptFile})" --model ${modelId} --dangerously-skip-permissions 2>&1
6632
+ `;
6633
+ } else {
6634
+ scriptContent = `#!/usr/bin/env bash
6635
+ set -euo pipefail
6636
+ ${providerCmd} exec --yolo "$(cat ${promptFile})" 2>&1
6637
+ `;
6638
+ }
6639
+ const scriptFile = path18.join(tmpDir, "run-analytics.sh");
6640
+ fs18.writeFileSync(scriptFile, scriptContent, { mode: 493 });
6641
+ const { exitCode, stdout, stderr } = await executeScriptWithOutput(scriptFile, [], config.providerEnv ?? {});
6642
+ if (exitCode !== 0) {
6643
+ throw new Error(`AI provider exited with code ${exitCode}: ${stderr || stdout}`);
6644
+ }
6645
+ const fullOutput = `${stdout}
6646
+ ${stderr}`;
6647
+ const issues = parseIssuesFromResponse(fullOutput);
6648
+ if (issues.length === 0) {
6649
+ logger3.info("No actionable insights found");
6650
+ return { issuesCreated: 0, summary: "No actionable insights found" };
6651
+ }
6652
+ const boardProvider = createBoardProvider(config.boardProvider, projectDir);
6653
+ const targetColumn = config.analytics.targetColumn;
6654
+ let created = 0;
6655
+ for (const issue of issues) {
6656
+ try {
6657
+ await boardProvider.createIssue({
6658
+ title: issue.title,
6659
+ body: issue.body,
6660
+ column: targetColumn,
6661
+ labels: issue.labels ?? ["analytics"]
6662
+ });
6663
+ created++;
6664
+ logger3.info("Created board issue", { title: issue.title, column: targetColumn });
6665
+ } catch (err) {
6666
+ logger3.error("Failed to create board issue", {
6667
+ title: issue.title,
6668
+ error: String(err)
6669
+ });
6670
+ }
6671
+ }
6672
+ return {
6673
+ issuesCreated: created,
6674
+ summary: `Created ${created} issue(s) from analytics insights`
6675
+ };
6676
+ } finally {
6677
+ try {
6678
+ fs18.rmSync(tmpDir, { recursive: true, force: true });
6679
+ } catch {
6680
+ }
6681
+ }
6682
+ }
6683
+ var logger3;
6684
+ var init_analytics_runner = __esm({
6685
+ "../core/dist/analytics/analytics-runner.js"() {
6686
+ "use strict";
6687
+ init_factory();
6688
+ init_config();
6689
+ init_constants();
6690
+ init_shell();
6691
+ init_logger();
6692
+ init_amplitude_client();
6693
+ logger3 = createLogger("analytics");
6694
+ }
6695
+ });
6696
+
6697
+ // ../core/dist/analytics/index.js
6698
+ var init_analytics = __esm({
6699
+ "../core/dist/analytics/index.js"() {
6700
+ "use strict";
6701
+ init_amplitude_client();
6702
+ init_analytics_runner();
6703
+ }
6704
+ });
6705
+
6395
6706
  // ../core/dist/templates/prd-template.js
6396
6707
  function renderDependsOn(deps) {
6397
6708
  if (deps.length === 0) {
@@ -6568,6 +6879,7 @@ sequenceDiagram
6568
6879
  // ../core/dist/index.js
6569
6880
  var dist_exports = {};
6570
6881
  __export(dist_exports, {
6882
+ ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
6571
6883
  AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
6572
6884
  BOARD_COLUMNS: () => BOARD_COLUMNS,
6573
6885
  BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
@@ -6579,6 +6891,13 @@ __export(dist_exports, {
6579
6891
  CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
6580
6892
  CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
6581
6893
  DATABASE_TOKEN: () => DATABASE_TOKEN,
6894
+ DEFAULT_ANALYTICS: () => DEFAULT_ANALYTICS,
6895
+ DEFAULT_ANALYTICS_ENABLED: () => DEFAULT_ANALYTICS_ENABLED,
6896
+ DEFAULT_ANALYTICS_LOOKBACK_DAYS: () => DEFAULT_ANALYTICS_LOOKBACK_DAYS,
6897
+ DEFAULT_ANALYTICS_MAX_RUNTIME: () => DEFAULT_ANALYTICS_MAX_RUNTIME,
6898
+ DEFAULT_ANALYTICS_PROMPT: () => DEFAULT_ANALYTICS_PROMPT,
6899
+ DEFAULT_ANALYTICS_SCHEDULE: () => DEFAULT_ANALYTICS_SCHEDULE,
6900
+ DEFAULT_ANALYTICS_TARGET_COLUMN: () => DEFAULT_ANALYTICS_TARGET_COLUMN,
6582
6901
  DEFAULT_AUDIT: () => DEFAULT_AUDIT,
6583
6902
  DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
6584
6903
  DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
@@ -6620,6 +6939,7 @@ __export(dist_exports, {
6620
6939
  DEFAULT_QUEUE_MODE: () => DEFAULT_QUEUE_MODE,
6621
6940
  DEFAULT_QUEUE_PRIORITY: () => DEFAULT_QUEUE_PRIORITY,
6622
6941
  DEFAULT_REVIEWER_ENABLED: () => DEFAULT_REVIEWER_ENABLED,
6942
+ DEFAULT_REVIEWER_MAX_PRS_PER_RUN: () => DEFAULT_REVIEWER_MAX_PRS_PER_RUN,
6623
6943
  DEFAULT_REVIEWER_MAX_RETRIES: () => DEFAULT_REVIEWER_MAX_RETRIES,
6624
6944
  DEFAULT_REVIEWER_MAX_RUNTIME: () => DEFAULT_REVIEWER_MAX_RUNTIME,
6625
6945
  DEFAULT_REVIEWER_RETRY_DELAY: () => DEFAULT_REVIEWER_RETRY_DELAY,
@@ -6664,6 +6984,7 @@ __export(dist_exports, {
6664
6984
  acquireLock: () => acquireLock,
6665
6985
  addDelayToIsoString: () => addDelayToIsoString,
6666
6986
  addEntry: () => addEntry,
6987
+ analyticsLockPath: () => analyticsLockPath,
6667
6988
  auditLockPath: () => auditLockPath,
6668
6989
  buildDescription: () => buildDescription,
6669
6990
  calculateStringSimilarity: () => calculateStringSimilarity,
@@ -6714,6 +7035,7 @@ __export(dist_exports, {
6714
7035
  extractPriority: () => extractPriority,
6715
7036
  extractQaScreenshotUrls: () => extractQaScreenshotUrls,
6716
7037
  extractSummary: () => extractSummary,
7038
+ fetchAmplitudeData: () => fetchAmplitudeData,
6717
7039
  fetchLatestQaCommentBody: () => fetchLatestQaCommentBody,
6718
7040
  fetchPrDetails: () => fetchPrDetails,
6719
7041
  fetchPrDetailsByNumber: () => fetchPrDetailsByNumber,
@@ -6821,6 +7143,7 @@ __export(dist_exports, {
6821
7143
  reviewerLockPath: () => reviewerLockPath,
6822
7144
  rotateLog: () => rotateLog,
6823
7145
  runAllChecks: () => runAllChecks,
7146
+ runAnalytics: () => runAnalytics,
6824
7147
  runMigrations: () => runMigrations,
6825
7148
  saveConfig: () => saveConfig,
6826
7149
  saveHistory: () => saveHistory,
@@ -6889,6 +7212,7 @@ var init_dist = __esm({
6889
7212
  init_webhook_validator();
6890
7213
  init_worktree_manager();
6891
7214
  init_job_queue();
7215
+ init_analytics();
6892
7216
  init_prd_template();
6893
7217
  init_slicer_prompt();
6894
7218
  }
@@ -6899,37 +7223,37 @@ import "reflect-metadata";
6899
7223
  import { Command as Command3 } from "commander";
6900
7224
  import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
6901
7225
  import { fileURLToPath as fileURLToPath4 } from "url";
6902
- import { dirname as dirname8, join as join34 } from "path";
7226
+ import { dirname as dirname8, join as join35 } from "path";
6903
7227
 
6904
7228
  // src/commands/init.ts
6905
7229
  init_dist();
6906
- import fs18 from "fs";
6907
- import path18 from "path";
7230
+ import fs19 from "fs";
7231
+ import path19 from "path";
6908
7232
  import { execSync as execSync3 } from "child_process";
6909
7233
  import { fileURLToPath as fileURLToPath2 } from "url";
6910
- import { dirname as dirname4, join as join16 } from "path";
7234
+ import { dirname as dirname4, join as join17 } from "path";
6911
7235
  import * as readline from "readline";
6912
7236
  var __filename = fileURLToPath2(import.meta.url);
6913
7237
  var __dirname2 = dirname4(__filename);
6914
7238
  function findTemplatesDir(startDir) {
6915
7239
  let d = startDir;
6916
7240
  for (let i = 0; i < 8; i++) {
6917
- const candidate = join16(d, "templates");
6918
- if (fs18.existsSync(candidate) && fs18.statSync(candidate).isDirectory()) {
7241
+ const candidate = join17(d, "templates");
7242
+ if (fs19.existsSync(candidate) && fs19.statSync(candidate).isDirectory()) {
6919
7243
  return candidate;
6920
7244
  }
6921
7245
  d = dirname4(d);
6922
7246
  }
6923
- return join16(startDir, "templates");
7247
+ return join17(startDir, "templates");
6924
7248
  }
6925
7249
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
6926
7250
  function hasPlaywrightDependency(cwd) {
6927
- const packageJsonPath = path18.join(cwd, "package.json");
6928
- if (!fs18.existsSync(packageJsonPath)) {
7251
+ const packageJsonPath = path19.join(cwd, "package.json");
7252
+ if (!fs19.existsSync(packageJsonPath)) {
6929
7253
  return false;
6930
7254
  }
6931
7255
  try {
6932
- const packageJson2 = JSON.parse(fs18.readFileSync(packageJsonPath, "utf-8"));
7256
+ const packageJson2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
6933
7257
  return Boolean(
6934
7258
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
6935
7259
  );
@@ -6941,7 +7265,7 @@ function detectPlaywright(cwd) {
6941
7265
  if (hasPlaywrightDependency(cwd)) {
6942
7266
  return true;
6943
7267
  }
6944
- if (fs18.existsSync(path18.join(cwd, "node_modules", ".bin", "playwright"))) {
7268
+ if (fs19.existsSync(path19.join(cwd, "node_modules", ".bin", "playwright"))) {
6945
7269
  return true;
6946
7270
  }
6947
7271
  try {
@@ -6957,10 +7281,10 @@ function detectPlaywright(cwd) {
6957
7281
  }
6958
7282
  }
6959
7283
  function resolvePlaywrightInstallCommand(cwd) {
6960
- if (fs18.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) {
7284
+ if (fs19.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) {
6961
7285
  return "pnpm add -D @playwright/test";
6962
7286
  }
6963
- if (fs18.existsSync(path18.join(cwd, "yarn.lock"))) {
7287
+ if (fs19.existsSync(path19.join(cwd, "yarn.lock"))) {
6964
7288
  return "yarn add -D @playwright/test";
6965
7289
  }
6966
7290
  return "npm install -D @playwright/test";
@@ -7106,8 +7430,8 @@ function promptProviderSelection(providers) {
7106
7430
  });
7107
7431
  }
7108
7432
  function ensureDir(dirPath) {
7109
- if (!fs18.existsSync(dirPath)) {
7110
- fs18.mkdirSync(dirPath, { recursive: true });
7433
+ if (!fs19.existsSync(dirPath)) {
7434
+ fs19.mkdirSync(dirPath, { recursive: true });
7111
7435
  }
7112
7436
  }
7113
7437
  function buildInitConfig(params) {
@@ -7130,6 +7454,7 @@ function buildInitConfig(params) {
7130
7454
  schedulingPriority: defaults.schedulingPriority,
7131
7455
  maxRetries: defaults.maxRetries,
7132
7456
  reviewerMaxRetries: defaults.reviewerMaxRetries,
7457
+ reviewerMaxPrsPerRun: defaults.reviewerMaxPrsPerRun,
7133
7458
  reviewerRetryDelay: defaults.reviewerRetryDelay,
7134
7459
  provider: params.provider,
7135
7460
  providerLabel: "",
@@ -7153,6 +7478,7 @@ function buildInitConfig(params) {
7153
7478
  branchPatterns: [...defaults.qa.branchPatterns]
7154
7479
  },
7155
7480
  audit: { ...defaults.audit },
7481
+ analytics: { ...defaults.analytics },
7156
7482
  jobProviders: { ...defaults.jobProviders },
7157
7483
  queue: {
7158
7484
  ...defaults.queue,
@@ -7162,30 +7488,30 @@ function buildInitConfig(params) {
7162
7488
  }
7163
7489
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
7164
7490
  if (customTemplatesDir !== null) {
7165
- const customPath = join16(customTemplatesDir, templateName);
7166
- if (fs18.existsSync(customPath)) {
7491
+ const customPath = join17(customTemplatesDir, templateName);
7492
+ if (fs19.existsSync(customPath)) {
7167
7493
  return { path: customPath, source: "custom" };
7168
7494
  }
7169
7495
  }
7170
- return { path: join16(bundledTemplatesDir, templateName), source: "bundled" };
7496
+ return { path: join17(bundledTemplatesDir, templateName), source: "bundled" };
7171
7497
  }
7172
7498
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
7173
- if (fs18.existsSync(targetPath) && !force) {
7499
+ if (fs19.existsSync(targetPath) && !force) {
7174
7500
  console.log(` Skipped (exists): ${targetPath}`);
7175
7501
  return { created: false, source: source ?? "bundled" };
7176
7502
  }
7177
- const templatePath = sourcePath ?? join16(TEMPLATES_DIR, templateName);
7503
+ const templatePath = sourcePath ?? join17(TEMPLATES_DIR, templateName);
7178
7504
  const resolvedSource = source ?? "bundled";
7179
- let content = fs18.readFileSync(templatePath, "utf-8");
7505
+ let content = fs19.readFileSync(templatePath, "utf-8");
7180
7506
  for (const [key, value] of Object.entries(replacements)) {
7181
7507
  content = content.replaceAll(key, value);
7182
7508
  }
7183
- fs18.writeFileSync(targetPath, content);
7509
+ fs19.writeFileSync(targetPath, content);
7184
7510
  console.log(` Created: ${targetPath} (${resolvedSource})`);
7185
7511
  return { created: true, source: resolvedSource };
7186
7512
  }
7187
7513
  function addToGitignore(cwd) {
7188
- const gitignorePath = path18.join(cwd, ".gitignore");
7514
+ const gitignorePath = path19.join(cwd, ".gitignore");
7189
7515
  const entries = [
7190
7516
  {
7191
7517
  pattern: "/logs/",
@@ -7199,13 +7525,13 @@ function addToGitignore(cwd) {
7199
7525
  },
7200
7526
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
7201
7527
  ];
7202
- if (!fs18.existsSync(gitignorePath)) {
7528
+ if (!fs19.existsSync(gitignorePath)) {
7203
7529
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
7204
- fs18.writeFileSync(gitignorePath, lines.join("\n"));
7530
+ fs19.writeFileSync(gitignorePath, lines.join("\n"));
7205
7531
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
7206
7532
  return;
7207
7533
  }
7208
- const content = fs18.readFileSync(gitignorePath, "utf-8");
7534
+ const content = fs19.readFileSync(gitignorePath, "utf-8");
7209
7535
  const missing = entries.filter((e) => !e.check(content));
7210
7536
  if (missing.length === 0) {
7211
7537
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -7213,7 +7539,7 @@ function addToGitignore(cwd) {
7213
7539
  }
7214
7540
  const additions = missing.map((e) => e.pattern).join("\n");
7215
7541
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
7216
- fs18.writeFileSync(gitignorePath, newContent);
7542
+ fs19.writeFileSync(gitignorePath, newContent);
7217
7543
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
7218
7544
  }
7219
7545
  function initCommand(program2) {
@@ -7334,28 +7660,28 @@ function initCommand(program2) {
7334
7660
  "${DEFAULT_BRANCH}": defaultBranch
7335
7661
  };
7336
7662
  step(6, totalSteps, "Creating PRD directory structure...");
7337
- const prdDirPath = path18.join(cwd, prdDir);
7338
- const doneDirPath = path18.join(prdDirPath, "done");
7663
+ const prdDirPath = path19.join(cwd, prdDir);
7664
+ const doneDirPath = path19.join(prdDirPath, "done");
7339
7665
  ensureDir(doneDirPath);
7340
7666
  success(`Created ${prdDirPath}/`);
7341
7667
  success(`Created ${doneDirPath}/`);
7342
7668
  step(7, totalSteps, "Creating logs directory...");
7343
- const logsPath = path18.join(cwd, LOG_DIR);
7669
+ const logsPath = path19.join(cwd, LOG_DIR);
7344
7670
  ensureDir(logsPath);
7345
7671
  success(`Created ${logsPath}/`);
7346
7672
  addToGitignore(cwd);
7347
7673
  step(8, totalSteps, "Creating instructions directory...");
7348
- const instructionsDir = path18.join(cwd, "instructions");
7674
+ const instructionsDir = path19.join(cwd, "instructions");
7349
7675
  ensureDir(instructionsDir);
7350
7676
  success(`Created ${instructionsDir}/`);
7351
7677
  const existingConfig = loadConfig(cwd);
7352
- const customTemplatesDirPath = path18.join(cwd, existingConfig.templatesDir);
7353
- const customTemplatesDir = fs18.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7678
+ const customTemplatesDirPath = path19.join(cwd, existingConfig.templatesDir);
7679
+ const customTemplatesDir = fs19.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7354
7680
  const templateSources = [];
7355
7681
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
7356
7682
  const nwResult = processTemplate(
7357
7683
  "executor.md",
7358
- path18.join(instructionsDir, "executor.md"),
7684
+ path19.join(instructionsDir, "executor.md"),
7359
7685
  replacements,
7360
7686
  force,
7361
7687
  nwResolution.path,
@@ -7369,7 +7695,7 @@ function initCommand(program2) {
7369
7695
  );
7370
7696
  const peResult = processTemplate(
7371
7697
  "prd-executor.md",
7372
- path18.join(instructionsDir, "prd-executor.md"),
7698
+ path19.join(instructionsDir, "prd-executor.md"),
7373
7699
  replacements,
7374
7700
  force,
7375
7701
  peResolution.path,
@@ -7379,7 +7705,7 @@ function initCommand(program2) {
7379
7705
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
7380
7706
  const prResult = processTemplate(
7381
7707
  "pr-reviewer.md",
7382
- path18.join(instructionsDir, "pr-reviewer.md"),
7708
+ path19.join(instructionsDir, "pr-reviewer.md"),
7383
7709
  replacements,
7384
7710
  force,
7385
7711
  prResolution.path,
@@ -7389,7 +7715,7 @@ function initCommand(program2) {
7389
7715
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
7390
7716
  const qaResult = processTemplate(
7391
7717
  "qa.md",
7392
- path18.join(instructionsDir, "qa.md"),
7718
+ path19.join(instructionsDir, "qa.md"),
7393
7719
  replacements,
7394
7720
  force,
7395
7721
  qaResolution.path,
@@ -7399,7 +7725,7 @@ function initCommand(program2) {
7399
7725
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
7400
7726
  const auditResult = processTemplate(
7401
7727
  "audit.md",
7402
- path18.join(instructionsDir, "audit.md"),
7728
+ path19.join(instructionsDir, "audit.md"),
7403
7729
  replacements,
7404
7730
  force,
7405
7731
  auditResolution.path,
@@ -7407,8 +7733,8 @@ function initCommand(program2) {
7407
7733
  );
7408
7734
  templateSources.push({ name: "audit.md", source: auditResult.source });
7409
7735
  step(9, totalSteps, "Creating configuration file...");
7410
- const configPath = path18.join(cwd, CONFIG_FILE_NAME);
7411
- if (fs18.existsSync(configPath) && !force) {
7736
+ const configPath = path19.join(cwd, CONFIG_FILE_NAME);
7737
+ if (fs19.existsSync(configPath) && !force) {
7412
7738
  console.log(` Skipped (exists): ${configPath}`);
7413
7739
  } else {
7414
7740
  const config = buildInitConfig({
@@ -7418,11 +7744,11 @@ function initCommand(program2) {
7418
7744
  reviewerEnabled,
7419
7745
  prdDir
7420
7746
  });
7421
- fs18.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7747
+ fs19.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7422
7748
  success(`Created ${configPath}`);
7423
7749
  }
7424
7750
  step(10, totalSteps, "Setting up GitHub Project board...");
7425
- const existingRaw = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
7751
+ const existingRaw = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
7426
7752
  const existingBoard = existingRaw.boardProvider;
7427
7753
  let boardSetupStatus = "Skipped";
7428
7754
  if (existingBoard?.projectNumber && !force) {
@@ -7436,19 +7762,21 @@ function initCommand(program2) {
7436
7762
  );
7437
7763
  } else if (!ghAuthenticated) {
7438
7764
  boardSetupStatus = "Skipped (gh auth required)";
7439
- info("GitHub CLI is not authenticated \u2014 run `gh auth login`, then `night-watch board setup`.");
7765
+ info(
7766
+ "GitHub CLI is not authenticated \u2014 run `gh auth login`, then `night-watch board setup`."
7767
+ );
7440
7768
  } else {
7441
7769
  try {
7442
7770
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
7443
7771
  const boardTitle = `${projectName} Night Watch`;
7444
7772
  const board = await provider.setupBoard(boardTitle);
7445
- const rawConfig = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
7773
+ const rawConfig = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
7446
7774
  rawConfig.boardProvider = {
7447
7775
  enabled: true,
7448
7776
  provider: "github",
7449
7777
  projectNumber: board.number
7450
7778
  };
7451
- fs18.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7779
+ fs19.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7452
7780
  boardSetupStatus = `Created (#${board.number})`;
7453
7781
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
7454
7782
  } catch (boardErr) {
@@ -7572,8 +7900,8 @@ function getTelegramStatusWebhooks(config) {
7572
7900
  }
7573
7901
 
7574
7902
  // src/commands/run.ts
7575
- import * as fs19 from "fs";
7576
- import * as path19 from "path";
7903
+ import * as fs20 from "fs";
7904
+ import * as path20 from "path";
7577
7905
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
7578
7906
  if (exitCode === 124) {
7579
7907
  return "run_timeout";
@@ -7605,12 +7933,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7605
7933
  return scriptStatus === "skip_no_eligible_prd";
7606
7934
  }
7607
7935
  function getCrossProjectFallbackCandidates(currentProjectDir) {
7608
- const current = path19.resolve(currentProjectDir);
7936
+ const current = path20.resolve(currentProjectDir);
7609
7937
  const { valid, invalid } = validateRegistry();
7610
7938
  for (const entry of invalid) {
7611
7939
  warn(`Skipping invalid registry entry: ${entry.path}`);
7612
7940
  }
7613
- return valid.filter((entry) => path19.resolve(entry.path) !== current);
7941
+ return valid.filter((entry) => path20.resolve(entry.path) !== current);
7614
7942
  }
7615
7943
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
7616
7944
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -7620,7 +7948,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7620
7948
  if (nonTelegramWebhooks.length > 0) {
7621
7949
  const _rateLimitCtx = {
7622
7950
  event: "rate_limit_fallback",
7623
- projectName: path19.basename(projectDir),
7951
+ projectName: path20.basename(projectDir),
7624
7952
  exitCode,
7625
7953
  provider: config.provider
7626
7954
  };
@@ -7648,7 +7976,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7648
7976
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
7649
7977
  const _ctx = {
7650
7978
  event,
7651
- projectName: path19.basename(projectDir),
7979
+ projectName: path20.basename(projectDir),
7652
7980
  exitCode,
7653
7981
  provider: config.provider,
7654
7982
  prdName: scriptResult?.data.prd,
@@ -7810,20 +8138,20 @@ function applyCliOverrides(config, options) {
7810
8138
  return overridden;
7811
8139
  }
7812
8140
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7813
- const absolutePrdDir = path19.join(projectDir, prdDir);
7814
- const doneDir = path19.join(absolutePrdDir, "done");
8141
+ const absolutePrdDir = path20.join(projectDir, prdDir);
8142
+ const doneDir = path20.join(absolutePrdDir, "done");
7815
8143
  const pending = [];
7816
8144
  const completed = [];
7817
- if (fs19.existsSync(absolutePrdDir)) {
7818
- const entries = fs19.readdirSync(absolutePrdDir, { withFileTypes: true });
8145
+ if (fs20.existsSync(absolutePrdDir)) {
8146
+ const entries = fs20.readdirSync(absolutePrdDir, { withFileTypes: true });
7819
8147
  for (const entry of entries) {
7820
8148
  if (entry.isFile() && entry.name.endsWith(".md")) {
7821
- const claimPath = path19.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
8149
+ const claimPath = path20.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
7822
8150
  let claimed = false;
7823
8151
  let claimInfo = null;
7824
- if (fs19.existsSync(claimPath)) {
8152
+ if (fs20.existsSync(claimPath)) {
7825
8153
  try {
7826
- const content = fs19.readFileSync(claimPath, "utf-8");
8154
+ const content = fs20.readFileSync(claimPath, "utf-8");
7827
8155
  const data = JSON.parse(content);
7828
8156
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
7829
8157
  if (age < maxRuntime) {
@@ -7837,8 +8165,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7837
8165
  }
7838
8166
  }
7839
8167
  }
7840
- if (fs19.existsSync(doneDir)) {
7841
- const entries = fs19.readdirSync(doneDir, { withFileTypes: true });
8168
+ if (fs20.existsSync(doneDir)) {
8169
+ const entries = fs20.readdirSync(doneDir, { withFileTypes: true });
7842
8170
  for (const entry of entries) {
7843
8171
  if (entry.isFile() && entry.name.endsWith(".md")) {
7844
8172
  completed.push(entry.name);
@@ -7998,7 +8326,7 @@ ${stderr}`);
7998
8326
  // src/commands/review.ts
7999
8327
  init_dist();
8000
8328
  import { execFileSync as execFileSync5 } from "child_process";
8001
- import * as path20 from "path";
8329
+ import * as path21 from "path";
8002
8330
  function shouldSendReviewNotification(scriptStatus) {
8003
8331
  if (!scriptStatus) {
8004
8332
  return true;
@@ -8033,6 +8361,7 @@ function buildEnvVars2(config, options) {
8033
8361
  env.NW_REVIEWER_MAX_RUNTIME = String(config.reviewerMaxRuntime);
8034
8362
  env.NW_REVIEWER_MAX_RETRIES = String(config.reviewerMaxRetries);
8035
8363
  env.NW_REVIEWER_RETRY_DELAY = String(config.reviewerRetryDelay);
8364
+ env.NW_REVIEWER_MAX_PRS_PER_RUN = String(config.reviewerMaxPrsPerRun);
8036
8365
  env.NW_MIN_REVIEW_SCORE = String(config.minReviewScore);
8037
8366
  env.NW_BRANCH_PATTERNS = config.branchPatterns.join(",");
8038
8367
  env.NW_PRD_DIR = config.prdDir;
@@ -8146,6 +8475,10 @@ function reviewCommand(program2) {
8146
8475
  ]);
8147
8476
  configTable.push(["Max Retry Attempts", String(config.reviewerMaxRetries)]);
8148
8477
  configTable.push(["Retry Delay", `${config.reviewerRetryDelay}s`]);
8478
+ configTable.push([
8479
+ "Max PRs Per Run",
8480
+ config.reviewerMaxPrsPerRun === 0 ? "Unlimited" : String(config.reviewerMaxPrsPerRun)
8481
+ ]);
8149
8482
  console.log(configTable.toString());
8150
8483
  header("Open PRs Needing Work");
8151
8484
  const openPrs = getOpenPrsNeedingWork(config.branchPatterns);
@@ -8233,7 +8566,7 @@ ${stderr}`);
8233
8566
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
8234
8567
  const _reviewCtx = {
8235
8568
  event: "review_completed",
8236
- projectName: path20.basename(projectDir),
8569
+ projectName: path21.basename(projectDir),
8237
8570
  exitCode,
8238
8571
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8239
8572
  prUrl: prDetails?.url,
@@ -8254,7 +8587,7 @@ ${stderr}`);
8254
8587
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
8255
8588
  const _mergeCtx = {
8256
8589
  event: "pr_auto_merged",
8257
- projectName: path20.basename(projectDir),
8590
+ projectName: path21.basename(projectDir),
8258
8591
  exitCode,
8259
8592
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8260
8593
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -8279,7 +8612,7 @@ ${stderr}`);
8279
8612
 
8280
8613
  // src/commands/qa.ts
8281
8614
  init_dist();
8282
- import * as path21 from "path";
8615
+ import * as path22 from "path";
8283
8616
  function shouldSendQaNotification(scriptStatus) {
8284
8617
  if (!scriptStatus) {
8285
8618
  return true;
@@ -8413,7 +8746,7 @@ ${stderr}`);
8413
8746
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
8414
8747
  const _qaCtx = {
8415
8748
  event: "qa_completed",
8416
- projectName: path21.basename(projectDir),
8749
+ projectName: path22.basename(projectDir),
8417
8750
  exitCode,
8418
8751
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8419
8752
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -8439,8 +8772,8 @@ ${stderr}`);
8439
8772
 
8440
8773
  // src/commands/audit.ts
8441
8774
  init_dist();
8442
- import * as fs20 from "fs";
8443
- import * as path22 from "path";
8775
+ import * as fs21 from "fs";
8776
+ import * as path23 from "path";
8444
8777
  function buildEnvVars4(config, options) {
8445
8778
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
8446
8779
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -8483,7 +8816,7 @@ function auditCommand(program2) {
8483
8816
  configTable.push(["Provider", auditProvider]);
8484
8817
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
8485
8818
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
8486
- configTable.push(["Report File", path22.join(projectDir, "logs", "audit-report.md")]);
8819
+ configTable.push(["Report File", path23.join(projectDir, "logs", "audit-report.md")]);
8487
8820
  console.log(configTable.toString());
8488
8821
  header("Provider Invocation");
8489
8822
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -8518,8 +8851,8 @@ ${stderr}`);
8518
8851
  } else if (scriptResult?.status?.startsWith("skip_")) {
8519
8852
  spinner.succeed("Code audit skipped");
8520
8853
  } else {
8521
- const reportPath = path22.join(projectDir, "logs", "audit-report.md");
8522
- if (!fs20.existsSync(reportPath)) {
8854
+ const reportPath = path23.join(projectDir, "logs", "audit-report.md");
8855
+ if (!fs21.existsSync(reportPath)) {
8523
8856
  spinner.fail("Code audit finished without a report file");
8524
8857
  process.exit(1);
8525
8858
  }
@@ -8530,9 +8863,9 @@ ${stderr}`);
8530
8863
  const providerExit = scriptResult?.data?.provider_exit;
8531
8864
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
8532
8865
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
8533
- const logPath = path22.join(projectDir, "logs", "audit.log");
8534
- if (fs20.existsSync(logPath)) {
8535
- const logLines = fs20.readFileSync(logPath, "utf-8").split("\n").filter((l) => l.trim()).slice(-8);
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);
8536
8869
  if (logLines.length > 0) {
8537
8870
  process.stderr.write(logLines.join("\n") + "\n");
8538
8871
  }
@@ -8546,19 +8879,80 @@ ${stderr}`);
8546
8879
  });
8547
8880
  }
8548
8881
 
8882
+ // src/commands/analytics.ts
8883
+ init_dist();
8884
+ function analyticsCommand(program2) {
8885
+ program2.command("analytics").description("Run Amplitude analytics job now").option("--dry-run", "Show what would be executed without running").option("--timeout <seconds>", "Override max runtime in seconds").option("--provider <string>", "AI provider to use (claude or codex)").action(async (options) => {
8886
+ const projectDir = process.cwd();
8887
+ let config = loadConfig(projectDir);
8888
+ if (options.timeout) {
8889
+ const timeout = parseInt(options.timeout, 10);
8890
+ if (!isNaN(timeout)) {
8891
+ config = { ...config, analytics: { ...config.analytics, maxRuntime: timeout } };
8892
+ }
8893
+ }
8894
+ if (options.provider) {
8895
+ config = {
8896
+ ...config,
8897
+ _cliProviderOverride: options.provider
8898
+ };
8899
+ }
8900
+ if (!config.analytics.enabled && !options.dryRun) {
8901
+ info("Analytics is disabled in config; skipping run.");
8902
+ process.exit(0);
8903
+ }
8904
+ const apiKey = config.providerEnv?.AMPLITUDE_API_KEY;
8905
+ const secretKey = config.providerEnv?.AMPLITUDE_SECRET_KEY;
8906
+ if (!apiKey || !secretKey) {
8907
+ info(
8908
+ "AMPLITUDE_API_KEY and AMPLITUDE_SECRET_KEY must be set in providerEnv to run analytics."
8909
+ );
8910
+ process.exit(1);
8911
+ }
8912
+ if (options.dryRun) {
8913
+ header("Dry Run: Analytics Job");
8914
+ const analyticsProvider = resolveJobProvider(config, "analytics");
8915
+ header("Configuration");
8916
+ const configTable = createTable({ head: ["Setting", "Value"] });
8917
+ configTable.push(["Provider", analyticsProvider]);
8918
+ configTable.push(["Max Runtime", `${config.analytics.maxRuntime}s`]);
8919
+ configTable.push(["Lookback Days", String(config.analytics.lookbackDays)]);
8920
+ configTable.push(["Target Column", config.analytics.targetColumn]);
8921
+ configTable.push(["Amplitude API Key", apiKey ? "***" + apiKey.slice(-4) : "not set"]);
8922
+ console.log(configTable.toString());
8923
+ console.log();
8924
+ process.exit(0);
8925
+ }
8926
+ const spinner = createSpinner("Running analytics job...");
8927
+ spinner.start();
8928
+ try {
8929
+ await maybeApplyCronSchedulingDelay(config, "analytics", projectDir);
8930
+ const result = await runAnalytics(config, projectDir);
8931
+ if (result.issuesCreated > 0) {
8932
+ spinner.succeed(`Analytics complete \u2014 ${result.summary}`);
8933
+ } else {
8934
+ spinner.succeed("Analytics complete \u2014 no actionable insights found");
8935
+ }
8936
+ } catch (err) {
8937
+ spinner.fail(`Analytics failed: ${err instanceof Error ? err.message : String(err)}`);
8938
+ process.exit(1);
8939
+ }
8940
+ });
8941
+ }
8942
+
8549
8943
  // src/commands/install.ts
8550
8944
  init_dist();
8551
8945
  import { execSync as execSync4 } from "child_process";
8552
- import * as path23 from "path";
8553
- import * as fs21 from "fs";
8946
+ import * as path24 from "path";
8947
+ import * as fs22 from "fs";
8554
8948
  function shellQuote(value) {
8555
8949
  return `'${value.replace(/'/g, `'"'"'`)}'`;
8556
8950
  }
8557
8951
  function getNightWatchBinPath() {
8558
8952
  try {
8559
8953
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
8560
- const binPath = path23.join(npmBin, "night-watch");
8561
- if (fs21.existsSync(binPath)) {
8954
+ const binPath = path24.join(npmBin, "night-watch");
8955
+ if (fs22.existsSync(binPath)) {
8562
8956
  return binPath;
8563
8957
  }
8564
8958
  } catch {
@@ -8571,17 +8965,17 @@ function getNightWatchBinPath() {
8571
8965
  }
8572
8966
  function getNodeBinDir() {
8573
8967
  if (process.execPath && process.execPath !== "node") {
8574
- return path23.dirname(process.execPath);
8968
+ return path24.dirname(process.execPath);
8575
8969
  }
8576
8970
  try {
8577
8971
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
8578
- return path23.dirname(nodePath);
8972
+ return path24.dirname(nodePath);
8579
8973
  } catch {
8580
8974
  return "";
8581
8975
  }
8582
8976
  }
8583
8977
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
8584
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path23.dirname(nightWatchBin) : "";
8978
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path24.dirname(nightWatchBin) : "";
8585
8979
  const pathParts = Array.from(
8586
8980
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
8587
8981
  );
@@ -8597,12 +8991,12 @@ function performInstall(projectDir, config, options) {
8597
8991
  const nightWatchBin = getNightWatchBinPath();
8598
8992
  const projectName = getProjectName(projectDir);
8599
8993
  const marker = generateMarker(projectName);
8600
- const logDir = path23.join(projectDir, LOG_DIR);
8601
- if (!fs21.existsSync(logDir)) {
8602
- fs21.mkdirSync(logDir, { recursive: true });
8994
+ const logDir = path24.join(projectDir, LOG_DIR);
8995
+ if (!fs22.existsSync(logDir)) {
8996
+ fs22.mkdirSync(logDir, { recursive: true });
8603
8997
  }
8604
- const executorLog = path23.join(logDir, "executor.log");
8605
- const reviewerLog = path23.join(logDir, "reviewer.log");
8998
+ const executorLog = path24.join(logDir, "executor.log");
8999
+ const reviewerLog = path24.join(logDir, "reviewer.log");
8606
9000
  if (!options?.force) {
8607
9001
  const existingEntries2 = Array.from(
8608
9002
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -8639,7 +9033,7 @@ function performInstall(projectDir, config, options) {
8639
9033
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
8640
9034
  if (installSlicer) {
8641
9035
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
8642
- const slicerLog = path23.join(logDir, "slicer.log");
9036
+ const slicerLog = path24.join(logDir, "slicer.log");
8643
9037
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8644
9038
  entries.push(slicerEntry);
8645
9039
  }
@@ -8647,7 +9041,7 @@ function performInstall(projectDir, config, options) {
8647
9041
  const installQa = disableQa ? false : config.qa.enabled;
8648
9042
  if (installQa) {
8649
9043
  const qaSchedule = config.qa.schedule;
8650
- const qaLog = path23.join(logDir, "qa.log");
9044
+ const qaLog = path24.join(logDir, "qa.log");
8651
9045
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8652
9046
  entries.push(qaEntry);
8653
9047
  }
@@ -8655,10 +9049,18 @@ function performInstall(projectDir, config, options) {
8655
9049
  const installAudit = disableAudit ? false : config.audit.enabled;
8656
9050
  if (installAudit) {
8657
9051
  const auditSchedule = config.audit.schedule;
8658
- const auditLog = path23.join(logDir, "audit.log");
9052
+ const auditLog = path24.join(logDir, "audit.log");
8659
9053
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8660
9054
  entries.push(auditEntry);
8661
9055
  }
9056
+ const disableAnalytics = options?.noAnalytics === true || options?.analytics === false;
9057
+ const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
9058
+ if (installAnalytics) {
9059
+ const analyticsSchedule = config.analytics.schedule;
9060
+ const analyticsLog = path24.join(logDir, "analytics.log");
9061
+ const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9062
+ entries.push(analyticsEntry);
9063
+ }
8662
9064
  const existingEntries = new Set(
8663
9065
  Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
8664
9066
  );
@@ -8677,7 +9079,7 @@ function performInstall(projectDir, config, options) {
8677
9079
  }
8678
9080
  }
8679
9081
  function installCommand(program2) {
8680
- program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
9082
+ program2.command("install").description("Add crontab entries for automated execution").option("-s, --schedule <cron>", "Cron schedule for PRD executor").option("--reviewer-schedule <cron>", "Cron schedule for reviewer").option("--no-reviewer", "Skip installing reviewer cron").option("--no-slicer", "Skip installing slicer cron").option("--no-qa", "Skip installing QA cron").option("--no-audit", "Skip installing audit cron").option("--no-analytics", "Skip installing analytics cron").option("-f, --force", "Replace existing cron entries for this project").action(async (options) => {
8681
9083
  try {
8682
9084
  const projectDir = process.cwd();
8683
9085
  const config = loadConfig(projectDir);
@@ -8686,12 +9088,12 @@ function installCommand(program2) {
8686
9088
  const nightWatchBin = getNightWatchBinPath();
8687
9089
  const projectName = getProjectName(projectDir);
8688
9090
  const marker = generateMarker(projectName);
8689
- const logDir = path23.join(projectDir, LOG_DIR);
8690
- if (!fs21.existsSync(logDir)) {
8691
- fs21.mkdirSync(logDir, { recursive: true });
9091
+ const logDir = path24.join(projectDir, LOG_DIR);
9092
+ if (!fs22.existsSync(logDir)) {
9093
+ fs22.mkdirSync(logDir, { recursive: true });
8692
9094
  }
8693
- const executorLog = path23.join(logDir, "executor.log");
8694
- const reviewerLog = path23.join(logDir, "reviewer.log");
9095
+ const executorLog = path24.join(logDir, "executor.log");
9096
+ const reviewerLog = path24.join(logDir, "reviewer.log");
8695
9097
  const existingEntries = Array.from(
8696
9098
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
8697
9099
  );
@@ -8727,7 +9129,7 @@ function installCommand(program2) {
8727
9129
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
8728
9130
  let slicerLog;
8729
9131
  if (installSlicer) {
8730
- slicerLog = path23.join(logDir, "slicer.log");
9132
+ slicerLog = path24.join(logDir, "slicer.log");
8731
9133
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
8732
9134
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8733
9135
  entries.push(slicerEntry);
@@ -8736,7 +9138,7 @@ function installCommand(program2) {
8736
9138
  const installQa = disableQa ? false : config.qa.enabled;
8737
9139
  let qaLog;
8738
9140
  if (installQa) {
8739
- qaLog = path23.join(logDir, "qa.log");
9141
+ qaLog = path24.join(logDir, "qa.log");
8740
9142
  const qaSchedule = config.qa.schedule;
8741
9143
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8742
9144
  entries.push(qaEntry);
@@ -8745,11 +9147,20 @@ function installCommand(program2) {
8745
9147
  const installAudit = disableAudit ? false : config.audit.enabled;
8746
9148
  let auditLog;
8747
9149
  if (installAudit) {
8748
- auditLog = path23.join(logDir, "audit.log");
9150
+ auditLog = path24.join(logDir, "audit.log");
8749
9151
  const auditSchedule = config.audit.schedule;
8750
9152
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8751
9153
  entries.push(auditEntry);
8752
9154
  }
9155
+ const disableAnalytics = options.noAnalytics === true || options.analytics === false;
9156
+ const installAnalytics = disableAnalytics ? false : config.analytics.enabled;
9157
+ let analyticsLog;
9158
+ if (installAnalytics) {
9159
+ analyticsLog = path24.join(logDir, "analytics.log");
9160
+ const analyticsSchedule = config.analytics.schedule;
9161
+ const analyticsEntry = `${analyticsSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} analytics >> ${shellQuote(analyticsLog)} 2>&1 ${marker}`;
9162
+ entries.push(analyticsEntry);
9163
+ }
8753
9164
  const existingEntrySet = new Set(existingEntries);
8754
9165
  const currentCrontab = readCrontab();
8755
9166
  const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
@@ -8776,6 +9187,9 @@ function installCommand(program2) {
8776
9187
  if (installAudit && auditLog) {
8777
9188
  dim(` Audit: ${auditLog}`);
8778
9189
  }
9190
+ if (installAnalytics && analyticsLog) {
9191
+ dim(` Analytics: ${analyticsLog}`);
9192
+ }
8779
9193
  console.log();
8780
9194
  dim("To uninstall, run: night-watch uninstall");
8781
9195
  dim("To check status, run: night-watch status");
@@ -8790,8 +9204,8 @@ function installCommand(program2) {
8790
9204
 
8791
9205
  // src/commands/uninstall.ts
8792
9206
  init_dist();
8793
- import * as path24 from "path";
8794
- import * as fs22 from "fs";
9207
+ import * as path25 from "path";
9208
+ import * as fs23 from "fs";
8795
9209
  function performUninstall(projectDir, options) {
8796
9210
  try {
8797
9211
  const projectName = getProjectName(projectDir);
@@ -8806,19 +9220,19 @@ function performUninstall(projectDir, options) {
8806
9220
  const removedCount = removeEntriesForProject(projectDir, marker);
8807
9221
  unregisterProject(projectDir);
8808
9222
  if (!options?.keepLogs) {
8809
- const logDir = path24.join(projectDir, "logs");
8810
- if (fs22.existsSync(logDir)) {
9223
+ const logDir = path25.join(projectDir, "logs");
9224
+ if (fs23.existsSync(logDir)) {
8811
9225
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8812
9226
  logFiles.forEach((logFile) => {
8813
- const logPath = path24.join(logDir, logFile);
8814
- if (fs22.existsSync(logPath)) {
8815
- fs22.unlinkSync(logPath);
9227
+ const logPath = path25.join(logDir, logFile);
9228
+ if (fs23.existsSync(logPath)) {
9229
+ fs23.unlinkSync(logPath);
8816
9230
  }
8817
9231
  });
8818
9232
  try {
8819
- const remainingFiles = fs22.readdirSync(logDir);
9233
+ const remainingFiles = fs23.readdirSync(logDir);
8820
9234
  if (remainingFiles.length === 0) {
8821
- fs22.rmdirSync(logDir);
9235
+ fs23.rmdirSync(logDir);
8822
9236
  }
8823
9237
  } catch {
8824
9238
  }
@@ -8851,21 +9265,21 @@ function uninstallCommand(program2) {
8851
9265
  existingEntries.forEach((entry) => dim(` ${entry}`));
8852
9266
  const removedCount = removeEntriesForProject(projectDir, marker);
8853
9267
  if (!options.keepLogs) {
8854
- const logDir = path24.join(projectDir, "logs");
8855
- if (fs22.existsSync(logDir)) {
9268
+ const logDir = path25.join(projectDir, "logs");
9269
+ if (fs23.existsSync(logDir)) {
8856
9270
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8857
9271
  let logsRemoved = 0;
8858
9272
  logFiles.forEach((logFile) => {
8859
- const logPath = path24.join(logDir, logFile);
8860
- if (fs22.existsSync(logPath)) {
8861
- fs22.unlinkSync(logPath);
9273
+ const logPath = path25.join(logDir, logFile);
9274
+ if (fs23.existsSync(logPath)) {
9275
+ fs23.unlinkSync(logPath);
8862
9276
  logsRemoved++;
8863
9277
  }
8864
9278
  });
8865
9279
  try {
8866
- const remainingFiles = fs22.readdirSync(logDir);
9280
+ const remainingFiles = fs23.readdirSync(logDir);
8867
9281
  if (remainingFiles.length === 0) {
8868
- fs22.rmdirSync(logDir);
9282
+ fs23.rmdirSync(logDir);
8869
9283
  }
8870
9284
  } catch {
8871
9285
  }
@@ -9101,14 +9515,14 @@ function statusCommand(program2) {
9101
9515
  // src/commands/logs.ts
9102
9516
  init_dist();
9103
9517
  import { spawn as spawn3 } from "child_process";
9104
- import * as path25 from "path";
9105
- import * as fs23 from "fs";
9518
+ import * as path26 from "path";
9519
+ import * as fs24 from "fs";
9106
9520
  function getLastLines(filePath, lineCount) {
9107
- if (!fs23.existsSync(filePath)) {
9521
+ if (!fs24.existsSync(filePath)) {
9108
9522
  return `Log file not found: ${filePath}`;
9109
9523
  }
9110
9524
  try {
9111
- const content = fs23.readFileSync(filePath, "utf-8");
9525
+ const content = fs24.readFileSync(filePath, "utf-8");
9112
9526
  const lines = content.trim().split("\n");
9113
9527
  return lines.slice(-lineCount).join("\n");
9114
9528
  } catch (error2) {
@@ -9116,7 +9530,7 @@ function getLastLines(filePath, lineCount) {
9116
9530
  }
9117
9531
  }
9118
9532
  function followLog(filePath) {
9119
- if (!fs23.existsSync(filePath)) {
9533
+ if (!fs24.existsSync(filePath)) {
9120
9534
  console.log(`Log file not found: ${filePath}`);
9121
9535
  console.log("The log file will be created when the first execution runs.");
9122
9536
  return;
@@ -9136,13 +9550,13 @@ function logsCommand(program2) {
9136
9550
  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) => {
9137
9551
  try {
9138
9552
  const projectDir = process.cwd();
9139
- const logDir = path25.join(projectDir, LOG_DIR);
9553
+ const logDir = path26.join(projectDir, LOG_DIR);
9140
9554
  const lineCount = parseInt(options.lines || "50", 10);
9141
- const executorLog = path25.join(logDir, EXECUTOR_LOG_FILE);
9142
- const reviewerLog = path25.join(logDir, REVIEWER_LOG_FILE);
9143
- const qaLog = path25.join(logDir, `${QA_LOG_NAME}.log`);
9144
- const auditLog = path25.join(logDir, `${AUDIT_LOG_NAME}.log`);
9145
- const plannerLog = path25.join(logDir, `${PLANNER_LOG_NAME}.log`);
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`);
9146
9560
  const logType = options.type?.toLowerCase() || "all";
9147
9561
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
9148
9562
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
@@ -9206,15 +9620,15 @@ function logsCommand(program2) {
9206
9620
 
9207
9621
  // src/commands/prd.ts
9208
9622
  init_dist();
9209
- import * as fs24 from "fs";
9210
- import * as path26 from "path";
9623
+ import * as fs25 from "fs";
9624
+ import * as path27 from "path";
9211
9625
  import * as readline2 from "readline";
9212
9626
  function slugify2(name) {
9213
9627
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9214
9628
  }
9215
9629
  function getNextPrdNumber2(prdDir) {
9216
- if (!fs24.existsSync(prdDir)) return 1;
9217
- const files = fs24.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
9630
+ if (!fs25.existsSync(prdDir)) return 1;
9631
+ const files = fs25.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
9218
9632
  const numbers = files.map((f) => {
9219
9633
  const match = f.match(/^(\d+)-/);
9220
9634
  return match ? parseInt(match[1], 10) : 0;
@@ -9235,10 +9649,10 @@ function parseDependencies(content) {
9235
9649
  }
9236
9650
  function isClaimActive(claimPath, maxRuntime) {
9237
9651
  try {
9238
- if (!fs24.existsSync(claimPath)) {
9652
+ if (!fs25.existsSync(claimPath)) {
9239
9653
  return { active: false };
9240
9654
  }
9241
- const content = fs24.readFileSync(claimPath, "utf-8");
9655
+ const content = fs25.readFileSync(claimPath, "utf-8");
9242
9656
  const claim = JSON.parse(content);
9243
9657
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
9244
9658
  if (age < maxRuntime) {
@@ -9254,9 +9668,9 @@ function prdCommand(program2) {
9254
9668
  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) => {
9255
9669
  const projectDir = process.cwd();
9256
9670
  const config = loadConfig(projectDir);
9257
- const prdDir = path26.join(projectDir, config.prdDir);
9258
- if (!fs24.existsSync(prdDir)) {
9259
- fs24.mkdirSync(prdDir, { recursive: true });
9671
+ const prdDir = path27.join(projectDir, config.prdDir);
9672
+ if (!fs25.existsSync(prdDir)) {
9673
+ fs25.mkdirSync(prdDir, { recursive: true });
9260
9674
  }
9261
9675
  let complexityScore = 5;
9262
9676
  let dependsOn = [];
@@ -9315,20 +9729,20 @@ function prdCommand(program2) {
9315
9729
  } else {
9316
9730
  filename = `${slug}.md`;
9317
9731
  }
9318
- const filePath = path26.join(prdDir, filename);
9319
- if (fs24.existsSync(filePath)) {
9732
+ const filePath = path27.join(prdDir, filename);
9733
+ if (fs25.existsSync(filePath)) {
9320
9734
  error(`File already exists: ${filePath}`);
9321
9735
  dim("Use a different name or remove the existing file.");
9322
9736
  process.exit(1);
9323
9737
  }
9324
9738
  let customTemplate;
9325
9739
  if (options.template) {
9326
- const templatePath = path26.resolve(options.template);
9327
- if (!fs24.existsSync(templatePath)) {
9740
+ const templatePath = path27.resolve(options.template);
9741
+ if (!fs25.existsSync(templatePath)) {
9328
9742
  error(`Template file not found: ${templatePath}`);
9329
9743
  process.exit(1);
9330
9744
  }
9331
- customTemplate = fs24.readFileSync(templatePath, "utf-8");
9745
+ customTemplate = fs25.readFileSync(templatePath, "utf-8");
9332
9746
  }
9333
9747
  const vars = {
9334
9748
  title: name,
@@ -9339,7 +9753,7 @@ function prdCommand(program2) {
9339
9753
  phaseCount
9340
9754
  };
9341
9755
  const content = renderPrdTemplate(vars, customTemplate);
9342
- fs24.writeFileSync(filePath, content, "utf-8");
9756
+ fs25.writeFileSync(filePath, content, "utf-8");
9343
9757
  header("PRD Created");
9344
9758
  success(`Created: ${filePath}`);
9345
9759
  info(`Title: ${name}`);
@@ -9351,15 +9765,15 @@ function prdCommand(program2) {
9351
9765
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
9352
9766
  const projectDir = process.cwd();
9353
9767
  const config = loadConfig(projectDir);
9354
- const absolutePrdDir = path26.join(projectDir, config.prdDir);
9355
- const doneDir = path26.join(absolutePrdDir, "done");
9768
+ const absolutePrdDir = path27.join(projectDir, config.prdDir);
9769
+ const doneDir = path27.join(absolutePrdDir, "done");
9356
9770
  const pending = [];
9357
- if (fs24.existsSync(absolutePrdDir)) {
9358
- const files = fs24.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
9771
+ if (fs25.existsSync(absolutePrdDir)) {
9772
+ const files = fs25.readdirSync(absolutePrdDir).filter((f) => f.endsWith(".md"));
9359
9773
  for (const file of files) {
9360
- const content = fs24.readFileSync(path26.join(absolutePrdDir, file), "utf-8");
9774
+ const content = fs25.readFileSync(path27.join(absolutePrdDir, file), "utf-8");
9361
9775
  const deps = parseDependencies(content);
9362
- const claimPath = path26.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9776
+ const claimPath = path27.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9363
9777
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
9364
9778
  pending.push({
9365
9779
  name: file,
@@ -9370,10 +9784,10 @@ function prdCommand(program2) {
9370
9784
  }
9371
9785
  }
9372
9786
  const done = [];
9373
- if (fs24.existsSync(doneDir)) {
9374
- const files = fs24.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
9787
+ if (fs25.existsSync(doneDir)) {
9788
+ const files = fs25.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
9375
9789
  for (const file of files) {
9376
- const content = fs24.readFileSync(path26.join(doneDir, file), "utf-8");
9790
+ const content = fs25.readFileSync(path27.join(doneDir, file), "utf-8");
9377
9791
  const deps = parseDependencies(content);
9378
9792
  done.push({ name: file, dependencies: deps });
9379
9793
  }
@@ -9412,7 +9826,7 @@ import blessed6 from "blessed";
9412
9826
  // src/commands/dashboard/tab-status.ts
9413
9827
  init_dist();
9414
9828
  import blessed from "blessed";
9415
- import * as fs25 from "fs";
9829
+ import * as fs26 from "fs";
9416
9830
  function sortPrdsByPriority(prds, priority) {
9417
9831
  if (priority.length === 0) return prds;
9418
9832
  const priorityMap = /* @__PURE__ */ new Map();
@@ -9508,7 +9922,7 @@ function renderLogPane(projectDir, logs) {
9508
9922
  let newestMtime = 0;
9509
9923
  for (const log of existingLogs) {
9510
9924
  try {
9511
- const stat = fs25.statSync(log.path);
9925
+ const stat = fs26.statSync(log.path);
9512
9926
  if (stat.mtimeMs > newestMtime) {
9513
9927
  newestMtime = stat.mtimeMs;
9514
9928
  newestLog = log;
@@ -11163,8 +11577,8 @@ function createActionsTab() {
11163
11577
  // src/commands/dashboard/tab-logs.ts
11164
11578
  init_dist();
11165
11579
  import blessed5 from "blessed";
11166
- import * as fs26 from "fs";
11167
- import * as path27 from "path";
11580
+ import * as fs27 from "fs";
11581
+ import * as path28 from "path";
11168
11582
  var LOG_NAMES = ["executor", "reviewer"];
11169
11583
  var LOG_LINES = 200;
11170
11584
  function createLogsTab() {
@@ -11205,7 +11619,7 @@ function createLogsTab() {
11205
11619
  let activeKeyHandlers = [];
11206
11620
  let activeCtx = null;
11207
11621
  function getLogPath(projectDir, logName) {
11208
- return path27.join(projectDir, "logs", `${logName}.log`);
11622
+ return path28.join(projectDir, "logs", `${logName}.log`);
11209
11623
  }
11210
11624
  function updateSelector() {
11211
11625
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -11219,7 +11633,7 @@ function createLogsTab() {
11219
11633
  function loadLog(ctx) {
11220
11634
  const logName = LOG_NAMES[selectedLogIndex];
11221
11635
  const logPath = getLogPath(ctx.projectDir, logName);
11222
- if (!fs26.existsSync(logPath)) {
11636
+ if (!fs27.existsSync(logPath)) {
11223
11637
  logContent.setContent(
11224
11638
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
11225
11639
 
@@ -11229,7 +11643,7 @@ Log will appear here once the ${logName} runs.`
11229
11643
  return;
11230
11644
  }
11231
11645
  try {
11232
- const stat = fs26.statSync(logPath);
11646
+ const stat = fs27.statSync(logPath);
11233
11647
  const sizeKB = (stat.size / 1024).toFixed(1);
11234
11648
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
11235
11649
  } catch {
@@ -11735,12 +12149,12 @@ function doctorCommand(program2) {
11735
12149
 
11736
12150
  // src/commands/serve.ts
11737
12151
  init_dist();
11738
- import * as fs31 from "fs";
12152
+ import * as fs32 from "fs";
11739
12153
 
11740
12154
  // ../server/dist/index.js
11741
12155
  init_dist();
11742
- import * as fs30 from "fs";
11743
- import * as path33 from "path";
12156
+ import * as fs31 from "fs";
12157
+ import * as path34 from "path";
11744
12158
  import { dirname as dirname7 } from "path";
11745
12159
  import { fileURLToPath as fileURLToPath3 } from "url";
11746
12160
  import cors from "cors";
@@ -11825,8 +12239,8 @@ function setupGracefulShutdown(server, beforeClose) {
11825
12239
 
11826
12240
  // ../server/dist/middleware/project-resolver.middleware.js
11827
12241
  init_dist();
11828
- import * as fs27 from "fs";
11829
- import * as path28 from "path";
12242
+ import * as fs28 from "fs";
12243
+ import * as path29 from "path";
11830
12244
  function resolveProject(req, res, next) {
11831
12245
  const projectId = req.params.projectId;
11832
12246
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -11836,7 +12250,7 @@ function resolveProject(req, res, next) {
11836
12250
  res.status(404).json({ error: `Project not found: ${decodedId}` });
11837
12251
  return;
11838
12252
  }
11839
- if (!fs27.existsSync(entry.path) || !fs27.existsSync(path28.join(entry.path, CONFIG_FILE_NAME))) {
12253
+ if (!fs28.existsSync(entry.path) || !fs28.existsSync(path29.join(entry.path, CONFIG_FILE_NAME))) {
11840
12254
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
11841
12255
  return;
11842
12256
  }
@@ -11881,8 +12295,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
11881
12295
 
11882
12296
  // ../server/dist/routes/action.routes.js
11883
12297
  init_dist();
11884
- import * as fs28 from "fs";
11885
- import * as path29 from "path";
12298
+ import * as fs29 from "fs";
12299
+ import * as path30 from "path";
11886
12300
  import { execSync as execSync5, spawn as spawn5 } from "child_process";
11887
12301
  import { Router } from "express";
11888
12302
 
@@ -11920,17 +12334,17 @@ function getBoardProvider(config, projectDir) {
11920
12334
  function cleanOrphanedClaims(dir) {
11921
12335
  let entries;
11922
12336
  try {
11923
- entries = fs28.readdirSync(dir, { withFileTypes: true });
12337
+ entries = fs29.readdirSync(dir, { withFileTypes: true });
11924
12338
  } catch {
11925
12339
  return;
11926
12340
  }
11927
12341
  for (const entry of entries) {
11928
- const fullPath = path29.join(dir, entry.name);
12342
+ const fullPath = path30.join(dir, entry.name);
11929
12343
  if (entry.isDirectory() && entry.name !== "done") {
11930
12344
  cleanOrphanedClaims(fullPath);
11931
12345
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
11932
12346
  try {
11933
- fs28.unlinkSync(fullPath);
12347
+ fs29.unlinkSync(fullPath);
11934
12348
  } catch {
11935
12349
  }
11936
12350
  }
@@ -12027,6 +12441,9 @@ function createActionRouteHandlers(ctx) {
12027
12441
  router.post(`/${p}audit`, (req, res) => {
12028
12442
  spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
12029
12443
  });
12444
+ router.post(`/${p}analytics`, (req, res) => {
12445
+ spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
12446
+ });
12030
12447
  router.post(`/${p}planner`, (req, res) => {
12031
12448
  spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12032
12449
  });
@@ -12084,19 +12501,19 @@ function createActionRouteHandlers(ctx) {
12084
12501
  res.status(400).json({ error: "Invalid PRD name" });
12085
12502
  return;
12086
12503
  }
12087
- const prdDir = path29.join(projectDir, config.prdDir);
12504
+ const prdDir = path30.join(projectDir, config.prdDir);
12088
12505
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
12089
- const pendingPath = path29.join(prdDir, normalized);
12090
- const donePath = path29.join(prdDir, "done", normalized);
12091
- if (fs28.existsSync(pendingPath)) {
12506
+ const pendingPath = path30.join(prdDir, normalized);
12507
+ const donePath = path30.join(prdDir, "done", normalized);
12508
+ if (fs29.existsSync(pendingPath)) {
12092
12509
  res.json({ message: `"${normalized}" is already pending` });
12093
12510
  return;
12094
12511
  }
12095
- if (!fs28.existsSync(donePath)) {
12512
+ if (!fs29.existsSync(donePath)) {
12096
12513
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
12097
12514
  return;
12098
12515
  }
12099
- fs28.renameSync(donePath, pendingPath);
12516
+ fs29.renameSync(donePath, pendingPath);
12100
12517
  res.json({ message: `Moved "${normalized}" back to pending` });
12101
12518
  } catch (error2) {
12102
12519
  res.status(500).json({
@@ -12114,11 +12531,11 @@ function createActionRouteHandlers(ctx) {
12114
12531
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
12115
12532
  return;
12116
12533
  }
12117
- if (fs28.existsSync(lockPath)) {
12118
- fs28.unlinkSync(lockPath);
12534
+ if (fs29.existsSync(lockPath)) {
12535
+ fs29.unlinkSync(lockPath);
12119
12536
  }
12120
- const prdDir = path29.join(projectDir, config.prdDir);
12121
- if (fs28.existsSync(prdDir)) {
12537
+ const prdDir = path30.join(projectDir, config.prdDir);
12538
+ if (fs29.existsSync(prdDir)) {
12122
12539
  cleanOrphanedClaims(prdDir);
12123
12540
  }
12124
12541
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -12314,6 +12731,7 @@ function createBoardRouteHandlers(ctx) {
12314
12731
  return;
12315
12732
  }
12316
12733
  await provider.closeIssue(issueNumber);
12734
+ await provider.moveIssue(issueNumber, "Done");
12317
12735
  invalidateBoardCache(projectDir);
12318
12736
  res.json({ closed: true });
12319
12737
  } catch (error2) {
@@ -12408,6 +12826,9 @@ function validateConfigChanges(changes, currentConfig) {
12408
12826
  if (changes.reviewerRetryDelay !== void 0 && (typeof changes.reviewerRetryDelay !== "number" || !Number.isInteger(changes.reviewerRetryDelay) || changes.reviewerRetryDelay < 0 || changes.reviewerRetryDelay > 300)) {
12409
12827
  return "reviewerRetryDelay must be an integer between 0 and 300";
12410
12828
  }
12829
+ if (changes.reviewerMaxPrsPerRun !== void 0 && (typeof changes.reviewerMaxPrsPerRun !== "number" || !Number.isInteger(changes.reviewerMaxPrsPerRun) || changes.reviewerMaxPrsPerRun < 0 || changes.reviewerMaxPrsPerRun > 100)) {
12830
+ return "reviewerMaxPrsPerRun must be an integer between 0 and 100";
12831
+ }
12411
12832
  if (changes.branchPatterns !== void 0 && (!Array.isArray(changes.branchPatterns) || !changes.branchPatterns.every((p) => typeof p === "string"))) {
12412
12833
  return "branchPatterns must be an array of strings";
12413
12834
  }
@@ -12617,6 +13038,34 @@ function validateConfigChanges(changes, currentConfig) {
12617
13038
  return "audit.maxRuntime must be a number >= 60";
12618
13039
  }
12619
13040
  }
13041
+ if (changes.analytics !== void 0) {
13042
+ if (typeof changes.analytics !== "object" || changes.analytics === null) {
13043
+ return "analytics must be an object";
13044
+ }
13045
+ const analytics = changes.analytics;
13046
+ if (analytics.enabled !== void 0 && typeof analytics.enabled !== "boolean") {
13047
+ return "analytics.enabled must be a boolean";
13048
+ }
13049
+ const analyticsScheduleError = validateCronField("analytics.schedule", analytics.schedule);
13050
+ if (analyticsScheduleError) {
13051
+ return analyticsScheduleError;
13052
+ }
13053
+ if (analytics.maxRuntime !== void 0 && (typeof analytics.maxRuntime !== "number" || analytics.maxRuntime < 60)) {
13054
+ return "analytics.maxRuntime must be a number >= 60";
13055
+ }
13056
+ if (analytics.lookbackDays !== void 0 && (typeof analytics.lookbackDays !== "number" || analytics.lookbackDays < 1 || analytics.lookbackDays > 90)) {
13057
+ return "analytics.lookbackDays must be a number between 1 and 90";
13058
+ }
13059
+ if (analytics.targetColumn !== void 0) {
13060
+ const validColumns = ["Draft", "Ready", "In Progress", "Review", "Done"];
13061
+ if (!validColumns.includes(analytics.targetColumn)) {
13062
+ return `analytics.targetColumn must be one of: ${validColumns.join(", ")}`;
13063
+ }
13064
+ }
13065
+ if (analytics.analysisPrompt !== void 0 && typeof analytics.analysisPrompt !== "string") {
13066
+ return "analytics.analysisPrompt must be a string";
13067
+ }
13068
+ }
12620
13069
  if (changes.queue !== void 0) {
12621
13070
  if (typeof changes.queue !== "object" || changes.queue === null) {
12622
13071
  return "queue must be an object";
@@ -12635,7 +13084,14 @@ function validateConfigChanges(changes, currentConfig) {
12635
13084
  if (typeof queue.priority !== "object" || queue.priority === null) {
12636
13085
  return "queue.priority must be an object";
12637
13086
  }
12638
- const validQueueJobs = ["executor", "reviewer", "qa", "audit", "slicer"];
13087
+ const validQueueJobs = [
13088
+ "executor",
13089
+ "reviewer",
13090
+ "qa",
13091
+ "audit",
13092
+ "slicer",
13093
+ "analytics"
13094
+ ];
12639
13095
  for (const [jobType, value] of Object.entries(queue.priority)) {
12640
13096
  if (!validQueueJobs.includes(jobType)) {
12641
13097
  return `queue.priority contains invalid job type: ${jobType}`;
@@ -12771,8 +13227,8 @@ function createProjectConfigRoutes() {
12771
13227
 
12772
13228
  // ../server/dist/routes/doctor.routes.js
12773
13229
  init_dist();
12774
- import * as fs29 from "fs";
12775
- import * as path30 from "path";
13230
+ import * as fs30 from "fs";
13231
+ import * as path31 from "path";
12776
13232
  import { execSync as execSync6 } from "child_process";
12777
13233
  import { Router as Router4 } from "express";
12778
13234
  function runDoctorChecks(projectDir, config) {
@@ -12805,7 +13261,7 @@ function runDoctorChecks(projectDir, config) {
12805
13261
  });
12806
13262
  }
12807
13263
  try {
12808
- const projectName = path30.basename(projectDir);
13264
+ const projectName = path31.basename(projectDir);
12809
13265
  const marker = generateMarker(projectName);
12810
13266
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
12811
13267
  if (crontabEntries.length > 0) {
@@ -12828,8 +13284,8 @@ function runDoctorChecks(projectDir, config) {
12828
13284
  detail: "Failed to check crontab"
12829
13285
  });
12830
13286
  }
12831
- const configPath = path30.join(projectDir, CONFIG_FILE_NAME);
12832
- if (fs29.existsSync(configPath)) {
13287
+ const configPath = path31.join(projectDir, CONFIG_FILE_NAME);
13288
+ if (fs30.existsSync(configPath)) {
12833
13289
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
12834
13290
  } else {
12835
13291
  checks.push({
@@ -12838,9 +13294,9 @@ function runDoctorChecks(projectDir, config) {
12838
13294
  detail: "Config file not found (using defaults)"
12839
13295
  });
12840
13296
  }
12841
- const prdDir = path30.join(projectDir, config.prdDir);
12842
- if (fs29.existsSync(prdDir)) {
12843
- const prds = fs29.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
13297
+ const prdDir = path31.join(projectDir, config.prdDir);
13298
+ if (fs30.existsSync(prdDir)) {
13299
+ const prds = fs30.readdirSync(prdDir).filter((f) => f.endsWith(".md"));
12844
13300
  checks.push({
12845
13301
  name: "prdDir",
12846
13302
  status: "pass",
@@ -12883,7 +13339,7 @@ function createProjectDoctorRoutes() {
12883
13339
 
12884
13340
  // ../server/dist/routes/log.routes.js
12885
13341
  init_dist();
12886
- import * as path31 from "path";
13342
+ import * as path32 from "path";
12887
13343
  import { Router as Router5 } from "express";
12888
13344
  function createLogRoutes(deps) {
12889
13345
  const { projectDir } = deps;
@@ -12891,7 +13347,7 @@ function createLogRoutes(deps) {
12891
13347
  router.get("/:name", (req, res) => {
12892
13348
  try {
12893
13349
  const { name } = req.params;
12894
- const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
13350
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
12895
13351
  if (!validNames.includes(name)) {
12896
13352
  res.status(400).json({
12897
13353
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -12902,7 +13358,7 @@ function createLogRoutes(deps) {
12902
13358
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12903
13359
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12904
13360
  const fileName = LOG_FILE_NAMES[name] || name;
12905
- const logPath = path31.join(projectDir, LOG_DIR, `${fileName}.log`);
13361
+ const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
12906
13362
  const logLines = getLastLogLines(logPath, linesToRead);
12907
13363
  res.json({ name, lines: logLines });
12908
13364
  } catch (error2) {
@@ -12917,7 +13373,7 @@ function createProjectLogRoutes() {
12917
13373
  try {
12918
13374
  const projectDir = req.projectDir;
12919
13375
  const { name } = req.params;
12920
- const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
13376
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
12921
13377
  if (!validNames.includes(name)) {
12922
13378
  res.status(400).json({
12923
13379
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -12928,7 +13384,7 @@ function createProjectLogRoutes() {
12928
13384
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12929
13385
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12930
13386
  const fileName = LOG_FILE_NAMES[name] || name;
12931
- const logPath = path31.join(projectDir, LOG_DIR, `${fileName}.log`);
13387
+ const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
12932
13388
  const logLines = getLastLogLines(logPath, linesToRead);
12933
13389
  res.json({ name, lines: logLines });
12934
13390
  } catch (error2) {
@@ -12963,7 +13419,7 @@ function createProjectPrdRoutes() {
12963
13419
 
12964
13420
  // ../server/dist/routes/roadmap.routes.js
12965
13421
  init_dist();
12966
- import * as path32 from "path";
13422
+ import * as path33 from "path";
12967
13423
  import { Router as Router7 } from "express";
12968
13424
  function createRoadmapRouteHandlers(ctx) {
12969
13425
  const router = Router7({ mergeParams: true });
@@ -12973,7 +13429,7 @@ function createRoadmapRouteHandlers(ctx) {
12973
13429
  const config = ctx.getConfig(req);
12974
13430
  const projectDir = ctx.getProjectDir(req);
12975
13431
  const status = getRoadmapStatus(projectDir, config);
12976
- const prdDir = path32.join(projectDir, config.prdDir);
13432
+ const prdDir = path33.join(projectDir, config.prdDir);
12977
13433
  const state = loadRoadmapState(prdDir);
12978
13434
  res.json({
12979
13435
  ...status,
@@ -13097,11 +13553,13 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
13097
13553
  const qaPlan = getSchedulingPlan(projectDir, config, "qa");
13098
13554
  const auditPlan = getSchedulingPlan(projectDir, config, "audit");
13099
13555
  const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
13556
+ const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
13100
13557
  const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
13101
13558
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
13102
13559
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
13103
13560
  const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
13104
13561
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
13562
+ const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
13105
13563
  return {
13106
13564
  executor: {
13107
13565
  schedule: config.cronSchedule,
@@ -13143,6 +13601,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
13143
13601
  manualDelayMinutes: plannerPlan.manualDelayMinutes,
13144
13602
  balancedDelayMinutes: plannerPlan.balancedDelayMinutes
13145
13603
  },
13604
+ analytics: {
13605
+ schedule: config.analytics.schedule,
13606
+ installed: analyticsInstalled,
13607
+ nextRun: analyticsInstalled ? addDelayToIsoString(computeNextRun(config.analytics.schedule), analyticsPlan.totalDelayMinutes) : null,
13608
+ delayMinutes: analyticsPlan.totalDelayMinutes,
13609
+ manualDelayMinutes: analyticsPlan.manualDelayMinutes,
13610
+ balancedDelayMinutes: analyticsPlan.balancedDelayMinutes
13611
+ },
13146
13612
  paused: !installed,
13147
13613
  schedulingPriority: config.schedulingPriority,
13148
13614
  entries
@@ -13297,14 +13763,14 @@ function createQueueRoutes(deps) {
13297
13763
  var __filename2 = fileURLToPath3(import.meta.url);
13298
13764
  var __dirname3 = dirname7(__filename2);
13299
13765
  function resolveWebDistPath() {
13300
- const bundled = path33.join(__dirname3, "web");
13301
- if (fs30.existsSync(path33.join(bundled, "index.html")))
13766
+ const bundled = path34.join(__dirname3, "web");
13767
+ if (fs31.existsSync(path34.join(bundled, "index.html")))
13302
13768
  return bundled;
13303
13769
  let d = __dirname3;
13304
13770
  for (let i = 0; i < 8; i++) {
13305
- if (fs30.existsSync(path33.join(d, "turbo.json"))) {
13306
- const dev = path33.join(d, "web/dist");
13307
- if (fs30.existsSync(path33.join(dev, "index.html")))
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")))
13308
13774
  return dev;
13309
13775
  break;
13310
13776
  }
@@ -13314,7 +13780,7 @@ function resolveWebDistPath() {
13314
13780
  }
13315
13781
  function setupStaticFiles(app) {
13316
13782
  const webDistPath = resolveWebDistPath();
13317
- if (fs30.existsSync(webDistPath)) {
13783
+ if (fs31.existsSync(webDistPath)) {
13318
13784
  app.use(express.static(webDistPath));
13319
13785
  }
13320
13786
  app.use((req, res, next) => {
@@ -13322,8 +13788,8 @@ function setupStaticFiles(app) {
13322
13788
  next();
13323
13789
  return;
13324
13790
  }
13325
- const indexPath = path33.resolve(webDistPath, "index.html");
13326
- if (fs30.existsSync(indexPath)) {
13791
+ const indexPath = path34.resolve(webDistPath, "index.html");
13792
+ if (fs31.existsSync(indexPath)) {
13327
13793
  res.sendFile(indexPath, (err) => {
13328
13794
  if (err)
13329
13795
  next();
@@ -13439,7 +13905,7 @@ function createGlobalApp() {
13439
13905
  return app;
13440
13906
  }
13441
13907
  function bootContainer() {
13442
- initContainer(path33.dirname(getDbPath()));
13908
+ initContainer(path34.dirname(getDbPath()));
13443
13909
  }
13444
13910
  function startServer(projectDir, port) {
13445
13911
  bootContainer();
@@ -13492,8 +13958,8 @@ function isProcessRunning2(pid) {
13492
13958
  }
13493
13959
  function readPid(lockPath) {
13494
13960
  try {
13495
- if (!fs31.existsSync(lockPath)) return null;
13496
- const raw = fs31.readFileSync(lockPath, "utf-8").trim();
13961
+ if (!fs32.existsSync(lockPath)) return null;
13962
+ const raw = fs32.readFileSync(lockPath, "utf-8").trim();
13497
13963
  const pid = parseInt(raw, 10);
13498
13964
  return Number.isFinite(pid) ? pid : null;
13499
13965
  } catch {
@@ -13505,10 +13971,10 @@ function acquireServeLock(mode, port) {
13505
13971
  let stalePidCleaned;
13506
13972
  for (let attempt = 0; attempt < 2; attempt++) {
13507
13973
  try {
13508
- const fd = fs31.openSync(lockPath, "wx");
13509
- fs31.writeFileSync(fd, `${process.pid}
13974
+ const fd = fs32.openSync(lockPath, "wx");
13975
+ fs32.writeFileSync(fd, `${process.pid}
13510
13976
  `);
13511
- fs31.closeSync(fd);
13977
+ fs32.closeSync(fd);
13512
13978
  return { acquired: true, lockPath, stalePidCleaned };
13513
13979
  } catch (error2) {
13514
13980
  const err = error2;
@@ -13529,7 +13995,7 @@ function acquireServeLock(mode, port) {
13529
13995
  };
13530
13996
  }
13531
13997
  try {
13532
- fs31.unlinkSync(lockPath);
13998
+ fs32.unlinkSync(lockPath);
13533
13999
  if (existingPid) {
13534
14000
  stalePidCleaned = existingPid;
13535
14001
  }
@@ -13552,10 +14018,10 @@ function acquireServeLock(mode, port) {
13552
14018
  }
13553
14019
  function releaseServeLock(lockPath) {
13554
14020
  try {
13555
- if (!fs31.existsSync(lockPath)) return;
14021
+ if (!fs32.existsSync(lockPath)) return;
13556
14022
  const lockPid = readPid(lockPath);
13557
14023
  if (lockPid !== null && lockPid !== process.pid) return;
13558
- fs31.unlinkSync(lockPath);
14024
+ fs32.unlinkSync(lockPath);
13559
14025
  } catch {
13560
14026
  }
13561
14027
  }
@@ -13651,14 +14117,14 @@ function historyCommand(program2) {
13651
14117
  // src/commands/update.ts
13652
14118
  init_dist();
13653
14119
  import { spawnSync } from "child_process";
13654
- import * as fs32 from "fs";
13655
- import * as path34 from "path";
14120
+ import * as fs33 from "fs";
14121
+ import * as path35 from "path";
13656
14122
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
13657
14123
  function parseProjectDirs(projects, cwd) {
13658
14124
  if (!projects || projects.trim().length === 0) {
13659
14125
  return [cwd];
13660
14126
  }
13661
- const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path34.resolve(cwd, entry));
14127
+ const dirs = projects.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0).map((entry) => path35.resolve(cwd, entry));
13662
14128
  return Array.from(new Set(dirs));
13663
14129
  }
13664
14130
  function shouldInstallGlobal(options) {
@@ -13700,7 +14166,7 @@ function updateCommand(program2) {
13700
14166
  }
13701
14167
  const nightWatchBin = resolveNightWatchBin();
13702
14168
  for (const projectDir of projectDirs) {
13703
- if (!fs32.existsSync(projectDir) || !fs32.statSync(projectDir).isDirectory()) {
14169
+ if (!fs33.existsSync(projectDir) || !fs33.statSync(projectDir).isDirectory()) {
13704
14170
  warn(`Skipping invalid project directory: ${projectDir}`);
13705
14171
  continue;
13706
14172
  }
@@ -13744,8 +14210,8 @@ function prdStateCommand(program2) {
13744
14210
 
13745
14211
  // src/commands/retry.ts
13746
14212
  init_dist();
13747
- import * as fs33 from "fs";
13748
- import * as path35 from "path";
14213
+ import * as fs34 from "fs";
14214
+ import * as path36 from "path";
13749
14215
  function normalizePrdName(name) {
13750
14216
  if (!name.endsWith(".md")) {
13751
14217
  return `${name}.md`;
@@ -13753,26 +14219,26 @@ function normalizePrdName(name) {
13753
14219
  return name;
13754
14220
  }
13755
14221
  function getDonePrds(doneDir) {
13756
- if (!fs33.existsSync(doneDir)) {
14222
+ if (!fs34.existsSync(doneDir)) {
13757
14223
  return [];
13758
14224
  }
13759
- return fs33.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
14225
+ return fs34.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
13760
14226
  }
13761
14227
  function retryCommand(program2) {
13762
14228
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
13763
14229
  const projectDir = process.cwd();
13764
14230
  const config = loadConfig(projectDir);
13765
- const prdDir = path35.join(projectDir, config.prdDir);
13766
- const doneDir = path35.join(prdDir, "done");
14231
+ const prdDir = path36.join(projectDir, config.prdDir);
14232
+ const doneDir = path36.join(prdDir, "done");
13767
14233
  const normalizedPrdName = normalizePrdName(prdName);
13768
- const pendingPath = path35.join(prdDir, normalizedPrdName);
13769
- if (fs33.existsSync(pendingPath)) {
14234
+ const pendingPath = path36.join(prdDir, normalizedPrdName);
14235
+ if (fs34.existsSync(pendingPath)) {
13770
14236
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
13771
14237
  return;
13772
14238
  }
13773
- const donePath = path35.join(doneDir, normalizedPrdName);
13774
- if (fs33.existsSync(donePath)) {
13775
- fs33.renameSync(donePath, pendingPath);
14239
+ const donePath = path36.join(doneDir, normalizedPrdName);
14240
+ if (fs34.existsSync(donePath)) {
14241
+ fs34.renameSync(donePath, pendingPath);
13776
14242
  success(`Moved "${normalizedPrdName}" back to pending.`);
13777
14243
  dim(`From: ${donePath}`);
13778
14244
  dim(`To: ${pendingPath}`);
@@ -14024,7 +14490,7 @@ function prdsCommand(program2) {
14024
14490
 
14025
14491
  // src/commands/cancel.ts
14026
14492
  init_dist();
14027
- import * as fs34 from "fs";
14493
+ import * as fs35 from "fs";
14028
14494
  import * as readline3 from "readline";
14029
14495
  function getLockFilePaths2(projectDir) {
14030
14496
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -14071,7 +14537,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14071
14537
  const pid = lockStatus.pid;
14072
14538
  if (!lockStatus.running) {
14073
14539
  try {
14074
- fs34.unlinkSync(lockPath);
14540
+ fs35.unlinkSync(lockPath);
14075
14541
  return {
14076
14542
  success: true,
14077
14543
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -14109,7 +14575,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14109
14575
  await sleep2(3e3);
14110
14576
  if (!isProcessRunning3(pid)) {
14111
14577
  try {
14112
- fs34.unlinkSync(lockPath);
14578
+ fs35.unlinkSync(lockPath);
14113
14579
  } catch {
14114
14580
  }
14115
14581
  return {
@@ -14144,7 +14610,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14144
14610
  await sleep2(500);
14145
14611
  if (!isProcessRunning3(pid)) {
14146
14612
  try {
14147
- fs34.unlinkSync(lockPath);
14613
+ fs35.unlinkSync(lockPath);
14148
14614
  } catch {
14149
14615
  }
14150
14616
  return {
@@ -14205,31 +14671,31 @@ function cancelCommand(program2) {
14205
14671
 
14206
14672
  // src/commands/slice.ts
14207
14673
  init_dist();
14208
- import * as fs35 from "fs";
14209
- import * as path36 from "path";
14674
+ import * as fs36 from "fs";
14675
+ import * as path37 from "path";
14210
14676
  function plannerLockPath2(projectDir) {
14211
14677
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
14212
14678
  }
14213
14679
  function acquirePlannerLock(projectDir) {
14214
14680
  const lockFile = plannerLockPath2(projectDir);
14215
- if (fs35.existsSync(lockFile)) {
14216
- const pidRaw = fs35.readFileSync(lockFile, "utf-8").trim();
14681
+ if (fs36.existsSync(lockFile)) {
14682
+ const pidRaw = fs36.readFileSync(lockFile, "utf-8").trim();
14217
14683
  const pid = parseInt(pidRaw, 10);
14218
14684
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
14219
14685
  return { acquired: false, lockFile, pid };
14220
14686
  }
14221
14687
  try {
14222
- fs35.unlinkSync(lockFile);
14688
+ fs36.unlinkSync(lockFile);
14223
14689
  } catch {
14224
14690
  }
14225
14691
  }
14226
- fs35.writeFileSync(lockFile, String(process.pid));
14692
+ fs36.writeFileSync(lockFile, String(process.pid));
14227
14693
  return { acquired: true, lockFile };
14228
14694
  }
14229
14695
  function releasePlannerLock(lockFile) {
14230
14696
  try {
14231
- if (fs35.existsSync(lockFile)) {
14232
- fs35.unlinkSync(lockFile);
14697
+ if (fs36.existsSync(lockFile)) {
14698
+ fs36.unlinkSync(lockFile);
14233
14699
  }
14234
14700
  } catch {
14235
14701
  }
@@ -14238,12 +14704,12 @@ function resolvePlannerIssueColumn(config) {
14238
14704
  return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
14239
14705
  }
14240
14706
  function buildPlannerIssueBody(projectDir, config, result) {
14241
- const relativePrdPath = path36.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
14242
- const absolutePrdPath = path36.join(projectDir, config.prdDir, result.file ?? "");
14707
+ const relativePrdPath = path37.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
14708
+ const absolutePrdPath = path37.join(projectDir, config.prdDir, result.file ?? "");
14243
14709
  const sourceItem = result.item;
14244
14710
  let prdContent;
14245
14711
  try {
14246
- prdContent = fs35.readFileSync(absolutePrdPath, "utf-8");
14712
+ prdContent = fs36.readFileSync(absolutePrdPath, "utf-8");
14247
14713
  } catch {
14248
14714
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
14249
14715
  }
@@ -14419,7 +14885,7 @@ function sliceCommand(program2) {
14419
14885
  if (!options.dryRun) {
14420
14886
  await sendNotifications(config, {
14421
14887
  event: "run_started",
14422
- projectName: path36.basename(projectDir),
14888
+ projectName: path37.basename(projectDir),
14423
14889
  exitCode: 0,
14424
14890
  provider: config.provider
14425
14891
  });
@@ -14454,7 +14920,7 @@ function sliceCommand(program2) {
14454
14920
  if (!options.dryRun && result.sliced) {
14455
14921
  await sendNotifications(config, {
14456
14922
  event: "run_succeeded",
14457
- projectName: path36.basename(projectDir),
14923
+ projectName: path37.basename(projectDir),
14458
14924
  exitCode,
14459
14925
  provider: config.provider,
14460
14926
  prTitle: result.item?.title
@@ -14462,7 +14928,7 @@ function sliceCommand(program2) {
14462
14928
  } else if (!options.dryRun && !nothingPending) {
14463
14929
  await sendNotifications(config, {
14464
14930
  event: "run_failed",
14465
- projectName: path36.basename(projectDir),
14931
+ projectName: path37.basename(projectDir),
14466
14932
  exitCode,
14467
14933
  provider: config.provider
14468
14934
  });
@@ -14478,21 +14944,21 @@ function sliceCommand(program2) {
14478
14944
 
14479
14945
  // src/commands/state.ts
14480
14946
  init_dist();
14481
- import * as os7 from "os";
14482
- import * as path37 from "path";
14947
+ import * as os8 from "os";
14948
+ import * as path38 from "path";
14483
14949
  import chalk5 from "chalk";
14484
14950
  import { Command } from "commander";
14485
14951
  function createStateCommand() {
14486
14952
  const state = new Command("state");
14487
14953
  state.description("Manage Night Watch state");
14488
14954
  state.command("migrate").description("Migrate legacy JSON state files to SQLite").option("--dry-run", "Show what would be migrated without making changes").action((opts) => {
14489
- const nightWatchHome = process.env.NIGHT_WATCH_HOME || path37.join(os7.homedir(), GLOBAL_CONFIG_DIR);
14955
+ const nightWatchHome = process.env.NIGHT_WATCH_HOME || path38.join(os8.homedir(), GLOBAL_CONFIG_DIR);
14490
14956
  if (opts.dryRun) {
14491
14957
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
14492
14958
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
14493
- console.log(` ${path37.join(nightWatchHome, "projects.json")}`);
14494
- console.log(` ${path37.join(nightWatchHome, "history.json")}`);
14495
- console.log(` ${path37.join(nightWatchHome, "prd-states.json")}`);
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")}`);
14496
14962
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
14497
14963
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
14498
14964
  return;
@@ -14530,8 +14996,8 @@ function createStateCommand() {
14530
14996
  init_dist();
14531
14997
  init_dist();
14532
14998
  import { execFileSync as execFileSync6 } from "child_process";
14533
- import * as fs36 from "fs";
14534
- import * as path38 from "path";
14999
+ import * as fs37 from "fs";
15000
+ import * as path39 from "path";
14535
15001
  import * as readline4 from "readline";
14536
15002
  import chalk6 from "chalk";
14537
15003
  async function run(fn) {
@@ -14553,7 +15019,7 @@ function getProvider(config, cwd) {
14553
15019
  return createBoardProvider(bp, cwd);
14554
15020
  }
14555
15021
  function defaultBoardTitle(cwd) {
14556
- return `${path38.basename(cwd)} Night Watch`;
15022
+ return `${path39.basename(cwd)} Night Watch`;
14557
15023
  }
14558
15024
  async function ensureBoardConfigured(config, cwd, provider, options) {
14559
15025
  if (config.boardProvider?.projectNumber) {
@@ -14752,11 +15218,11 @@ function boardCommand(program2) {
14752
15218
  let body = options.body ?? "";
14753
15219
  if (options.bodyFile) {
14754
15220
  const filePath = options.bodyFile;
14755
- if (!fs36.existsSync(filePath)) {
15221
+ if (!fs37.existsSync(filePath)) {
14756
15222
  console.error(`File not found: ${filePath}`);
14757
15223
  process.exit(1);
14758
15224
  }
14759
- body = fs36.readFileSync(filePath, "utf-8");
15225
+ body = fs37.readFileSync(filePath, "utf-8");
14760
15226
  }
14761
15227
  const labels = [];
14762
15228
  if (options.label) {
@@ -14978,12 +15444,12 @@ function boardCommand(program2) {
14978
15444
  const config = loadConfig(cwd);
14979
15445
  const provider = getProvider(config, cwd);
14980
15446
  await ensureBoardConfigured(config, cwd, provider);
14981
- const roadmapPath = options.roadmap ?? path38.join(cwd, "ROADMAP.md");
14982
- if (!fs36.existsSync(roadmapPath)) {
15447
+ const roadmapPath = options.roadmap ?? path39.join(cwd, "ROADMAP.md");
15448
+ if (!fs37.existsSync(roadmapPath)) {
14983
15449
  console.error(`Roadmap file not found: ${roadmapPath}`);
14984
15450
  process.exit(1);
14985
15451
  }
14986
- const roadmapContent = fs36.readFileSync(roadmapPath, "utf-8");
15452
+ const roadmapContent = fs37.readFileSync(roadmapPath, "utf-8");
14987
15453
  const items = parseRoadmap(roadmapContent);
14988
15454
  const uncheckedItems = getUncheckedItems(items);
14989
15455
  if (uncheckedItems.length === 0) {
@@ -15107,11 +15573,11 @@ function boardCommand(program2) {
15107
15573
  // src/commands/queue.ts
15108
15574
  init_dist();
15109
15575
  init_dist();
15110
- import * as path39 from "path";
15576
+ import * as path40 from "path";
15111
15577
  import { spawn as spawn6 } from "child_process";
15112
15578
  import chalk7 from "chalk";
15113
15579
  import { Command as Command2 } from "commander";
15114
- var logger2 = createLogger("queue");
15580
+ var logger4 = createLogger("queue");
15115
15581
  var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer"];
15116
15582
  function formatTimestamp(unixTs) {
15117
15583
  if (unixTs === null) return "-";
@@ -15227,7 +15693,7 @@ function createQueueCommand() {
15227
15693
  process.exit(1);
15228
15694
  }
15229
15695
  }
15230
- const projectName = path39.basename(projectDir);
15696
+ const projectName = path40.basename(projectDir);
15231
15697
  const queueConfig = loadConfig(projectDir).queue;
15232
15698
  const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
15233
15699
  console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
@@ -15235,13 +15701,13 @@ function createQueueCommand() {
15235
15701
  queue.command("dispatch").description("Dispatch the next pending job (used by cron scripts)").option("--log <file>", "Log file to write dispatch output").action((_opts) => {
15236
15702
  const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
15237
15703
  if (!entry) {
15238
- logger2.info("No pending jobs to dispatch");
15704
+ logger4.info("No pending jobs to dispatch");
15239
15705
  return;
15240
15706
  }
15241
- logger2.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
15707
+ logger4.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
15242
15708
  const scriptName = getScriptNameForJobType(entry.jobType);
15243
15709
  if (!scriptName) {
15244
- logger2.error(`Unknown job type: ${entry.jobType}`);
15710
+ logger4.error(`Unknown job type: ${entry.jobType}`);
15245
15711
  return;
15246
15712
  }
15247
15713
  let projectEnv;
@@ -15260,7 +15726,7 @@ function createQueueCommand() {
15260
15726
  NW_QUEUE_ENTRY_ID: String(entry.id)
15261
15727
  };
15262
15728
  const scriptPath = getScriptPath(scriptName);
15263
- logger2.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15729
+ logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15264
15730
  try {
15265
15731
  const child = spawn6("bash", [scriptPath, entry.projectPath], {
15266
15732
  detached: true,
@@ -15269,11 +15735,11 @@ function createQueueCommand() {
15269
15735
  cwd: entry.projectPath
15270
15736
  });
15271
15737
  child.unref();
15272
- logger2.info(`Spawned PID: ${child.pid}`);
15738
+ logger4.info(`Spawned PID: ${child.pid}`);
15273
15739
  markJobRunning(entry.id);
15274
15740
  } catch (error2) {
15275
15741
  updateJobStatus(entry.id, "pending");
15276
- logger2.error(
15742
+ logger4.error(
15277
15743
  `Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
15278
15744
  );
15279
15745
  process.exit(1);
@@ -15387,13 +15853,13 @@ var __dirname4 = dirname8(__filename3);
15387
15853
  function findPackageRoot(dir) {
15388
15854
  let d = dir;
15389
15855
  for (let i = 0; i < 5; i++) {
15390
- if (existsSync29(join34(d, "package.json"))) return d;
15856
+ if (existsSync29(join35(d, "package.json"))) return d;
15391
15857
  d = dirname8(d);
15392
15858
  }
15393
15859
  return dir;
15394
15860
  }
15395
15861
  var packageRoot = findPackageRoot(__dirname4);
15396
- var packageJson = JSON.parse(readFileSync18(join34(packageRoot, "package.json"), "utf-8"));
15862
+ var packageJson = JSON.parse(readFileSync18(join35(packageRoot, "package.json"), "utf-8"));
15397
15863
  var program = new Command3();
15398
15864
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
15399
15865
  initCommand(program);
@@ -15401,6 +15867,7 @@ runCommand(program);
15401
15867
  reviewCommand(program);
15402
15868
  qaCommand(program);
15403
15869
  auditCommand(program);
15870
+ analyticsCommand(program);
15404
15871
  installCommand(program);
15405
15872
  uninstallCommand(program);
15406
15873
  statusCommand(program);