@jonit-dev/night-watch-cli 1.8.1 → 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_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, 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";
@@ -109,10 +109,36 @@ var init_constants = __esm({
109
109
  schedule: DEFAULT_AUDIT_SCHEDULE,
110
110
  maxRuntime: DEFAULT_AUDIT_MAX_RUNTIME
111
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
+ };
112
130
  AUDIT_LOG_NAME = "audit";
113
131
  PLANNER_LOG_NAME = "slicer";
132
+ ANALYTICS_LOG_NAME = "analytics";
114
133
  VALID_PROVIDERS = ["claude", "codex"];
115
- 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
+ ];
116
142
  DEFAULT_JOB_PROVIDERS = {};
117
143
  BUILT_IN_PRESETS = {
118
144
  claude: {
@@ -191,7 +217,8 @@ var init_constants = __esm({
191
217
  reviewer: REVIEWER_LOG_NAME,
192
218
  qa: QA_LOG_NAME,
193
219
  audit: AUDIT_LOG_NAME,
194
- planner: PLANNER_LOG_NAME
220
+ planner: PLANNER_LOG_NAME,
221
+ analytics: ANALYTICS_LOG_NAME
195
222
  };
196
223
  GLOBAL_CONFIG_DIR = ".night-watch";
197
224
  REGISTRY_FILE_NAME = "projects.json";
@@ -208,7 +235,8 @@ var init_constants = __esm({
208
235
  reviewer: 40,
209
236
  slicer: 30,
210
237
  qa: 20,
211
- audit: 10
238
+ audit: 10,
239
+ analytics: 10
212
240
  };
213
241
  DEFAULT_QUEUE = {
214
242
  enabled: DEFAULT_QUEUE_ENABLED,
@@ -223,6 +251,15 @@ var init_constants = __esm({
223
251
  }
224
252
  });
225
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
+
226
263
  // ../core/dist/config-normalize.js
227
264
  function validateProvider(value) {
228
265
  const trimmed = value.trim();
@@ -418,6 +455,20 @@ function normalizeConfig(rawConfig) {
418
455
  };
419
456
  normalized.audit = audit;
420
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
+ }
421
472
  const rawJobProviders = readObject(rawConfig.jobProviders);
422
473
  if (rawJobProviders) {
423
474
  const jobProviders = {};
@@ -477,6 +528,7 @@ function normalizeConfig(rawConfig) {
477
528
  var init_config_normalize = __esm({
478
529
  "../core/dist/config-normalize.js"() {
479
530
  "use strict";
531
+ init_types2();
480
532
  init_constants();
481
533
  }
482
534
  });
@@ -700,6 +752,25 @@ function buildEnvOverrideConfig(fileConfig) {
700
752
  if (!isNaN(v) && v > 0)
701
753
  env.audit = { ...auditBase(), maxRuntime: v };
702
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
+ }
703
774
  const jobProvidersEnv = {};
704
775
  for (const jobType of VALID_JOB_TYPES) {
705
776
  const val = process.env[`NW_JOB_PROVIDER_${jobType.toUpperCase()}`];
@@ -792,6 +863,7 @@ function getDefaultConfig() {
792
863
  claudeModel: DEFAULT_CLAUDE_MODEL,
793
864
  qa: { ...DEFAULT_QA },
794
865
  audit: { ...DEFAULT_AUDIT },
866
+ analytics: { ...DEFAULT_ANALYTICS },
795
867
  jobProviders: { ...DEFAULT_JOB_PROVIDERS },
796
868
  queue: { ...DEFAULT_QUEUE }
797
869
  };
@@ -858,7 +930,7 @@ function mergeConfigLayer(base, layer) {
858
930
  ...layerQueue,
859
931
  providerBuckets: { ...baseQueue.providerBuckets, ...layerQueue.providerBuckets }
860
932
  };
861
- } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit") {
933
+ } else if (_key === "providerEnv" || _key === "boardProvider" || _key === "qa" || _key === "audit" || _key === "analytics") {
862
934
  base[_key] = {
863
935
  ...base[_key],
864
936
  ...value
@@ -943,15 +1015,6 @@ var init_config = __esm({
943
1015
  }
944
1016
  });
945
1017
 
946
- // ../core/dist/board/types.js
947
- var BOARD_COLUMNS;
948
- var init_types2 = __esm({
949
- "../core/dist/board/types.js"() {
950
- "use strict";
951
- BOARD_COLUMNS = ["Draft", "Ready", "In Progress", "Review", "Done"];
952
- }
953
- });
954
-
955
1018
  // ../core/dist/storage/repositories/sqlite/execution-history.repository.js
956
1019
  import Database from "better-sqlite3";
957
1020
  import { inject, injectable } from "tsyringe";
@@ -3046,6 +3109,9 @@ function auditLockPath(projectDir) {
3046
3109
  function plannerLockPath(projectDir) {
3047
3110
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
3048
3111
  }
3112
+ function analyticsLockPath(projectDir) {
3113
+ return `${LOCK_FILE_PREFIX}analytics-${projectRuntimeKey(projectDir)}.lock`;
3114
+ }
3049
3115
  function isProcessRunning(pid) {
3050
3116
  try {
3051
3117
  process.kill(pid, 0);
@@ -3404,7 +3470,8 @@ function collectLogInfo(projectDir) {
3404
3470
  { name: "reviewer", fileName: "reviewer.log" },
3405
3471
  { name: "qa", fileName: `${QA_LOG_NAME}.log` },
3406
3472
  { name: "audit", fileName: `${AUDIT_LOG_NAME}.log` },
3407
- { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` }
3473
+ { name: "planner", fileName: `${PLANNER_LOG_NAME}.log` },
3474
+ { name: "analytics", fileName: `${ANALYTICS_LOG_NAME}.log` }
3408
3475
  ];
3409
3476
  return logEntries.map(({ name, fileName }) => {
3410
3477
  const logPath = path6.join(projectDir, LOG_DIR, fileName);
@@ -3433,12 +3500,14 @@ async function fetchStatusSnapshot(projectDir, config) {
3433
3500
  const qaLock = checkLockFile(qaLockPath(projectDir));
3434
3501
  const auditLock = checkLockFile(auditLockPath(projectDir));
3435
3502
  const plannerLock = checkLockFile(plannerLockPath(projectDir));
3503
+ const analyticsLock = checkLockFile(analyticsLockPath(projectDir));
3436
3504
  const processes = [
3437
3505
  { name: "executor", running: executorLock.running, pid: executorLock.pid },
3438
3506
  { name: "reviewer", running: reviewerLock.running, pid: reviewerLock.pid },
3439
3507
  { name: "qa", running: qaLock.running, pid: qaLock.pid },
3440
3508
  { name: "audit", running: auditLock.running, pid: auditLock.pid },
3441
- { 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 }
3442
3511
  ];
3443
3512
  const prds = collectPrdInfo(projectDir, config.prdDir, config.maxRuntime);
3444
3513
  const prs = await collectPrInfo(projectDir, config.branchPatterns);
@@ -5628,6 +5697,8 @@ function isJobTypeEnabled(config, jobType) {
5628
5697
  return config.audit.enabled;
5629
5698
  case "slicer":
5630
5699
  return config.roadmapScanner.enabled;
5700
+ case "analytics":
5701
+ return config.analytics.enabled;
5631
5702
  default:
5632
5703
  return true;
5633
5704
  }
@@ -5987,6 +6058,8 @@ function getLockPathForJob(projectPath, jobType) {
5987
6058
  return auditLockPath(projectPath);
5988
6059
  case "slicer":
5989
6060
  return plannerLockPath(projectPath);
6061
+ case "analytics":
6062
+ return analyticsLockPath(projectPath);
5990
6063
  }
5991
6064
  }
5992
6065
  function reconcileStaleRunningJobs(db) {
@@ -6166,7 +6239,10 @@ function fitsProviderCapacity(candidate, config, inFlightByBucket) {
6166
6239
  }
6167
6240
  const bucketConfig = config?.providerBuckets?.[bucketKey];
6168
6241
  if (!bucketConfig) {
6169
- 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
+ });
6170
6246
  return true;
6171
6247
  }
6172
6248
  const inFlightCount = inFlightByBucket[bucketKey] ?? 0;
@@ -6197,7 +6273,10 @@ function dispatchNextJob(config) {
6197
6273
  const runningCount = running?.count ?? 0;
6198
6274
  logger.debug("Dispatch attempt", { mode, runningCount, maxConcurrency });
6199
6275
  if (runningCount >= maxConcurrency) {
6200
- logger.info("Dispatch skipped: global concurrency limit reached", { runningCount, maxConcurrency });
6276
+ logger.info("Dispatch skipped: global concurrency limit reached", {
6277
+ runningCount,
6278
+ maxConcurrency
6279
+ });
6201
6280
  return null;
6202
6281
  }
6203
6282
  const now = Math.floor(Date.now() / 1e3);
@@ -6223,7 +6302,9 @@ function dispatchNextJob(config) {
6223
6302
  logger.debug("Dispatch skipped: no pending jobs");
6224
6303
  return null;
6225
6304
  }
6226
- logger.debug("Provider-aware dispatch: evaluating candidates", { candidateCount: candidates.length });
6305
+ logger.debug("Provider-aware dispatch: evaluating candidates", {
6306
+ candidateCount: candidates.length
6307
+ });
6227
6308
  const inFlightByBucket = getInFlightCountByBucket(db);
6228
6309
  for (const candidate of candidates) {
6229
6310
  if (fitsProviderCapacity(candidate, config, inFlightByBucket)) {
@@ -6299,7 +6380,11 @@ function clearQueue(filter, force) {
6299
6380
  } else {
6300
6381
  result = db.prepare(`DELETE FROM job_queue WHERE status IN ${statuses}`).run();
6301
6382
  }
6302
- 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
+ });
6303
6388
  return result.changes;
6304
6389
  } finally {
6305
6390
  db.close();
@@ -6432,6 +6517,192 @@ var init_job_queue = __esm({
6432
6517
  }
6433
6518
  });
6434
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
+
6435
6706
  // ../core/dist/templates/prd-template.js
6436
6707
  function renderDependsOn(deps) {
6437
6708
  if (deps.length === 0) {
@@ -6608,6 +6879,7 @@ sequenceDiagram
6608
6879
  // ../core/dist/index.js
6609
6880
  var dist_exports = {};
6610
6881
  __export(dist_exports, {
6882
+ ANALYTICS_LOG_NAME: () => ANALYTICS_LOG_NAME,
6611
6883
  AUDIT_LOG_NAME: () => AUDIT_LOG_NAME,
6612
6884
  BOARD_COLUMNS: () => BOARD_COLUMNS,
6613
6885
  BUILT_IN_PRESETS: () => BUILT_IN_PRESETS,
@@ -6619,6 +6891,13 @@ __export(dist_exports, {
6619
6891
  CONFIG_FILE_NAME: () => CONFIG_FILE_NAME,
6620
6892
  CRONTAB_MARKER_PREFIX: () => CRONTAB_MARKER_PREFIX,
6621
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,
6622
6901
  DEFAULT_AUDIT: () => DEFAULT_AUDIT,
6623
6902
  DEFAULT_AUDIT_ENABLED: () => DEFAULT_AUDIT_ENABLED,
6624
6903
  DEFAULT_AUDIT_MAX_RUNTIME: () => DEFAULT_AUDIT_MAX_RUNTIME,
@@ -6705,6 +6984,7 @@ __export(dist_exports, {
6705
6984
  acquireLock: () => acquireLock,
6706
6985
  addDelayToIsoString: () => addDelayToIsoString,
6707
6986
  addEntry: () => addEntry,
6987
+ analyticsLockPath: () => analyticsLockPath,
6708
6988
  auditLockPath: () => auditLockPath,
6709
6989
  buildDescription: () => buildDescription,
6710
6990
  calculateStringSimilarity: () => calculateStringSimilarity,
@@ -6755,6 +7035,7 @@ __export(dist_exports, {
6755
7035
  extractPriority: () => extractPriority,
6756
7036
  extractQaScreenshotUrls: () => extractQaScreenshotUrls,
6757
7037
  extractSummary: () => extractSummary,
7038
+ fetchAmplitudeData: () => fetchAmplitudeData,
6758
7039
  fetchLatestQaCommentBody: () => fetchLatestQaCommentBody,
6759
7040
  fetchPrDetails: () => fetchPrDetails,
6760
7041
  fetchPrDetailsByNumber: () => fetchPrDetailsByNumber,
@@ -6862,6 +7143,7 @@ __export(dist_exports, {
6862
7143
  reviewerLockPath: () => reviewerLockPath,
6863
7144
  rotateLog: () => rotateLog,
6864
7145
  runAllChecks: () => runAllChecks,
7146
+ runAnalytics: () => runAnalytics,
6865
7147
  runMigrations: () => runMigrations,
6866
7148
  saveConfig: () => saveConfig,
6867
7149
  saveHistory: () => saveHistory,
@@ -6930,6 +7212,7 @@ var init_dist = __esm({
6930
7212
  init_webhook_validator();
6931
7213
  init_worktree_manager();
6932
7214
  init_job_queue();
7215
+ init_analytics();
6933
7216
  init_prd_template();
6934
7217
  init_slicer_prompt();
6935
7218
  }
@@ -6940,37 +7223,37 @@ import "reflect-metadata";
6940
7223
  import { Command as Command3 } from "commander";
6941
7224
  import { existsSync as existsSync29, readFileSync as readFileSync18 } from "fs";
6942
7225
  import { fileURLToPath as fileURLToPath4 } from "url";
6943
- import { dirname as dirname8, join as join34 } from "path";
7226
+ import { dirname as dirname8, join as join35 } from "path";
6944
7227
 
6945
7228
  // src/commands/init.ts
6946
7229
  init_dist();
6947
- import fs18 from "fs";
6948
- import path18 from "path";
7230
+ import fs19 from "fs";
7231
+ import path19 from "path";
6949
7232
  import { execSync as execSync3 } from "child_process";
6950
7233
  import { fileURLToPath as fileURLToPath2 } from "url";
6951
- import { dirname as dirname4, join as join16 } from "path";
7234
+ import { dirname as dirname4, join as join17 } from "path";
6952
7235
  import * as readline from "readline";
6953
7236
  var __filename = fileURLToPath2(import.meta.url);
6954
7237
  var __dirname2 = dirname4(__filename);
6955
7238
  function findTemplatesDir(startDir) {
6956
7239
  let d = startDir;
6957
7240
  for (let i = 0; i < 8; i++) {
6958
- const candidate = join16(d, "templates");
6959
- if (fs18.existsSync(candidate) && fs18.statSync(candidate).isDirectory()) {
7241
+ const candidate = join17(d, "templates");
7242
+ if (fs19.existsSync(candidate) && fs19.statSync(candidate).isDirectory()) {
6960
7243
  return candidate;
6961
7244
  }
6962
7245
  d = dirname4(d);
6963
7246
  }
6964
- return join16(startDir, "templates");
7247
+ return join17(startDir, "templates");
6965
7248
  }
6966
7249
  var TEMPLATES_DIR = findTemplatesDir(__dirname2);
6967
7250
  function hasPlaywrightDependency(cwd) {
6968
- const packageJsonPath = path18.join(cwd, "package.json");
6969
- if (!fs18.existsSync(packageJsonPath)) {
7251
+ const packageJsonPath = path19.join(cwd, "package.json");
7252
+ if (!fs19.existsSync(packageJsonPath)) {
6970
7253
  return false;
6971
7254
  }
6972
7255
  try {
6973
- const packageJson2 = JSON.parse(fs18.readFileSync(packageJsonPath, "utf-8"));
7256
+ const packageJson2 = JSON.parse(fs19.readFileSync(packageJsonPath, "utf-8"));
6974
7257
  return Boolean(
6975
7258
  packageJson2.dependencies?.["@playwright/test"] || packageJson2.dependencies?.playwright || packageJson2.devDependencies?.["@playwright/test"] || packageJson2.devDependencies?.playwright
6976
7259
  );
@@ -6982,7 +7265,7 @@ function detectPlaywright(cwd) {
6982
7265
  if (hasPlaywrightDependency(cwd)) {
6983
7266
  return true;
6984
7267
  }
6985
- if (fs18.existsSync(path18.join(cwd, "node_modules", ".bin", "playwright"))) {
7268
+ if (fs19.existsSync(path19.join(cwd, "node_modules", ".bin", "playwright"))) {
6986
7269
  return true;
6987
7270
  }
6988
7271
  try {
@@ -6998,10 +7281,10 @@ function detectPlaywright(cwd) {
6998
7281
  }
6999
7282
  }
7000
7283
  function resolvePlaywrightInstallCommand(cwd) {
7001
- if (fs18.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) {
7284
+ if (fs19.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) {
7002
7285
  return "pnpm add -D @playwright/test";
7003
7286
  }
7004
- if (fs18.existsSync(path18.join(cwd, "yarn.lock"))) {
7287
+ if (fs19.existsSync(path19.join(cwd, "yarn.lock"))) {
7005
7288
  return "yarn add -D @playwright/test";
7006
7289
  }
7007
7290
  return "npm install -D @playwright/test";
@@ -7147,8 +7430,8 @@ function promptProviderSelection(providers) {
7147
7430
  });
7148
7431
  }
7149
7432
  function ensureDir(dirPath) {
7150
- if (!fs18.existsSync(dirPath)) {
7151
- fs18.mkdirSync(dirPath, { recursive: true });
7433
+ if (!fs19.existsSync(dirPath)) {
7434
+ fs19.mkdirSync(dirPath, { recursive: true });
7152
7435
  }
7153
7436
  }
7154
7437
  function buildInitConfig(params) {
@@ -7195,6 +7478,7 @@ function buildInitConfig(params) {
7195
7478
  branchPatterns: [...defaults.qa.branchPatterns]
7196
7479
  },
7197
7480
  audit: { ...defaults.audit },
7481
+ analytics: { ...defaults.analytics },
7198
7482
  jobProviders: { ...defaults.jobProviders },
7199
7483
  queue: {
7200
7484
  ...defaults.queue,
@@ -7204,30 +7488,30 @@ function buildInitConfig(params) {
7204
7488
  }
7205
7489
  function resolveTemplatePath(templateName, customTemplatesDir, bundledTemplatesDir) {
7206
7490
  if (customTemplatesDir !== null) {
7207
- const customPath = join16(customTemplatesDir, templateName);
7208
- if (fs18.existsSync(customPath)) {
7491
+ const customPath = join17(customTemplatesDir, templateName);
7492
+ if (fs19.existsSync(customPath)) {
7209
7493
  return { path: customPath, source: "custom" };
7210
7494
  }
7211
7495
  }
7212
- return { path: join16(bundledTemplatesDir, templateName), source: "bundled" };
7496
+ return { path: join17(bundledTemplatesDir, templateName), source: "bundled" };
7213
7497
  }
7214
7498
  function processTemplate(templateName, targetPath, replacements, force, sourcePath, source) {
7215
- if (fs18.existsSync(targetPath) && !force) {
7499
+ if (fs19.existsSync(targetPath) && !force) {
7216
7500
  console.log(` Skipped (exists): ${targetPath}`);
7217
7501
  return { created: false, source: source ?? "bundled" };
7218
7502
  }
7219
- const templatePath = sourcePath ?? join16(TEMPLATES_DIR, templateName);
7503
+ const templatePath = sourcePath ?? join17(TEMPLATES_DIR, templateName);
7220
7504
  const resolvedSource = source ?? "bundled";
7221
- let content = fs18.readFileSync(templatePath, "utf-8");
7505
+ let content = fs19.readFileSync(templatePath, "utf-8");
7222
7506
  for (const [key, value] of Object.entries(replacements)) {
7223
7507
  content = content.replaceAll(key, value);
7224
7508
  }
7225
- fs18.writeFileSync(targetPath, content);
7509
+ fs19.writeFileSync(targetPath, content);
7226
7510
  console.log(` Created: ${targetPath} (${resolvedSource})`);
7227
7511
  return { created: true, source: resolvedSource };
7228
7512
  }
7229
7513
  function addToGitignore(cwd) {
7230
- const gitignorePath = path18.join(cwd, ".gitignore");
7514
+ const gitignorePath = path19.join(cwd, ".gitignore");
7231
7515
  const entries = [
7232
7516
  {
7233
7517
  pattern: "/logs/",
@@ -7241,13 +7525,13 @@ function addToGitignore(cwd) {
7241
7525
  },
7242
7526
  { pattern: "*.claim", label: "*.claim", check: (c) => c.includes("*.claim") }
7243
7527
  ];
7244
- if (!fs18.existsSync(gitignorePath)) {
7528
+ if (!fs19.existsSync(gitignorePath)) {
7245
7529
  const lines = ["# Night Watch", ...entries.map((e) => e.pattern), ""];
7246
- fs18.writeFileSync(gitignorePath, lines.join("\n"));
7530
+ fs19.writeFileSync(gitignorePath, lines.join("\n"));
7247
7531
  console.log(` Created: ${gitignorePath} (with Night Watch entries)`);
7248
7532
  return;
7249
7533
  }
7250
- const content = fs18.readFileSync(gitignorePath, "utf-8");
7534
+ const content = fs19.readFileSync(gitignorePath, "utf-8");
7251
7535
  const missing = entries.filter((e) => !e.check(content));
7252
7536
  if (missing.length === 0) {
7253
7537
  console.log(` Skipped (exists): Night Watch entries in .gitignore`);
@@ -7255,7 +7539,7 @@ function addToGitignore(cwd) {
7255
7539
  }
7256
7540
  const additions = missing.map((e) => e.pattern).join("\n");
7257
7541
  const newContent = content.trimEnd() + "\n\n# Night Watch\n" + additions + "\n";
7258
- fs18.writeFileSync(gitignorePath, newContent);
7542
+ fs19.writeFileSync(gitignorePath, newContent);
7259
7543
  console.log(` Updated: ${gitignorePath} (added ${missing.map((e) => e.label).join(", ")})`);
7260
7544
  }
7261
7545
  function initCommand(program2) {
@@ -7376,28 +7660,28 @@ function initCommand(program2) {
7376
7660
  "${DEFAULT_BRANCH}": defaultBranch
7377
7661
  };
7378
7662
  step(6, totalSteps, "Creating PRD directory structure...");
7379
- const prdDirPath = path18.join(cwd, prdDir);
7380
- const doneDirPath = path18.join(prdDirPath, "done");
7663
+ const prdDirPath = path19.join(cwd, prdDir);
7664
+ const doneDirPath = path19.join(prdDirPath, "done");
7381
7665
  ensureDir(doneDirPath);
7382
7666
  success(`Created ${prdDirPath}/`);
7383
7667
  success(`Created ${doneDirPath}/`);
7384
7668
  step(7, totalSteps, "Creating logs directory...");
7385
- const logsPath = path18.join(cwd, LOG_DIR);
7669
+ const logsPath = path19.join(cwd, LOG_DIR);
7386
7670
  ensureDir(logsPath);
7387
7671
  success(`Created ${logsPath}/`);
7388
7672
  addToGitignore(cwd);
7389
7673
  step(8, totalSteps, "Creating instructions directory...");
7390
- const instructionsDir = path18.join(cwd, "instructions");
7674
+ const instructionsDir = path19.join(cwd, "instructions");
7391
7675
  ensureDir(instructionsDir);
7392
7676
  success(`Created ${instructionsDir}/`);
7393
7677
  const existingConfig = loadConfig(cwd);
7394
- const customTemplatesDirPath = path18.join(cwd, existingConfig.templatesDir);
7395
- const customTemplatesDir = fs18.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7678
+ const customTemplatesDirPath = path19.join(cwd, existingConfig.templatesDir);
7679
+ const customTemplatesDir = fs19.existsSync(customTemplatesDirPath) ? customTemplatesDirPath : null;
7396
7680
  const templateSources = [];
7397
7681
  const nwResolution = resolveTemplatePath("executor.md", customTemplatesDir, TEMPLATES_DIR);
7398
7682
  const nwResult = processTemplate(
7399
7683
  "executor.md",
7400
- path18.join(instructionsDir, "executor.md"),
7684
+ path19.join(instructionsDir, "executor.md"),
7401
7685
  replacements,
7402
7686
  force,
7403
7687
  nwResolution.path,
@@ -7411,7 +7695,7 @@ function initCommand(program2) {
7411
7695
  );
7412
7696
  const peResult = processTemplate(
7413
7697
  "prd-executor.md",
7414
- path18.join(instructionsDir, "prd-executor.md"),
7698
+ path19.join(instructionsDir, "prd-executor.md"),
7415
7699
  replacements,
7416
7700
  force,
7417
7701
  peResolution.path,
@@ -7421,7 +7705,7 @@ function initCommand(program2) {
7421
7705
  const prResolution = resolveTemplatePath("pr-reviewer.md", customTemplatesDir, TEMPLATES_DIR);
7422
7706
  const prResult = processTemplate(
7423
7707
  "pr-reviewer.md",
7424
- path18.join(instructionsDir, "pr-reviewer.md"),
7708
+ path19.join(instructionsDir, "pr-reviewer.md"),
7425
7709
  replacements,
7426
7710
  force,
7427
7711
  prResolution.path,
@@ -7431,7 +7715,7 @@ function initCommand(program2) {
7431
7715
  const qaResolution = resolveTemplatePath("qa.md", customTemplatesDir, TEMPLATES_DIR);
7432
7716
  const qaResult = processTemplate(
7433
7717
  "qa.md",
7434
- path18.join(instructionsDir, "qa.md"),
7718
+ path19.join(instructionsDir, "qa.md"),
7435
7719
  replacements,
7436
7720
  force,
7437
7721
  qaResolution.path,
@@ -7441,7 +7725,7 @@ function initCommand(program2) {
7441
7725
  const auditResolution = resolveTemplatePath("audit.md", customTemplatesDir, TEMPLATES_DIR);
7442
7726
  const auditResult = processTemplate(
7443
7727
  "audit.md",
7444
- path18.join(instructionsDir, "audit.md"),
7728
+ path19.join(instructionsDir, "audit.md"),
7445
7729
  replacements,
7446
7730
  force,
7447
7731
  auditResolution.path,
@@ -7449,8 +7733,8 @@ function initCommand(program2) {
7449
7733
  );
7450
7734
  templateSources.push({ name: "audit.md", source: auditResult.source });
7451
7735
  step(9, totalSteps, "Creating configuration file...");
7452
- const configPath = path18.join(cwd, CONFIG_FILE_NAME);
7453
- if (fs18.existsSync(configPath) && !force) {
7736
+ const configPath = path19.join(cwd, CONFIG_FILE_NAME);
7737
+ if (fs19.existsSync(configPath) && !force) {
7454
7738
  console.log(` Skipped (exists): ${configPath}`);
7455
7739
  } else {
7456
7740
  const config = buildInitConfig({
@@ -7460,11 +7744,11 @@ function initCommand(program2) {
7460
7744
  reviewerEnabled,
7461
7745
  prdDir
7462
7746
  });
7463
- fs18.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7747
+ fs19.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
7464
7748
  success(`Created ${configPath}`);
7465
7749
  }
7466
7750
  step(10, totalSteps, "Setting up GitHub Project board...");
7467
- const existingRaw = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
7751
+ const existingRaw = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
7468
7752
  const existingBoard = existingRaw.boardProvider;
7469
7753
  let boardSetupStatus = "Skipped";
7470
7754
  if (existingBoard?.projectNumber && !force) {
@@ -7486,13 +7770,13 @@ function initCommand(program2) {
7486
7770
  const provider = createBoardProvider({ enabled: true, provider: "github" }, cwd);
7487
7771
  const boardTitle = `${projectName} Night Watch`;
7488
7772
  const board = await provider.setupBoard(boardTitle);
7489
- const rawConfig = JSON.parse(fs18.readFileSync(configPath, "utf-8"));
7773
+ const rawConfig = JSON.parse(fs19.readFileSync(configPath, "utf-8"));
7490
7774
  rawConfig.boardProvider = {
7491
7775
  enabled: true,
7492
7776
  provider: "github",
7493
7777
  projectNumber: board.number
7494
7778
  };
7495
- fs18.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7779
+ fs19.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n");
7496
7780
  boardSetupStatus = `Created (#${board.number})`;
7497
7781
  success(`GitHub Project board "${boardTitle}" ready (#${board.number})`);
7498
7782
  } catch (boardErr) {
@@ -7616,8 +7900,8 @@ function getTelegramStatusWebhooks(config) {
7616
7900
  }
7617
7901
 
7618
7902
  // src/commands/run.ts
7619
- import * as fs19 from "fs";
7620
- import * as path19 from "path";
7903
+ import * as fs20 from "fs";
7904
+ import * as path20 from "path";
7621
7905
  function resolveRunNotificationEvent(exitCode, scriptStatus) {
7622
7906
  if (exitCode === 124) {
7623
7907
  return "run_timeout";
@@ -7649,12 +7933,12 @@ function shouldAttemptCrossProjectFallback(options, scriptStatus) {
7649
7933
  return scriptStatus === "skip_no_eligible_prd";
7650
7934
  }
7651
7935
  function getCrossProjectFallbackCandidates(currentProjectDir) {
7652
- const current = path19.resolve(currentProjectDir);
7936
+ const current = path20.resolve(currentProjectDir);
7653
7937
  const { valid, invalid } = validateRegistry();
7654
7938
  for (const entry of invalid) {
7655
7939
  warn(`Skipping invalid registry entry: ${entry.path}`);
7656
7940
  }
7657
- return valid.filter((entry) => path19.resolve(entry.path) !== current);
7941
+ return valid.filter((entry) => path20.resolve(entry.path) !== current);
7658
7942
  }
7659
7943
  async function sendRunCompletionNotifications(config, projectDir, options, exitCode, scriptResult) {
7660
7944
  if (isRateLimitFallbackTriggered(scriptResult?.data)) {
@@ -7664,7 +7948,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7664
7948
  if (nonTelegramWebhooks.length > 0) {
7665
7949
  const _rateLimitCtx = {
7666
7950
  event: "rate_limit_fallback",
7667
- projectName: path19.basename(projectDir),
7951
+ projectName: path20.basename(projectDir),
7668
7952
  exitCode,
7669
7953
  provider: config.provider
7670
7954
  };
@@ -7692,7 +7976,7 @@ async function sendRunCompletionNotifications(config, projectDir, options, exitC
7692
7976
  const timeoutDuration = event === "run_timeout" ? config.maxRuntime : void 0;
7693
7977
  const _ctx = {
7694
7978
  event,
7695
- projectName: path19.basename(projectDir),
7979
+ projectName: path20.basename(projectDir),
7696
7980
  exitCode,
7697
7981
  provider: config.provider,
7698
7982
  prdName: scriptResult?.data.prd,
@@ -7854,20 +8138,20 @@ function applyCliOverrides(config, options) {
7854
8138
  return overridden;
7855
8139
  }
7856
8140
  function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7857
- const absolutePrdDir = path19.join(projectDir, prdDir);
7858
- const doneDir = path19.join(absolutePrdDir, "done");
8141
+ const absolutePrdDir = path20.join(projectDir, prdDir);
8142
+ const doneDir = path20.join(absolutePrdDir, "done");
7859
8143
  const pending = [];
7860
8144
  const completed = [];
7861
- if (fs19.existsSync(absolutePrdDir)) {
7862
- const entries = fs19.readdirSync(absolutePrdDir, { withFileTypes: true });
8145
+ if (fs20.existsSync(absolutePrdDir)) {
8146
+ const entries = fs20.readdirSync(absolutePrdDir, { withFileTypes: true });
7863
8147
  for (const entry of entries) {
7864
8148
  if (entry.isFile() && entry.name.endsWith(".md")) {
7865
- const claimPath = path19.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
8149
+ const claimPath = path20.join(absolutePrdDir, entry.name + CLAIM_FILE_EXTENSION);
7866
8150
  let claimed = false;
7867
8151
  let claimInfo = null;
7868
- if (fs19.existsSync(claimPath)) {
8152
+ if (fs20.existsSync(claimPath)) {
7869
8153
  try {
7870
- const content = fs19.readFileSync(claimPath, "utf-8");
8154
+ const content = fs20.readFileSync(claimPath, "utf-8");
7871
8155
  const data = JSON.parse(content);
7872
8156
  const age = Math.floor(Date.now() / 1e3) - data.timestamp;
7873
8157
  if (age < maxRuntime) {
@@ -7881,8 +8165,8 @@ function scanPrdDirectory(projectDir, prdDir, maxRuntime) {
7881
8165
  }
7882
8166
  }
7883
8167
  }
7884
- if (fs19.existsSync(doneDir)) {
7885
- const entries = fs19.readdirSync(doneDir, { withFileTypes: true });
8168
+ if (fs20.existsSync(doneDir)) {
8169
+ const entries = fs20.readdirSync(doneDir, { withFileTypes: true });
7886
8170
  for (const entry of entries) {
7887
8171
  if (entry.isFile() && entry.name.endsWith(".md")) {
7888
8172
  completed.push(entry.name);
@@ -8042,7 +8326,7 @@ ${stderr}`);
8042
8326
  // src/commands/review.ts
8043
8327
  init_dist();
8044
8328
  import { execFileSync as execFileSync5 } from "child_process";
8045
- import * as path20 from "path";
8329
+ import * as path21 from "path";
8046
8330
  function shouldSendReviewNotification(scriptStatus) {
8047
8331
  if (!scriptStatus) {
8048
8332
  return true;
@@ -8282,7 +8566,7 @@ ${stderr}`);
8282
8566
  const finalScore = parseFinalReviewScore(scriptResult?.data.final_score);
8283
8567
  const _reviewCtx = {
8284
8568
  event: "review_completed",
8285
- projectName: path20.basename(projectDir),
8569
+ projectName: path21.basename(projectDir),
8286
8570
  exitCode,
8287
8571
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8288
8572
  prUrl: prDetails?.url,
@@ -8303,7 +8587,7 @@ ${stderr}`);
8303
8587
  const autoMergedPrDetails = fetchPrDetailsByNumber(autoMergedPrNumber, projectDir);
8304
8588
  const _mergeCtx = {
8305
8589
  event: "pr_auto_merged",
8306
- projectName: path20.basename(projectDir),
8590
+ projectName: path21.basename(projectDir),
8307
8591
  exitCode,
8308
8592
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8309
8593
  prNumber: autoMergedPrDetails?.number ?? autoMergedPrNumber,
@@ -8328,7 +8612,7 @@ ${stderr}`);
8328
8612
 
8329
8613
  // src/commands/qa.ts
8330
8614
  init_dist();
8331
- import * as path21 from "path";
8615
+ import * as path22 from "path";
8332
8616
  function shouldSendQaNotification(scriptStatus) {
8333
8617
  if (!scriptStatus) {
8334
8618
  return true;
@@ -8462,7 +8746,7 @@ ${stderr}`);
8462
8746
  const qaScreenshotUrls = primaryQaPr !== void 0 ? fetchQaScreenshotUrlsForPr(primaryQaPr, projectDir, repo) : [];
8463
8747
  const _qaCtx = {
8464
8748
  event: "qa_completed",
8465
- projectName: path21.basename(projectDir),
8749
+ projectName: path22.basename(projectDir),
8466
8750
  exitCode,
8467
8751
  provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
8468
8752
  prNumber: prDetails?.number ?? primaryQaPr,
@@ -8488,8 +8772,8 @@ ${stderr}`);
8488
8772
 
8489
8773
  // src/commands/audit.ts
8490
8774
  init_dist();
8491
- import * as fs20 from "fs";
8492
- import * as path22 from "path";
8775
+ import * as fs21 from "fs";
8776
+ import * as path23 from "path";
8493
8777
  function buildEnvVars4(config, options) {
8494
8778
  const env = buildBaseEnvVars(config, "audit", options.dryRun);
8495
8779
  env.NW_AUDIT_MAX_RUNTIME = String(config.audit.maxRuntime);
@@ -8532,7 +8816,7 @@ function auditCommand(program2) {
8532
8816
  configTable.push(["Provider", auditProvider]);
8533
8817
  configTable.push(["Provider CLI", PROVIDER_COMMANDS[auditProvider]]);
8534
8818
  configTable.push(["Max Runtime", `${config.audit.maxRuntime}s`]);
8535
- configTable.push(["Report File", path22.join(projectDir, "logs", "audit-report.md")]);
8819
+ configTable.push(["Report File", path23.join(projectDir, "logs", "audit-report.md")]);
8536
8820
  console.log(configTable.toString());
8537
8821
  header("Provider Invocation");
8538
8822
  const providerCmd = PROVIDER_COMMANDS[auditProvider];
@@ -8567,8 +8851,8 @@ ${stderr}`);
8567
8851
  } else if (scriptResult?.status?.startsWith("skip_")) {
8568
8852
  spinner.succeed("Code audit skipped");
8569
8853
  } else {
8570
- const reportPath = path22.join(projectDir, "logs", "audit-report.md");
8571
- if (!fs20.existsSync(reportPath)) {
8854
+ const reportPath = path23.join(projectDir, "logs", "audit-report.md");
8855
+ if (!fs21.existsSync(reportPath)) {
8572
8856
  spinner.fail("Code audit finished without a report file");
8573
8857
  process.exit(1);
8574
8858
  }
@@ -8579,9 +8863,9 @@ ${stderr}`);
8579
8863
  const providerExit = scriptResult?.data?.provider_exit;
8580
8864
  const exitDetail = providerExit && providerExit !== String(exitCode) ? `, provider exit ${providerExit}` : "";
8581
8865
  spinner.fail(`Code audit exited with code ${exitCode}${statusSuffix}${exitDetail}`);
8582
- const logPath = path22.join(projectDir, "logs", "audit.log");
8583
- if (fs20.existsSync(logPath)) {
8584
- 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);
8585
8869
  if (logLines.length > 0) {
8586
8870
  process.stderr.write(logLines.join("\n") + "\n");
8587
8871
  }
@@ -8595,19 +8879,80 @@ ${stderr}`);
8595
8879
  });
8596
8880
  }
8597
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
+
8598
8943
  // src/commands/install.ts
8599
8944
  init_dist();
8600
8945
  import { execSync as execSync4 } from "child_process";
8601
- import * as path23 from "path";
8602
- import * as fs21 from "fs";
8946
+ import * as path24 from "path";
8947
+ import * as fs22 from "fs";
8603
8948
  function shellQuote(value) {
8604
8949
  return `'${value.replace(/'/g, `'"'"'`)}'`;
8605
8950
  }
8606
8951
  function getNightWatchBinPath() {
8607
8952
  try {
8608
8953
  const npmBin = execSync4("npm bin -g", { encoding: "utf-8" }).trim();
8609
- const binPath = path23.join(npmBin, "night-watch");
8610
- if (fs21.existsSync(binPath)) {
8954
+ const binPath = path24.join(npmBin, "night-watch");
8955
+ if (fs22.existsSync(binPath)) {
8611
8956
  return binPath;
8612
8957
  }
8613
8958
  } catch {
@@ -8620,17 +8965,17 @@ function getNightWatchBinPath() {
8620
8965
  }
8621
8966
  function getNodeBinDir() {
8622
8967
  if (process.execPath && process.execPath !== "node") {
8623
- return path23.dirname(process.execPath);
8968
+ return path24.dirname(process.execPath);
8624
8969
  }
8625
8970
  try {
8626
8971
  const nodePath = execSync4("which node", { encoding: "utf-8" }).trim();
8627
- return path23.dirname(nodePath);
8972
+ return path24.dirname(nodePath);
8628
8973
  } catch {
8629
8974
  return "";
8630
8975
  }
8631
8976
  }
8632
8977
  function buildCronPathPrefix(nodeBinDir, nightWatchBin) {
8633
- const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path23.dirname(nightWatchBin) : "";
8978
+ const nightWatchBinDir = nightWatchBin.includes("/") || nightWatchBin.includes("\\") ? path24.dirname(nightWatchBin) : "";
8634
8979
  const pathParts = Array.from(
8635
8980
  new Set([nodeBinDir, nightWatchBinDir].filter((part) => part.length > 0))
8636
8981
  );
@@ -8646,12 +8991,12 @@ function performInstall(projectDir, config, options) {
8646
8991
  const nightWatchBin = getNightWatchBinPath();
8647
8992
  const projectName = getProjectName(projectDir);
8648
8993
  const marker = generateMarker(projectName);
8649
- const logDir = path23.join(projectDir, LOG_DIR);
8650
- if (!fs21.existsSync(logDir)) {
8651
- fs21.mkdirSync(logDir, { recursive: true });
8994
+ const logDir = path24.join(projectDir, LOG_DIR);
8995
+ if (!fs22.existsSync(logDir)) {
8996
+ fs22.mkdirSync(logDir, { recursive: true });
8652
8997
  }
8653
- const executorLog = path23.join(logDir, "executor.log");
8654
- const reviewerLog = path23.join(logDir, "reviewer.log");
8998
+ const executorLog = path24.join(logDir, "executor.log");
8999
+ const reviewerLog = path24.join(logDir, "reviewer.log");
8655
9000
  if (!options?.force) {
8656
9001
  const existingEntries2 = Array.from(
8657
9002
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
@@ -8688,7 +9033,7 @@ function performInstall(projectDir, config, options) {
8688
9033
  const installSlicer = options?.noSlicer === true ? false : config.roadmapScanner.enabled;
8689
9034
  if (installSlicer) {
8690
9035
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
8691
- const slicerLog = path23.join(logDir, "slicer.log");
9036
+ const slicerLog = path24.join(logDir, "slicer.log");
8692
9037
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8693
9038
  entries.push(slicerEntry);
8694
9039
  }
@@ -8696,7 +9041,7 @@ function performInstall(projectDir, config, options) {
8696
9041
  const installQa = disableQa ? false : config.qa.enabled;
8697
9042
  if (installQa) {
8698
9043
  const qaSchedule = config.qa.schedule;
8699
- const qaLog = path23.join(logDir, "qa.log");
9044
+ const qaLog = path24.join(logDir, "qa.log");
8700
9045
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8701
9046
  entries.push(qaEntry);
8702
9047
  }
@@ -8704,10 +9049,18 @@ function performInstall(projectDir, config, options) {
8704
9049
  const installAudit = disableAudit ? false : config.audit.enabled;
8705
9050
  if (installAudit) {
8706
9051
  const auditSchedule = config.audit.schedule;
8707
- const auditLog = path23.join(logDir, "audit.log");
9052
+ const auditLog = path24.join(logDir, "audit.log");
8708
9053
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8709
9054
  entries.push(auditEntry);
8710
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
+ }
8711
9064
  const existingEntries = new Set(
8712
9065
  Array.from(/* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)]))
8713
9066
  );
@@ -8726,7 +9079,7 @@ function performInstall(projectDir, config, options) {
8726
9079
  }
8727
9080
  }
8728
9081
  function installCommand(program2) {
8729
- 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) => {
8730
9083
  try {
8731
9084
  const projectDir = process.cwd();
8732
9085
  const config = loadConfig(projectDir);
@@ -8735,12 +9088,12 @@ function installCommand(program2) {
8735
9088
  const nightWatchBin = getNightWatchBinPath();
8736
9089
  const projectName = getProjectName(projectDir);
8737
9090
  const marker = generateMarker(projectName);
8738
- const logDir = path23.join(projectDir, LOG_DIR);
8739
- if (!fs21.existsSync(logDir)) {
8740
- fs21.mkdirSync(logDir, { recursive: true });
9091
+ const logDir = path24.join(projectDir, LOG_DIR);
9092
+ if (!fs22.existsSync(logDir)) {
9093
+ fs22.mkdirSync(logDir, { recursive: true });
8741
9094
  }
8742
- const executorLog = path23.join(logDir, "executor.log");
8743
- const reviewerLog = path23.join(logDir, "reviewer.log");
9095
+ const executorLog = path24.join(logDir, "executor.log");
9096
+ const reviewerLog = path24.join(logDir, "reviewer.log");
8744
9097
  const existingEntries = Array.from(
8745
9098
  /* @__PURE__ */ new Set([...getEntries(marker), ...getProjectEntries(projectDir)])
8746
9099
  );
@@ -8776,7 +9129,7 @@ function installCommand(program2) {
8776
9129
  const installSlicer = options.noSlicer === true ? false : config.roadmapScanner.enabled;
8777
9130
  let slicerLog;
8778
9131
  if (installSlicer) {
8779
- slicerLog = path23.join(logDir, "slicer.log");
9132
+ slicerLog = path24.join(logDir, "slicer.log");
8780
9133
  const slicerSchedule = config.roadmapScanner.slicerSchedule;
8781
9134
  const slicerEntry = `${slicerSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} planner >> ${shellQuote(slicerLog)} 2>&1 ${marker}`;
8782
9135
  entries.push(slicerEntry);
@@ -8785,7 +9138,7 @@ function installCommand(program2) {
8785
9138
  const installQa = disableQa ? false : config.qa.enabled;
8786
9139
  let qaLog;
8787
9140
  if (installQa) {
8788
- qaLog = path23.join(logDir, "qa.log");
9141
+ qaLog = path24.join(logDir, "qa.log");
8789
9142
  const qaSchedule = config.qa.schedule;
8790
9143
  const qaEntry = `${qaSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} qa >> ${shellQuote(qaLog)} 2>&1 ${marker}`;
8791
9144
  entries.push(qaEntry);
@@ -8794,11 +9147,20 @@ function installCommand(program2) {
8794
9147
  const installAudit = disableAudit ? false : config.audit.enabled;
8795
9148
  let auditLog;
8796
9149
  if (installAudit) {
8797
- auditLog = path23.join(logDir, "audit.log");
9150
+ auditLog = path24.join(logDir, "audit.log");
8798
9151
  const auditSchedule = config.audit.schedule;
8799
9152
  const auditEntry = `${auditSchedule} ${pathPrefix}${providerEnvPrefix}${cliBinPrefix}${cronTriggerPrefix}cd ${shellQuote(projectDir)} && ${shellQuote(nightWatchBin)} audit >> ${shellQuote(auditLog)} 2>&1 ${marker}`;
8800
9153
  entries.push(auditEntry);
8801
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
+ }
8802
9164
  const existingEntrySet = new Set(existingEntries);
8803
9165
  const currentCrontab = readCrontab();
8804
9166
  const baseCrontab = options.force ? currentCrontab.filter((line) => !existingEntrySet.has(line) && !line.includes(marker)) : currentCrontab;
@@ -8825,6 +9187,9 @@ function installCommand(program2) {
8825
9187
  if (installAudit && auditLog) {
8826
9188
  dim(` Audit: ${auditLog}`);
8827
9189
  }
9190
+ if (installAnalytics && analyticsLog) {
9191
+ dim(` Analytics: ${analyticsLog}`);
9192
+ }
8828
9193
  console.log();
8829
9194
  dim("To uninstall, run: night-watch uninstall");
8830
9195
  dim("To check status, run: night-watch status");
@@ -8839,8 +9204,8 @@ function installCommand(program2) {
8839
9204
 
8840
9205
  // src/commands/uninstall.ts
8841
9206
  init_dist();
8842
- import * as path24 from "path";
8843
- import * as fs22 from "fs";
9207
+ import * as path25 from "path";
9208
+ import * as fs23 from "fs";
8844
9209
  function performUninstall(projectDir, options) {
8845
9210
  try {
8846
9211
  const projectName = getProjectName(projectDir);
@@ -8855,19 +9220,19 @@ function performUninstall(projectDir, options) {
8855
9220
  const removedCount = removeEntriesForProject(projectDir, marker);
8856
9221
  unregisterProject(projectDir);
8857
9222
  if (!options?.keepLogs) {
8858
- const logDir = path24.join(projectDir, "logs");
8859
- if (fs22.existsSync(logDir)) {
9223
+ const logDir = path25.join(projectDir, "logs");
9224
+ if (fs23.existsSync(logDir)) {
8860
9225
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8861
9226
  logFiles.forEach((logFile) => {
8862
- const logPath = path24.join(logDir, logFile);
8863
- if (fs22.existsSync(logPath)) {
8864
- fs22.unlinkSync(logPath);
9227
+ const logPath = path25.join(logDir, logFile);
9228
+ if (fs23.existsSync(logPath)) {
9229
+ fs23.unlinkSync(logPath);
8865
9230
  }
8866
9231
  });
8867
9232
  try {
8868
- const remainingFiles = fs22.readdirSync(logDir);
9233
+ const remainingFiles = fs23.readdirSync(logDir);
8869
9234
  if (remainingFiles.length === 0) {
8870
- fs22.rmdirSync(logDir);
9235
+ fs23.rmdirSync(logDir);
8871
9236
  }
8872
9237
  } catch {
8873
9238
  }
@@ -8900,21 +9265,21 @@ function uninstallCommand(program2) {
8900
9265
  existingEntries.forEach((entry) => dim(` ${entry}`));
8901
9266
  const removedCount = removeEntriesForProject(projectDir, marker);
8902
9267
  if (!options.keepLogs) {
8903
- const logDir = path24.join(projectDir, "logs");
8904
- if (fs22.existsSync(logDir)) {
9268
+ const logDir = path25.join(projectDir, "logs");
9269
+ if (fs23.existsSync(logDir)) {
8905
9270
  const logFiles = ["executor.log", "reviewer.log", "slicer.log", "audit.log"];
8906
9271
  let logsRemoved = 0;
8907
9272
  logFiles.forEach((logFile) => {
8908
- const logPath = path24.join(logDir, logFile);
8909
- if (fs22.existsSync(logPath)) {
8910
- fs22.unlinkSync(logPath);
9273
+ const logPath = path25.join(logDir, logFile);
9274
+ if (fs23.existsSync(logPath)) {
9275
+ fs23.unlinkSync(logPath);
8911
9276
  logsRemoved++;
8912
9277
  }
8913
9278
  });
8914
9279
  try {
8915
- const remainingFiles = fs22.readdirSync(logDir);
9280
+ const remainingFiles = fs23.readdirSync(logDir);
8916
9281
  if (remainingFiles.length === 0) {
8917
- fs22.rmdirSync(logDir);
9282
+ fs23.rmdirSync(logDir);
8918
9283
  }
8919
9284
  } catch {
8920
9285
  }
@@ -9150,14 +9515,14 @@ function statusCommand(program2) {
9150
9515
  // src/commands/logs.ts
9151
9516
  init_dist();
9152
9517
  import { spawn as spawn3 } from "child_process";
9153
- import * as path25 from "path";
9154
- import * as fs23 from "fs";
9518
+ import * as path26 from "path";
9519
+ import * as fs24 from "fs";
9155
9520
  function getLastLines(filePath, lineCount) {
9156
- if (!fs23.existsSync(filePath)) {
9521
+ if (!fs24.existsSync(filePath)) {
9157
9522
  return `Log file not found: ${filePath}`;
9158
9523
  }
9159
9524
  try {
9160
- const content = fs23.readFileSync(filePath, "utf-8");
9525
+ const content = fs24.readFileSync(filePath, "utf-8");
9161
9526
  const lines = content.trim().split("\n");
9162
9527
  return lines.slice(-lineCount).join("\n");
9163
9528
  } catch (error2) {
@@ -9165,7 +9530,7 @@ function getLastLines(filePath, lineCount) {
9165
9530
  }
9166
9531
  }
9167
9532
  function followLog(filePath) {
9168
- if (!fs23.existsSync(filePath)) {
9533
+ if (!fs24.existsSync(filePath)) {
9169
9534
  console.log(`Log file not found: ${filePath}`);
9170
9535
  console.log("The log file will be created when the first execution runs.");
9171
9536
  return;
@@ -9185,13 +9550,13 @@ function logsCommand(program2) {
9185
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) => {
9186
9551
  try {
9187
9552
  const projectDir = process.cwd();
9188
- const logDir = path25.join(projectDir, LOG_DIR);
9553
+ const logDir = path26.join(projectDir, LOG_DIR);
9189
9554
  const lineCount = parseInt(options.lines || "50", 10);
9190
- const executorLog = path25.join(logDir, EXECUTOR_LOG_FILE);
9191
- const reviewerLog = path25.join(logDir, REVIEWER_LOG_FILE);
9192
- const qaLog = path25.join(logDir, `${QA_LOG_NAME}.log`);
9193
- const auditLog = path25.join(logDir, `${AUDIT_LOG_NAME}.log`);
9194
- 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`);
9195
9560
  const logType = options.type?.toLowerCase() || "all";
9196
9561
  const showExecutor = logType === "all" || logType === "run" || logType === "executor";
9197
9562
  const showReviewer = logType === "all" || logType === "review" || logType === "reviewer";
@@ -9255,15 +9620,15 @@ function logsCommand(program2) {
9255
9620
 
9256
9621
  // src/commands/prd.ts
9257
9622
  init_dist();
9258
- import * as fs24 from "fs";
9259
- import * as path26 from "path";
9623
+ import * as fs25 from "fs";
9624
+ import * as path27 from "path";
9260
9625
  import * as readline2 from "readline";
9261
9626
  function slugify2(name) {
9262
9627
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9263
9628
  }
9264
9629
  function getNextPrdNumber2(prdDir) {
9265
- if (!fs24.existsSync(prdDir)) return 1;
9266
- 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"));
9267
9632
  const numbers = files.map((f) => {
9268
9633
  const match = f.match(/^(\d+)-/);
9269
9634
  return match ? parseInt(match[1], 10) : 0;
@@ -9284,10 +9649,10 @@ function parseDependencies(content) {
9284
9649
  }
9285
9650
  function isClaimActive(claimPath, maxRuntime) {
9286
9651
  try {
9287
- if (!fs24.existsSync(claimPath)) {
9652
+ if (!fs25.existsSync(claimPath)) {
9288
9653
  return { active: false };
9289
9654
  }
9290
- const content = fs24.readFileSync(claimPath, "utf-8");
9655
+ const content = fs25.readFileSync(claimPath, "utf-8");
9291
9656
  const claim = JSON.parse(content);
9292
9657
  const age = Math.floor(Date.now() / 1e3) - claim.timestamp;
9293
9658
  if (age < maxRuntime) {
@@ -9303,9 +9668,9 @@ function prdCommand(program2) {
9303
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) => {
9304
9669
  const projectDir = process.cwd();
9305
9670
  const config = loadConfig(projectDir);
9306
- const prdDir = path26.join(projectDir, config.prdDir);
9307
- if (!fs24.existsSync(prdDir)) {
9308
- fs24.mkdirSync(prdDir, { recursive: true });
9671
+ const prdDir = path27.join(projectDir, config.prdDir);
9672
+ if (!fs25.existsSync(prdDir)) {
9673
+ fs25.mkdirSync(prdDir, { recursive: true });
9309
9674
  }
9310
9675
  let complexityScore = 5;
9311
9676
  let dependsOn = [];
@@ -9364,20 +9729,20 @@ function prdCommand(program2) {
9364
9729
  } else {
9365
9730
  filename = `${slug}.md`;
9366
9731
  }
9367
- const filePath = path26.join(prdDir, filename);
9368
- if (fs24.existsSync(filePath)) {
9732
+ const filePath = path27.join(prdDir, filename);
9733
+ if (fs25.existsSync(filePath)) {
9369
9734
  error(`File already exists: ${filePath}`);
9370
9735
  dim("Use a different name or remove the existing file.");
9371
9736
  process.exit(1);
9372
9737
  }
9373
9738
  let customTemplate;
9374
9739
  if (options.template) {
9375
- const templatePath = path26.resolve(options.template);
9376
- if (!fs24.existsSync(templatePath)) {
9740
+ const templatePath = path27.resolve(options.template);
9741
+ if (!fs25.existsSync(templatePath)) {
9377
9742
  error(`Template file not found: ${templatePath}`);
9378
9743
  process.exit(1);
9379
9744
  }
9380
- customTemplate = fs24.readFileSync(templatePath, "utf-8");
9745
+ customTemplate = fs25.readFileSync(templatePath, "utf-8");
9381
9746
  }
9382
9747
  const vars = {
9383
9748
  title: name,
@@ -9388,7 +9753,7 @@ function prdCommand(program2) {
9388
9753
  phaseCount
9389
9754
  };
9390
9755
  const content = renderPrdTemplate(vars, customTemplate);
9391
- fs24.writeFileSync(filePath, content, "utf-8");
9756
+ fs25.writeFileSync(filePath, content, "utf-8");
9392
9757
  header("PRD Created");
9393
9758
  success(`Created: ${filePath}`);
9394
9759
  info(`Title: ${name}`);
@@ -9400,15 +9765,15 @@ function prdCommand(program2) {
9400
9765
  prd.command("list").description("List all PRDs with status").option("--json", "Output as JSON").action(async (options) => {
9401
9766
  const projectDir = process.cwd();
9402
9767
  const config = loadConfig(projectDir);
9403
- const absolutePrdDir = path26.join(projectDir, config.prdDir);
9404
- const doneDir = path26.join(absolutePrdDir, "done");
9768
+ const absolutePrdDir = path27.join(projectDir, config.prdDir);
9769
+ const doneDir = path27.join(absolutePrdDir, "done");
9405
9770
  const pending = [];
9406
- if (fs24.existsSync(absolutePrdDir)) {
9407
- 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"));
9408
9773
  for (const file of files) {
9409
- const content = fs24.readFileSync(path26.join(absolutePrdDir, file), "utf-8");
9774
+ const content = fs25.readFileSync(path27.join(absolutePrdDir, file), "utf-8");
9410
9775
  const deps = parseDependencies(content);
9411
- const claimPath = path26.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9776
+ const claimPath = path27.join(absolutePrdDir, file + CLAIM_FILE_EXTENSION);
9412
9777
  const claimStatus = isClaimActive(claimPath, config.maxRuntime);
9413
9778
  pending.push({
9414
9779
  name: file,
@@ -9419,10 +9784,10 @@ function prdCommand(program2) {
9419
9784
  }
9420
9785
  }
9421
9786
  const done = [];
9422
- if (fs24.existsSync(doneDir)) {
9423
- 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"));
9424
9789
  for (const file of files) {
9425
- const content = fs24.readFileSync(path26.join(doneDir, file), "utf-8");
9790
+ const content = fs25.readFileSync(path27.join(doneDir, file), "utf-8");
9426
9791
  const deps = parseDependencies(content);
9427
9792
  done.push({ name: file, dependencies: deps });
9428
9793
  }
@@ -9461,7 +9826,7 @@ import blessed6 from "blessed";
9461
9826
  // src/commands/dashboard/tab-status.ts
9462
9827
  init_dist();
9463
9828
  import blessed from "blessed";
9464
- import * as fs25 from "fs";
9829
+ import * as fs26 from "fs";
9465
9830
  function sortPrdsByPriority(prds, priority) {
9466
9831
  if (priority.length === 0) return prds;
9467
9832
  const priorityMap = /* @__PURE__ */ new Map();
@@ -9557,7 +9922,7 @@ function renderLogPane(projectDir, logs) {
9557
9922
  let newestMtime = 0;
9558
9923
  for (const log of existingLogs) {
9559
9924
  try {
9560
- const stat = fs25.statSync(log.path);
9925
+ const stat = fs26.statSync(log.path);
9561
9926
  if (stat.mtimeMs > newestMtime) {
9562
9927
  newestMtime = stat.mtimeMs;
9563
9928
  newestLog = log;
@@ -11212,8 +11577,8 @@ function createActionsTab() {
11212
11577
  // src/commands/dashboard/tab-logs.ts
11213
11578
  init_dist();
11214
11579
  import blessed5 from "blessed";
11215
- import * as fs26 from "fs";
11216
- import * as path27 from "path";
11580
+ import * as fs27 from "fs";
11581
+ import * as path28 from "path";
11217
11582
  var LOG_NAMES = ["executor", "reviewer"];
11218
11583
  var LOG_LINES = 200;
11219
11584
  function createLogsTab() {
@@ -11254,7 +11619,7 @@ function createLogsTab() {
11254
11619
  let activeKeyHandlers = [];
11255
11620
  let activeCtx = null;
11256
11621
  function getLogPath(projectDir, logName) {
11257
- return path27.join(projectDir, "logs", `${logName}.log`);
11622
+ return path28.join(projectDir, "logs", `${logName}.log`);
11258
11623
  }
11259
11624
  function updateSelector() {
11260
11625
  const tabs = LOG_NAMES.map((name, idx) => {
@@ -11268,7 +11633,7 @@ function createLogsTab() {
11268
11633
  function loadLog(ctx) {
11269
11634
  const logName = LOG_NAMES[selectedLogIndex];
11270
11635
  const logPath = getLogPath(ctx.projectDir, logName);
11271
- if (!fs26.existsSync(logPath)) {
11636
+ if (!fs27.existsSync(logPath)) {
11272
11637
  logContent.setContent(
11273
11638
  `{yellow-fg}No ${logName}.log file found{/yellow-fg}
11274
11639
 
@@ -11278,7 +11643,7 @@ Log will appear here once the ${logName} runs.`
11278
11643
  return;
11279
11644
  }
11280
11645
  try {
11281
- const stat = fs26.statSync(logPath);
11646
+ const stat = fs27.statSync(logPath);
11282
11647
  const sizeKB = (stat.size / 1024).toFixed(1);
11283
11648
  logContent.setLabel(`[ ${logName}.log - ${sizeKB} KB ]`);
11284
11649
  } catch {
@@ -11784,12 +12149,12 @@ function doctorCommand(program2) {
11784
12149
 
11785
12150
  // src/commands/serve.ts
11786
12151
  init_dist();
11787
- import * as fs31 from "fs";
12152
+ import * as fs32 from "fs";
11788
12153
 
11789
12154
  // ../server/dist/index.js
11790
12155
  init_dist();
11791
- import * as fs30 from "fs";
11792
- import * as path33 from "path";
12156
+ import * as fs31 from "fs";
12157
+ import * as path34 from "path";
11793
12158
  import { dirname as dirname7 } from "path";
11794
12159
  import { fileURLToPath as fileURLToPath3 } from "url";
11795
12160
  import cors from "cors";
@@ -11874,8 +12239,8 @@ function setupGracefulShutdown(server, beforeClose) {
11874
12239
 
11875
12240
  // ../server/dist/middleware/project-resolver.middleware.js
11876
12241
  init_dist();
11877
- import * as fs27 from "fs";
11878
- import * as path28 from "path";
12242
+ import * as fs28 from "fs";
12243
+ import * as path29 from "path";
11879
12244
  function resolveProject(req, res, next) {
11880
12245
  const projectId = req.params.projectId;
11881
12246
  const decodedId = decodeURIComponent(projectId).replace(/~/g, "/");
@@ -11885,7 +12250,7 @@ function resolveProject(req, res, next) {
11885
12250
  res.status(404).json({ error: `Project not found: ${decodedId}` });
11886
12251
  return;
11887
12252
  }
11888
- 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))) {
11889
12254
  res.status(404).json({ error: `Project path invalid or missing config: ${entry.path}` });
11890
12255
  return;
11891
12256
  }
@@ -11930,8 +12295,8 @@ function startSseStatusWatcher(clients, projectDir, getConfig) {
11930
12295
 
11931
12296
  // ../server/dist/routes/action.routes.js
11932
12297
  init_dist();
11933
- import * as fs28 from "fs";
11934
- import * as path29 from "path";
12298
+ import * as fs29 from "fs";
12299
+ import * as path30 from "path";
11935
12300
  import { execSync as execSync5, spawn as spawn5 } from "child_process";
11936
12301
  import { Router } from "express";
11937
12302
 
@@ -11969,17 +12334,17 @@ function getBoardProvider(config, projectDir) {
11969
12334
  function cleanOrphanedClaims(dir) {
11970
12335
  let entries;
11971
12336
  try {
11972
- entries = fs28.readdirSync(dir, { withFileTypes: true });
12337
+ entries = fs29.readdirSync(dir, { withFileTypes: true });
11973
12338
  } catch {
11974
12339
  return;
11975
12340
  }
11976
12341
  for (const entry of entries) {
11977
- const fullPath = path29.join(dir, entry.name);
12342
+ const fullPath = path30.join(dir, entry.name);
11978
12343
  if (entry.isDirectory() && entry.name !== "done") {
11979
12344
  cleanOrphanedClaims(fullPath);
11980
12345
  } else if (entry.name.endsWith(CLAIM_FILE_EXTENSION)) {
11981
12346
  try {
11982
- fs28.unlinkSync(fullPath);
12347
+ fs29.unlinkSync(fullPath);
11983
12348
  } catch {
11984
12349
  }
11985
12350
  }
@@ -12076,6 +12441,9 @@ function createActionRouteHandlers(ctx) {
12076
12441
  router.post(`/${p}audit`, (req, res) => {
12077
12442
  spawnAction2(ctx.getProjectDir(req), ["audit"], req, res);
12078
12443
  });
12444
+ router.post(`/${p}analytics`, (req, res) => {
12445
+ spawnAction2(ctx.getProjectDir(req), ["analytics"], req, res);
12446
+ });
12079
12447
  router.post(`/${p}planner`, (req, res) => {
12080
12448
  spawnAction2(ctx.getProjectDir(req), ["planner"], req, res);
12081
12449
  });
@@ -12133,19 +12501,19 @@ function createActionRouteHandlers(ctx) {
12133
12501
  res.status(400).json({ error: "Invalid PRD name" });
12134
12502
  return;
12135
12503
  }
12136
- const prdDir = path29.join(projectDir, config.prdDir);
12504
+ const prdDir = path30.join(projectDir, config.prdDir);
12137
12505
  const normalized = prdName.endsWith(".md") ? prdName : `${prdName}.md`;
12138
- const pendingPath = path29.join(prdDir, normalized);
12139
- const donePath = path29.join(prdDir, "done", normalized);
12140
- if (fs28.existsSync(pendingPath)) {
12506
+ const pendingPath = path30.join(prdDir, normalized);
12507
+ const donePath = path30.join(prdDir, "done", normalized);
12508
+ if (fs29.existsSync(pendingPath)) {
12141
12509
  res.json({ message: `"${normalized}" is already pending` });
12142
12510
  return;
12143
12511
  }
12144
- if (!fs28.existsSync(donePath)) {
12512
+ if (!fs29.existsSync(donePath)) {
12145
12513
  res.status(404).json({ error: `PRD "${normalized}" not found in done/` });
12146
12514
  return;
12147
12515
  }
12148
- fs28.renameSync(donePath, pendingPath);
12516
+ fs29.renameSync(donePath, pendingPath);
12149
12517
  res.json({ message: `Moved "${normalized}" back to pending` });
12150
12518
  } catch (error2) {
12151
12519
  res.status(500).json({
@@ -12163,11 +12531,11 @@ function createActionRouteHandlers(ctx) {
12163
12531
  res.status(409).json({ error: "Executor is actively running \u2014 use Stop instead" });
12164
12532
  return;
12165
12533
  }
12166
- if (fs28.existsSync(lockPath)) {
12167
- fs28.unlinkSync(lockPath);
12534
+ if (fs29.existsSync(lockPath)) {
12535
+ fs29.unlinkSync(lockPath);
12168
12536
  }
12169
- const prdDir = path29.join(projectDir, config.prdDir);
12170
- if (fs28.existsSync(prdDir)) {
12537
+ const prdDir = path30.join(projectDir, config.prdDir);
12538
+ if (fs29.existsSync(prdDir)) {
12171
12539
  cleanOrphanedClaims(prdDir);
12172
12540
  }
12173
12541
  broadcastSSE(ctx.getSseClients(req), "status_changed", await fetchStatusSnapshot(projectDir, config));
@@ -12363,6 +12731,7 @@ function createBoardRouteHandlers(ctx) {
12363
12731
  return;
12364
12732
  }
12365
12733
  await provider.closeIssue(issueNumber);
12734
+ await provider.moveIssue(issueNumber, "Done");
12366
12735
  invalidateBoardCache(projectDir);
12367
12736
  res.json({ closed: true });
12368
12737
  } catch (error2) {
@@ -12669,6 +13038,34 @@ function validateConfigChanges(changes, currentConfig) {
12669
13038
  return "audit.maxRuntime must be a number >= 60";
12670
13039
  }
12671
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
+ }
12672
13069
  if (changes.queue !== void 0) {
12673
13070
  if (typeof changes.queue !== "object" || changes.queue === null) {
12674
13071
  return "queue must be an object";
@@ -12687,7 +13084,14 @@ function validateConfigChanges(changes, currentConfig) {
12687
13084
  if (typeof queue.priority !== "object" || queue.priority === null) {
12688
13085
  return "queue.priority must be an object";
12689
13086
  }
12690
- const validQueueJobs = ["executor", "reviewer", "qa", "audit", "slicer"];
13087
+ const validQueueJobs = [
13088
+ "executor",
13089
+ "reviewer",
13090
+ "qa",
13091
+ "audit",
13092
+ "slicer",
13093
+ "analytics"
13094
+ ];
12691
13095
  for (const [jobType, value] of Object.entries(queue.priority)) {
12692
13096
  if (!validQueueJobs.includes(jobType)) {
12693
13097
  return `queue.priority contains invalid job type: ${jobType}`;
@@ -12823,8 +13227,8 @@ function createProjectConfigRoutes() {
12823
13227
 
12824
13228
  // ../server/dist/routes/doctor.routes.js
12825
13229
  init_dist();
12826
- import * as fs29 from "fs";
12827
- import * as path30 from "path";
13230
+ import * as fs30 from "fs";
13231
+ import * as path31 from "path";
12828
13232
  import { execSync as execSync6 } from "child_process";
12829
13233
  import { Router as Router4 } from "express";
12830
13234
  function runDoctorChecks(projectDir, config) {
@@ -12857,7 +13261,7 @@ function runDoctorChecks(projectDir, config) {
12857
13261
  });
12858
13262
  }
12859
13263
  try {
12860
- const projectName = path30.basename(projectDir);
13264
+ const projectName = path31.basename(projectDir);
12861
13265
  const marker = generateMarker(projectName);
12862
13266
  const crontabEntries = [...getEntries(marker), ...getProjectEntries(projectDir)];
12863
13267
  if (crontabEntries.length > 0) {
@@ -12880,8 +13284,8 @@ function runDoctorChecks(projectDir, config) {
12880
13284
  detail: "Failed to check crontab"
12881
13285
  });
12882
13286
  }
12883
- const configPath = path30.join(projectDir, CONFIG_FILE_NAME);
12884
- if (fs29.existsSync(configPath)) {
13287
+ const configPath = path31.join(projectDir, CONFIG_FILE_NAME);
13288
+ if (fs30.existsSync(configPath)) {
12885
13289
  checks.push({ name: "config", status: "pass", detail: "Config file exists" });
12886
13290
  } else {
12887
13291
  checks.push({
@@ -12890,9 +13294,9 @@ function runDoctorChecks(projectDir, config) {
12890
13294
  detail: "Config file not found (using defaults)"
12891
13295
  });
12892
13296
  }
12893
- const prdDir = path30.join(projectDir, config.prdDir);
12894
- if (fs29.existsSync(prdDir)) {
12895
- 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"));
12896
13300
  checks.push({
12897
13301
  name: "prdDir",
12898
13302
  status: "pass",
@@ -12935,7 +13339,7 @@ function createProjectDoctorRoutes() {
12935
13339
 
12936
13340
  // ../server/dist/routes/log.routes.js
12937
13341
  init_dist();
12938
- import * as path31 from "path";
13342
+ import * as path32 from "path";
12939
13343
  import { Router as Router5 } from "express";
12940
13344
  function createLogRoutes(deps) {
12941
13345
  const { projectDir } = deps;
@@ -12943,7 +13347,7 @@ function createLogRoutes(deps) {
12943
13347
  router.get("/:name", (req, res) => {
12944
13348
  try {
12945
13349
  const { name } = req.params;
12946
- const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
13350
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
12947
13351
  if (!validNames.includes(name)) {
12948
13352
  res.status(400).json({
12949
13353
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -12954,7 +13358,7 @@ function createLogRoutes(deps) {
12954
13358
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12955
13359
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12956
13360
  const fileName = LOG_FILE_NAMES[name] || name;
12957
- const logPath = path31.join(projectDir, LOG_DIR, `${fileName}.log`);
13361
+ const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
12958
13362
  const logLines = getLastLogLines(logPath, linesToRead);
12959
13363
  res.json({ name, lines: logLines });
12960
13364
  } catch (error2) {
@@ -12969,7 +13373,7 @@ function createProjectLogRoutes() {
12969
13373
  try {
12970
13374
  const projectDir = req.projectDir;
12971
13375
  const { name } = req.params;
12972
- const validNames = ["executor", "reviewer", "qa", "audit", "planner"];
13376
+ const validNames = ["executor", "reviewer", "qa", "audit", "planner", "analytics"];
12973
13377
  if (!validNames.includes(name)) {
12974
13378
  res.status(400).json({
12975
13379
  error: `Invalid log name. Must be one of: ${validNames.join(", ")}`
@@ -12980,7 +13384,7 @@ function createProjectLogRoutes() {
12980
13384
  const lines = typeof linesParam === "string" ? parseInt(linesParam, 10) : 200;
12981
13385
  const linesToRead = isNaN(lines) || lines < 1 ? 200 : Math.min(lines, 1e4);
12982
13386
  const fileName = LOG_FILE_NAMES[name] || name;
12983
- const logPath = path31.join(projectDir, LOG_DIR, `${fileName}.log`);
13387
+ const logPath = path32.join(projectDir, LOG_DIR, `${fileName}.log`);
12984
13388
  const logLines = getLastLogLines(logPath, linesToRead);
12985
13389
  res.json({ name, lines: logLines });
12986
13390
  } catch (error2) {
@@ -13015,7 +13419,7 @@ function createProjectPrdRoutes() {
13015
13419
 
13016
13420
  // ../server/dist/routes/roadmap.routes.js
13017
13421
  init_dist();
13018
- import * as path32 from "path";
13422
+ import * as path33 from "path";
13019
13423
  import { Router as Router7 } from "express";
13020
13424
  function createRoadmapRouteHandlers(ctx) {
13021
13425
  const router = Router7({ mergeParams: true });
@@ -13025,7 +13429,7 @@ function createRoadmapRouteHandlers(ctx) {
13025
13429
  const config = ctx.getConfig(req);
13026
13430
  const projectDir = ctx.getProjectDir(req);
13027
13431
  const status = getRoadmapStatus(projectDir, config);
13028
- const prdDir = path32.join(projectDir, config.prdDir);
13432
+ const prdDir = path33.join(projectDir, config.prdDir);
13029
13433
  const state = loadRoadmapState(prdDir);
13030
13434
  res.json({
13031
13435
  ...status,
@@ -13149,11 +13553,13 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
13149
13553
  const qaPlan = getSchedulingPlan(projectDir, config, "qa");
13150
13554
  const auditPlan = getSchedulingPlan(projectDir, config, "audit");
13151
13555
  const plannerPlan = getSchedulingPlan(projectDir, config, "slicer");
13556
+ const analyticsPlan = getSchedulingPlan(projectDir, config, "analytics");
13152
13557
  const executorInstalled = installed && config.executorEnabled !== false && hasScheduledCommand(entries, "run");
13153
13558
  const reviewerInstalled = installed && config.reviewerEnabled && hasScheduledCommand(entries, "review");
13154
13559
  const qaInstalled = installed && config.qa.enabled && hasScheduledCommand(entries, "qa");
13155
13560
  const auditInstalled = installed && config.audit.enabled && hasScheduledCommand(entries, "audit");
13156
13561
  const plannerInstalled = installed && config.roadmapScanner.enabled && (hasScheduledCommand(entries, "planner") || hasScheduledCommand(entries, "slice"));
13562
+ const analyticsInstalled = installed && config.analytics.enabled && hasScheduledCommand(entries, "analytics");
13157
13563
  return {
13158
13564
  executor: {
13159
13565
  schedule: config.cronSchedule,
@@ -13195,6 +13601,14 @@ function buildScheduleInfoResponse(projectDir, config, entries, installed) {
13195
13601
  manualDelayMinutes: plannerPlan.manualDelayMinutes,
13196
13602
  balancedDelayMinutes: plannerPlan.balancedDelayMinutes
13197
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
+ },
13198
13612
  paused: !installed,
13199
13613
  schedulingPriority: config.schedulingPriority,
13200
13614
  entries
@@ -13349,14 +13763,14 @@ function createQueueRoutes(deps) {
13349
13763
  var __filename2 = fileURLToPath3(import.meta.url);
13350
13764
  var __dirname3 = dirname7(__filename2);
13351
13765
  function resolveWebDistPath() {
13352
- const bundled = path33.join(__dirname3, "web");
13353
- if (fs30.existsSync(path33.join(bundled, "index.html")))
13766
+ const bundled = path34.join(__dirname3, "web");
13767
+ if (fs31.existsSync(path34.join(bundled, "index.html")))
13354
13768
  return bundled;
13355
13769
  let d = __dirname3;
13356
13770
  for (let i = 0; i < 8; i++) {
13357
- if (fs30.existsSync(path33.join(d, "turbo.json"))) {
13358
- const dev = path33.join(d, "web/dist");
13359
- 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")))
13360
13774
  return dev;
13361
13775
  break;
13362
13776
  }
@@ -13366,7 +13780,7 @@ function resolveWebDistPath() {
13366
13780
  }
13367
13781
  function setupStaticFiles(app) {
13368
13782
  const webDistPath = resolveWebDistPath();
13369
- if (fs30.existsSync(webDistPath)) {
13783
+ if (fs31.existsSync(webDistPath)) {
13370
13784
  app.use(express.static(webDistPath));
13371
13785
  }
13372
13786
  app.use((req, res, next) => {
@@ -13374,8 +13788,8 @@ function setupStaticFiles(app) {
13374
13788
  next();
13375
13789
  return;
13376
13790
  }
13377
- const indexPath = path33.resolve(webDistPath, "index.html");
13378
- if (fs30.existsSync(indexPath)) {
13791
+ const indexPath = path34.resolve(webDistPath, "index.html");
13792
+ if (fs31.existsSync(indexPath)) {
13379
13793
  res.sendFile(indexPath, (err) => {
13380
13794
  if (err)
13381
13795
  next();
@@ -13491,7 +13905,7 @@ function createGlobalApp() {
13491
13905
  return app;
13492
13906
  }
13493
13907
  function bootContainer() {
13494
- initContainer(path33.dirname(getDbPath()));
13908
+ initContainer(path34.dirname(getDbPath()));
13495
13909
  }
13496
13910
  function startServer(projectDir, port) {
13497
13911
  bootContainer();
@@ -13544,8 +13958,8 @@ function isProcessRunning2(pid) {
13544
13958
  }
13545
13959
  function readPid(lockPath) {
13546
13960
  try {
13547
- if (!fs31.existsSync(lockPath)) return null;
13548
- const raw = fs31.readFileSync(lockPath, "utf-8").trim();
13961
+ if (!fs32.existsSync(lockPath)) return null;
13962
+ const raw = fs32.readFileSync(lockPath, "utf-8").trim();
13549
13963
  const pid = parseInt(raw, 10);
13550
13964
  return Number.isFinite(pid) ? pid : null;
13551
13965
  } catch {
@@ -13557,10 +13971,10 @@ function acquireServeLock(mode, port) {
13557
13971
  let stalePidCleaned;
13558
13972
  for (let attempt = 0; attempt < 2; attempt++) {
13559
13973
  try {
13560
- const fd = fs31.openSync(lockPath, "wx");
13561
- fs31.writeFileSync(fd, `${process.pid}
13974
+ const fd = fs32.openSync(lockPath, "wx");
13975
+ fs32.writeFileSync(fd, `${process.pid}
13562
13976
  `);
13563
- fs31.closeSync(fd);
13977
+ fs32.closeSync(fd);
13564
13978
  return { acquired: true, lockPath, stalePidCleaned };
13565
13979
  } catch (error2) {
13566
13980
  const err = error2;
@@ -13581,7 +13995,7 @@ function acquireServeLock(mode, port) {
13581
13995
  };
13582
13996
  }
13583
13997
  try {
13584
- fs31.unlinkSync(lockPath);
13998
+ fs32.unlinkSync(lockPath);
13585
13999
  if (existingPid) {
13586
14000
  stalePidCleaned = existingPid;
13587
14001
  }
@@ -13604,10 +14018,10 @@ function acquireServeLock(mode, port) {
13604
14018
  }
13605
14019
  function releaseServeLock(lockPath) {
13606
14020
  try {
13607
- if (!fs31.existsSync(lockPath)) return;
14021
+ if (!fs32.existsSync(lockPath)) return;
13608
14022
  const lockPid = readPid(lockPath);
13609
14023
  if (lockPid !== null && lockPid !== process.pid) return;
13610
- fs31.unlinkSync(lockPath);
14024
+ fs32.unlinkSync(lockPath);
13611
14025
  } catch {
13612
14026
  }
13613
14027
  }
@@ -13703,14 +14117,14 @@ function historyCommand(program2) {
13703
14117
  // src/commands/update.ts
13704
14118
  init_dist();
13705
14119
  import { spawnSync } from "child_process";
13706
- import * as fs32 from "fs";
13707
- import * as path34 from "path";
14120
+ import * as fs33 from "fs";
14121
+ import * as path35 from "path";
13708
14122
  var DEFAULT_GLOBAL_SPEC = "@jonit-dev/night-watch-cli@latest";
13709
14123
  function parseProjectDirs(projects, cwd) {
13710
14124
  if (!projects || projects.trim().length === 0) {
13711
14125
  return [cwd];
13712
14126
  }
13713
- 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));
13714
14128
  return Array.from(new Set(dirs));
13715
14129
  }
13716
14130
  function shouldInstallGlobal(options) {
@@ -13752,7 +14166,7 @@ function updateCommand(program2) {
13752
14166
  }
13753
14167
  const nightWatchBin = resolveNightWatchBin();
13754
14168
  for (const projectDir of projectDirs) {
13755
- if (!fs32.existsSync(projectDir) || !fs32.statSync(projectDir).isDirectory()) {
14169
+ if (!fs33.existsSync(projectDir) || !fs33.statSync(projectDir).isDirectory()) {
13756
14170
  warn(`Skipping invalid project directory: ${projectDir}`);
13757
14171
  continue;
13758
14172
  }
@@ -13796,8 +14210,8 @@ function prdStateCommand(program2) {
13796
14210
 
13797
14211
  // src/commands/retry.ts
13798
14212
  init_dist();
13799
- import * as fs33 from "fs";
13800
- import * as path35 from "path";
14213
+ import * as fs34 from "fs";
14214
+ import * as path36 from "path";
13801
14215
  function normalizePrdName(name) {
13802
14216
  if (!name.endsWith(".md")) {
13803
14217
  return `${name}.md`;
@@ -13805,26 +14219,26 @@ function normalizePrdName(name) {
13805
14219
  return name;
13806
14220
  }
13807
14221
  function getDonePrds(doneDir) {
13808
- if (!fs33.existsSync(doneDir)) {
14222
+ if (!fs34.existsSync(doneDir)) {
13809
14223
  return [];
13810
14224
  }
13811
- return fs33.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
14225
+ return fs34.readdirSync(doneDir).filter((f) => f.endsWith(".md"));
13812
14226
  }
13813
14227
  function retryCommand(program2) {
13814
14228
  program2.command("retry <prdName>").description("Move a completed PRD from done/ back to pending").action((prdName) => {
13815
14229
  const projectDir = process.cwd();
13816
14230
  const config = loadConfig(projectDir);
13817
- const prdDir = path35.join(projectDir, config.prdDir);
13818
- const doneDir = path35.join(prdDir, "done");
14231
+ const prdDir = path36.join(projectDir, config.prdDir);
14232
+ const doneDir = path36.join(prdDir, "done");
13819
14233
  const normalizedPrdName = normalizePrdName(prdName);
13820
- const pendingPath = path35.join(prdDir, normalizedPrdName);
13821
- if (fs33.existsSync(pendingPath)) {
14234
+ const pendingPath = path36.join(prdDir, normalizedPrdName);
14235
+ if (fs34.existsSync(pendingPath)) {
13822
14236
  info(`"${normalizedPrdName}" is already pending, nothing to retry.`);
13823
14237
  return;
13824
14238
  }
13825
- const donePath = path35.join(doneDir, normalizedPrdName);
13826
- if (fs33.existsSync(donePath)) {
13827
- fs33.renameSync(donePath, pendingPath);
14239
+ const donePath = path36.join(doneDir, normalizedPrdName);
14240
+ if (fs34.existsSync(donePath)) {
14241
+ fs34.renameSync(donePath, pendingPath);
13828
14242
  success(`Moved "${normalizedPrdName}" back to pending.`);
13829
14243
  dim(`From: ${donePath}`);
13830
14244
  dim(`To: ${pendingPath}`);
@@ -14076,7 +14490,7 @@ function prdsCommand(program2) {
14076
14490
 
14077
14491
  // src/commands/cancel.ts
14078
14492
  init_dist();
14079
- import * as fs34 from "fs";
14493
+ import * as fs35 from "fs";
14080
14494
  import * as readline3 from "readline";
14081
14495
  function getLockFilePaths2(projectDir) {
14082
14496
  const runtimeKey = projectRuntimeKey(projectDir);
@@ -14123,7 +14537,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14123
14537
  const pid = lockStatus.pid;
14124
14538
  if (!lockStatus.running) {
14125
14539
  try {
14126
- fs34.unlinkSync(lockPath);
14540
+ fs35.unlinkSync(lockPath);
14127
14541
  return {
14128
14542
  success: true,
14129
14543
  message: `${processType} is not running (cleaned up stale lock file for PID ${pid})`,
@@ -14161,7 +14575,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14161
14575
  await sleep2(3e3);
14162
14576
  if (!isProcessRunning3(pid)) {
14163
14577
  try {
14164
- fs34.unlinkSync(lockPath);
14578
+ fs35.unlinkSync(lockPath);
14165
14579
  } catch {
14166
14580
  }
14167
14581
  return {
@@ -14196,7 +14610,7 @@ async function cancelProcess2(processType, lockPath, force = false) {
14196
14610
  await sleep2(500);
14197
14611
  if (!isProcessRunning3(pid)) {
14198
14612
  try {
14199
- fs34.unlinkSync(lockPath);
14613
+ fs35.unlinkSync(lockPath);
14200
14614
  } catch {
14201
14615
  }
14202
14616
  return {
@@ -14257,31 +14671,31 @@ function cancelCommand(program2) {
14257
14671
 
14258
14672
  // src/commands/slice.ts
14259
14673
  init_dist();
14260
- import * as fs35 from "fs";
14261
- import * as path36 from "path";
14674
+ import * as fs36 from "fs";
14675
+ import * as path37 from "path";
14262
14676
  function plannerLockPath2(projectDir) {
14263
14677
  return `${LOCK_FILE_PREFIX}slicer-${projectRuntimeKey(projectDir)}.lock`;
14264
14678
  }
14265
14679
  function acquirePlannerLock(projectDir) {
14266
14680
  const lockFile = plannerLockPath2(projectDir);
14267
- if (fs35.existsSync(lockFile)) {
14268
- const pidRaw = fs35.readFileSync(lockFile, "utf-8").trim();
14681
+ if (fs36.existsSync(lockFile)) {
14682
+ const pidRaw = fs36.readFileSync(lockFile, "utf-8").trim();
14269
14683
  const pid = parseInt(pidRaw, 10);
14270
14684
  if (!Number.isNaN(pid) && isProcessRunning(pid)) {
14271
14685
  return { acquired: false, lockFile, pid };
14272
14686
  }
14273
14687
  try {
14274
- fs35.unlinkSync(lockFile);
14688
+ fs36.unlinkSync(lockFile);
14275
14689
  } catch {
14276
14690
  }
14277
14691
  }
14278
- fs35.writeFileSync(lockFile, String(process.pid));
14692
+ fs36.writeFileSync(lockFile, String(process.pid));
14279
14693
  return { acquired: true, lockFile };
14280
14694
  }
14281
14695
  function releasePlannerLock(lockFile) {
14282
14696
  try {
14283
- if (fs35.existsSync(lockFile)) {
14284
- fs35.unlinkSync(lockFile);
14697
+ if (fs36.existsSync(lockFile)) {
14698
+ fs36.unlinkSync(lockFile);
14285
14699
  }
14286
14700
  } catch {
14287
14701
  }
@@ -14290,12 +14704,12 @@ function resolvePlannerIssueColumn(config) {
14290
14704
  return config.roadmapScanner.issueColumn === "Ready" ? "Ready" : "Draft";
14291
14705
  }
14292
14706
  function buildPlannerIssueBody(projectDir, config, result) {
14293
- const relativePrdPath = path36.join(config.prdDir, result.file ?? "").replace(/\\/g, "/");
14294
- 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 ?? "");
14295
14709
  const sourceItem = result.item;
14296
14710
  let prdContent;
14297
14711
  try {
14298
- prdContent = fs35.readFileSync(absolutePrdPath, "utf-8");
14712
+ prdContent = fs36.readFileSync(absolutePrdPath, "utf-8");
14299
14713
  } catch {
14300
14714
  prdContent = `Unable to read generated PRD file at \`${relativePrdPath}\`.`;
14301
14715
  }
@@ -14471,7 +14885,7 @@ function sliceCommand(program2) {
14471
14885
  if (!options.dryRun) {
14472
14886
  await sendNotifications(config, {
14473
14887
  event: "run_started",
14474
- projectName: path36.basename(projectDir),
14888
+ projectName: path37.basename(projectDir),
14475
14889
  exitCode: 0,
14476
14890
  provider: config.provider
14477
14891
  });
@@ -14506,7 +14920,7 @@ function sliceCommand(program2) {
14506
14920
  if (!options.dryRun && result.sliced) {
14507
14921
  await sendNotifications(config, {
14508
14922
  event: "run_succeeded",
14509
- projectName: path36.basename(projectDir),
14923
+ projectName: path37.basename(projectDir),
14510
14924
  exitCode,
14511
14925
  provider: config.provider,
14512
14926
  prTitle: result.item?.title
@@ -14514,7 +14928,7 @@ function sliceCommand(program2) {
14514
14928
  } else if (!options.dryRun && !nothingPending) {
14515
14929
  await sendNotifications(config, {
14516
14930
  event: "run_failed",
14517
- projectName: path36.basename(projectDir),
14931
+ projectName: path37.basename(projectDir),
14518
14932
  exitCode,
14519
14933
  provider: config.provider
14520
14934
  });
@@ -14530,21 +14944,21 @@ function sliceCommand(program2) {
14530
14944
 
14531
14945
  // src/commands/state.ts
14532
14946
  init_dist();
14533
- import * as os7 from "os";
14534
- import * as path37 from "path";
14947
+ import * as os8 from "os";
14948
+ import * as path38 from "path";
14535
14949
  import chalk5 from "chalk";
14536
14950
  import { Command } from "commander";
14537
14951
  function createStateCommand() {
14538
14952
  const state = new Command("state");
14539
14953
  state.description("Manage Night Watch state");
14540
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) => {
14541
- 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);
14542
14956
  if (opts.dryRun) {
14543
14957
  console.log(chalk5.cyan("Dry-run mode: no changes will be made.\n"));
14544
14958
  console.log(`Legacy JSON files that would be migrated from: ${chalk5.bold(nightWatchHome)}`);
14545
- console.log(` ${path37.join(nightWatchHome, "projects.json")}`);
14546
- console.log(` ${path37.join(nightWatchHome, "history.json")}`);
14547
- 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")}`);
14548
14962
  console.log(` <project>/<prdDir>/.roadmap-state.json (per project)`);
14549
14963
  console.log(chalk5.dim("\nRun without --dry-run to apply the migration."));
14550
14964
  return;
@@ -14582,8 +14996,8 @@ function createStateCommand() {
14582
14996
  init_dist();
14583
14997
  init_dist();
14584
14998
  import { execFileSync as execFileSync6 } from "child_process";
14585
- import * as fs36 from "fs";
14586
- import * as path38 from "path";
14999
+ import * as fs37 from "fs";
15000
+ import * as path39 from "path";
14587
15001
  import * as readline4 from "readline";
14588
15002
  import chalk6 from "chalk";
14589
15003
  async function run(fn) {
@@ -14605,7 +15019,7 @@ function getProvider(config, cwd) {
14605
15019
  return createBoardProvider(bp, cwd);
14606
15020
  }
14607
15021
  function defaultBoardTitle(cwd) {
14608
- return `${path38.basename(cwd)} Night Watch`;
15022
+ return `${path39.basename(cwd)} Night Watch`;
14609
15023
  }
14610
15024
  async function ensureBoardConfigured(config, cwd, provider, options) {
14611
15025
  if (config.boardProvider?.projectNumber) {
@@ -14804,11 +15218,11 @@ function boardCommand(program2) {
14804
15218
  let body = options.body ?? "";
14805
15219
  if (options.bodyFile) {
14806
15220
  const filePath = options.bodyFile;
14807
- if (!fs36.existsSync(filePath)) {
15221
+ if (!fs37.existsSync(filePath)) {
14808
15222
  console.error(`File not found: ${filePath}`);
14809
15223
  process.exit(1);
14810
15224
  }
14811
- body = fs36.readFileSync(filePath, "utf-8");
15225
+ body = fs37.readFileSync(filePath, "utf-8");
14812
15226
  }
14813
15227
  const labels = [];
14814
15228
  if (options.label) {
@@ -15030,12 +15444,12 @@ function boardCommand(program2) {
15030
15444
  const config = loadConfig(cwd);
15031
15445
  const provider = getProvider(config, cwd);
15032
15446
  await ensureBoardConfigured(config, cwd, provider);
15033
- const roadmapPath = options.roadmap ?? path38.join(cwd, "ROADMAP.md");
15034
- if (!fs36.existsSync(roadmapPath)) {
15447
+ const roadmapPath = options.roadmap ?? path39.join(cwd, "ROADMAP.md");
15448
+ if (!fs37.existsSync(roadmapPath)) {
15035
15449
  console.error(`Roadmap file not found: ${roadmapPath}`);
15036
15450
  process.exit(1);
15037
15451
  }
15038
- const roadmapContent = fs36.readFileSync(roadmapPath, "utf-8");
15452
+ const roadmapContent = fs37.readFileSync(roadmapPath, "utf-8");
15039
15453
  const items = parseRoadmap(roadmapContent);
15040
15454
  const uncheckedItems = getUncheckedItems(items);
15041
15455
  if (uncheckedItems.length === 0) {
@@ -15159,11 +15573,11 @@ function boardCommand(program2) {
15159
15573
  // src/commands/queue.ts
15160
15574
  init_dist();
15161
15575
  init_dist();
15162
- import * as path39 from "path";
15576
+ import * as path40 from "path";
15163
15577
  import { spawn as spawn6 } from "child_process";
15164
15578
  import chalk7 from "chalk";
15165
15579
  import { Command as Command2 } from "commander";
15166
- var logger2 = createLogger("queue");
15580
+ var logger4 = createLogger("queue");
15167
15581
  var VALID_JOB_TYPES2 = ["executor", "reviewer", "qa", "audit", "slicer"];
15168
15582
  function formatTimestamp(unixTs) {
15169
15583
  if (unixTs === null) return "-";
@@ -15279,7 +15693,7 @@ function createQueueCommand() {
15279
15693
  process.exit(1);
15280
15694
  }
15281
15695
  }
15282
- const projectName = path39.basename(projectDir);
15696
+ const projectName = path40.basename(projectDir);
15283
15697
  const queueConfig = loadConfig(projectDir).queue;
15284
15698
  const id = enqueueJob(projectDir, projectName, jobType, envVars, queueConfig);
15285
15699
  console.log(chalk7.green(`Enqueued ${jobType} for ${projectName} (ID: ${id})`));
@@ -15287,13 +15701,13 @@ function createQueueCommand() {
15287
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) => {
15288
15702
  const entry = dispatchNextJob(loadConfig(process.cwd()).queue);
15289
15703
  if (!entry) {
15290
- logger2.info("No pending jobs to dispatch");
15704
+ logger4.info("No pending jobs to dispatch");
15291
15705
  return;
15292
15706
  }
15293
- logger2.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
15707
+ logger4.info(`Dispatching ${entry.jobType} for ${entry.projectName} (ID: ${entry.id})`);
15294
15708
  const scriptName = getScriptNameForJobType(entry.jobType);
15295
15709
  if (!scriptName) {
15296
- logger2.error(`Unknown job type: ${entry.jobType}`);
15710
+ logger4.error(`Unknown job type: ${entry.jobType}`);
15297
15711
  return;
15298
15712
  }
15299
15713
  let projectEnv;
@@ -15312,7 +15726,7 @@ function createQueueCommand() {
15312
15726
  NW_QUEUE_ENTRY_ID: String(entry.id)
15313
15727
  };
15314
15728
  const scriptPath = getScriptPath(scriptName);
15315
- logger2.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15729
+ logger4.info(`Spawning: ${scriptPath} ${entry.projectPath}`);
15316
15730
  try {
15317
15731
  const child = spawn6("bash", [scriptPath, entry.projectPath], {
15318
15732
  detached: true,
@@ -15321,11 +15735,11 @@ function createQueueCommand() {
15321
15735
  cwd: entry.projectPath
15322
15736
  });
15323
15737
  child.unref();
15324
- logger2.info(`Spawned PID: ${child.pid}`);
15738
+ logger4.info(`Spawned PID: ${child.pid}`);
15325
15739
  markJobRunning(entry.id);
15326
15740
  } catch (error2) {
15327
15741
  updateJobStatus(entry.id, "pending");
15328
- logger2.error(
15742
+ logger4.error(
15329
15743
  `Failed to dispatch ${entry.jobType} for ${entry.projectName}: ${error2 instanceof Error ? error2.message : String(error2)}`
15330
15744
  );
15331
15745
  process.exit(1);
@@ -15439,13 +15853,13 @@ var __dirname4 = dirname8(__filename3);
15439
15853
  function findPackageRoot(dir) {
15440
15854
  let d = dir;
15441
15855
  for (let i = 0; i < 5; i++) {
15442
- if (existsSync29(join34(d, "package.json"))) return d;
15856
+ if (existsSync29(join35(d, "package.json"))) return d;
15443
15857
  d = dirname8(d);
15444
15858
  }
15445
15859
  return dir;
15446
15860
  }
15447
15861
  var packageRoot = findPackageRoot(__dirname4);
15448
- var packageJson = JSON.parse(readFileSync18(join34(packageRoot, "package.json"), "utf-8"));
15862
+ var packageJson = JSON.parse(readFileSync18(join35(packageRoot, "package.json"), "utf-8"));
15449
15863
  var program = new Command3();
15450
15864
  program.name("night-watch").description("Autonomous PRD execution using Claude CLI + cron").version(packageJson.version);
15451
15865
  initCommand(program);
@@ -15453,6 +15867,7 @@ runCommand(program);
15453
15867
  reviewCommand(program);
15454
15868
  qaCommand(program);
15455
15869
  auditCommand(program);
15870
+ analyticsCommand(program);
15456
15871
  installCommand(program);
15457
15872
  uninstallCommand(program);
15458
15873
  statusCommand(program);